├── .gitignore ├── .travis.yml ├── README.md ├── pom.xml └── src ├── main └── java │ └── org │ └── jetlinks │ └── registry │ └── redis │ ├── CompositeDeviceMessageSenderInterceptor.java │ ├── NullValue.java │ ├── RedissonDeviceMessageHandler.java │ ├── RedissonDeviceMessageSender.java │ ├── RedissonDeviceOperation.java │ ├── RedissonDeviceProductOperation.java │ ├── RedissonDeviceRegistry.java │ └── lettuce │ ├── LettuceDeviceMessageHandler.java │ ├── LettuceDeviceMessageSender.java │ ├── LettuceDeviceOperation.java │ ├── LettuceDeviceProductOperation.java │ └── LettuceDeviceRegistry.java └── test ├── java └── org │ └── jetlinks │ └── registry │ └── redis │ ├── MockProtocolSupports.java │ ├── RedissonDeviceOperationTest.java │ ├── RedissonDeviceRegistryTest.java │ ├── RedissonHelper.java │ └── lettuce │ ├── LettuceDeviceOperationTest.java │ ├── LettuceDeviceRegistryTest.java │ └── RedisClientHelper.java └── resources ├── logback.xml └── testValidateParameter.meta.json /.gitignore: -------------------------------------------------------------------------------- 1 | **/pom.xml.versionsBackup 2 | **/target/ 3 | **/out/ 4 | *.class 5 | # Mobile Tools for Java (J2ME) 6 | .mtj.tmp/ 7 | .idea/ 8 | /nbproject 9 | *.ipr 10 | *.iws 11 | *.iml 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.ear 17 | *.log 18 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 19 | hs_err_pid* 20 | **/transaction-logs/ 21 | !/.mvn/wrapper/maven-wrapper.jar 22 | /data/ 23 | *.db 24 | /static/ 25 | /upload 26 | /ui/upload/ 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: false 3 | jdk: 4 | - openjdk8 5 | service: 6 | - redis-server 7 | before_script: 8 | - sudo redis-server /etc/redis/redis.conf --port 6379 9 | script: 10 | - mvn -Dredis.host=redis://127.0.0.1:6379 test 11 | after_success: 12 | - bash <(curl -s https://codecov.io/bash) 13 | cache: 14 | directories: 15 | - '$HOME/.m2/repository' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jetlinks-registry-redis 2 | 设备注册中心redis实现 3 | 4 | [![Maven Central](https://img.shields.io/maven-central/v/org.jetlinks/jetlinks-registry-redis.svg)](http://search.maven.org/#search%7Cga%7C1%7Cjetlinks-registry-redis) 5 | [![Maven metadata URL](https://img.shields.io/maven-metadata/v/https/oss.sonatype.org/content/repositories/snapshots/org/jetlinks/jetlinks-registry-redis/maven-metadata.xml.svg)](https://oss.sonatype.org/content/repositories/snapshots/org/jetlinks/jetlinks-registry-redis) 6 | [![Build Status](https://travis-ci.com/jetlinks/jetlinks-registry-redis.svg?branch=master)](https://travis-ci.com/jetlinks/jetlinks-registry-redis) 7 | [![codecov](https://codecov.io/gh/jetlinks/jetlinks-registry-redis/branch/master/graph/badge.svg)](https://codecov.io/gh/jetlinks/jetlinks-registry-redis) 8 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | jetlinks 7 | org.jetlinks 8 | 1.0.0-BUILD-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | jetlinks-registry-redis 13 | JetLinks Device Registry Redis Implement 14 | 基于Redis的设备注册中心 15 | 16 | 17 | 18 | org.jetlinks 19 | jetlinks-core 20 | ${project.version} 21 | 22 | 23 | 24 | org.redisson 25 | redisson 26 | true 27 | 28 | 29 | 30 | org.jetlinks 31 | lettuce-plus-core 32 | true 33 | 34 | 35 | 36 | io.projectreactor 37 | reactor-core 38 | true 39 | 40 | 41 | 42 | org.jetlinks 43 | jetlinks-supports 44 | ${project.version} 45 | 46 | 47 | 48 | io.netty 49 | netty-transport-native-epoll 50 | linux-x86_64 51 | test 52 | 53 | 54 | 55 | io.netty 56 | netty-transport-native-kqueue 57 | osx-x86_64 58 | test 59 | 60 | 61 | 62 | org.springframework 63 | spring-core 64 | test 65 | 66 | 67 | 68 | 69 | 70 | 71 | hsweb-nexus 72 | Nexus Release Repository 73 | http://nexus.hsweb.me/content/groups/public/ 74 | 75 | true 76 | always 77 | 78 | 79 | 80 | aliyun-nexus 81 | aliyun 82 | http://maven.aliyun.com/nexus/content/groups/public/ 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/registry/redis/CompositeDeviceMessageSenderInterceptor.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.registry.redis; 2 | 3 | import org.jetlinks.core.device.DeviceOperation; 4 | import org.jetlinks.core.message.DeviceMessage; 5 | import org.jetlinks.core.message.DeviceMessageReply; 6 | import org.jetlinks.core.message.interceptor.DeviceMessageSenderInterceptor; 7 | 8 | import java.util.List; 9 | import java.util.concurrent.CompletableFuture; 10 | import java.util.concurrent.CompletionStage; 11 | import java.util.concurrent.CopyOnWriteArrayList; 12 | 13 | public class CompositeDeviceMessageSenderInterceptor implements DeviceMessageSenderInterceptor { 14 | private List interceptors = new CopyOnWriteArrayList<>(); 15 | 16 | public void addInterceptor(DeviceMessageSenderInterceptor interceptor) { 17 | interceptors.add(interceptor); 18 | } 19 | 20 | @Override 21 | public DeviceMessage preSend(DeviceOperation device, DeviceMessage message) { 22 | for (DeviceMessageSenderInterceptor interceptor : interceptors) { 23 | message = interceptor.preSend(device, message); 24 | } 25 | return message; 26 | } 27 | 28 | @Override 29 | public CompletionStage afterReply(DeviceOperation device, DeviceMessage message, R reply) { 30 | 31 | CompletableFuture future = CompletableFuture.completedFuture(reply); 32 | 33 | for (DeviceMessageSenderInterceptor interceptor : interceptors) { 34 | future = future.thenCompose(r -> interceptor.afterReply(device, message, r)); 35 | } 36 | 37 | return future; 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/registry/redis/NullValue.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.registry.redis; 2 | 3 | public class NullValue { 4 | public static final NullValue instance = new NullValue(); 5 | 6 | } -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/registry/redis/RedissonDeviceMessageHandler.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.registry.redis; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.apache.commons.lang.StringUtils; 8 | import org.jetlinks.core.device.registry.DeviceMessageHandler; 9 | import org.jetlinks.core.enums.ErrorCode; 10 | import org.jetlinks.core.message.DeviceMessage; 11 | import org.jetlinks.core.message.DeviceMessageReply; 12 | import org.redisson.api.RBucket; 13 | import org.redisson.api.RSemaphore; 14 | import org.redisson.api.RTopic; 15 | import org.redisson.api.RedissonClient; 16 | 17 | import java.util.Map; 18 | import java.util.Optional; 19 | import java.util.concurrent.*; 20 | import java.util.function.Consumer; 21 | 22 | /** 23 | * @author zhouhao 24 | * @since 1.0.0 25 | */ 26 | @Slf4j 27 | public class RedissonDeviceMessageHandler implements DeviceMessageHandler { 28 | private RedissonClient redissonClient; 29 | 30 | private Map futureMap = new ConcurrentHashMap<>(); 31 | 32 | @Getter 33 | @Setter 34 | private long replyExpireTimeSeconds = Long.getLong("device.message.reply.expire-time-seconds", TimeUnit.MINUTES.toSeconds(3)); 35 | 36 | @Getter 37 | @Setter 38 | private long asyncFlagExpireTimeSeconds = Long.getLong("device.message.async-flag.expire-time-seconds", TimeUnit.MINUTES.toSeconds(30)); 39 | 40 | private RTopic replyTopic; 41 | 42 | private Map> localConsumer = new ConcurrentHashMap<>(); 43 | 44 | 45 | public RedissonDeviceMessageHandler(RedissonClient redissonClient) { 46 | this(redissonClient, Executors.newSingleThreadScheduledExecutor()); 47 | } 48 | 49 | public RedissonDeviceMessageHandler(RedissonClient redissonClient, ScheduledExecutorService executorService) { 50 | this.redissonClient = redissonClient; 51 | replyTopic = this.redissonClient.getTopic("device:message:reply"); 52 | 53 | replyTopic.addListener(String.class, (channel, msg) -> Optional.ofNullable(futureMap.remove(msg)) 54 | .map(MessageFuture::getFuture) 55 | .ifPresent(future -> { 56 | if (!future.isCancelled()) { 57 | CompletableFuture 58 | .supplyAsync(() -> redissonClient.getBucket("device:message:reply:".concat(msg)).getAndDelete()) 59 | .whenComplete((data, error) -> { 60 | if (error != null) { 61 | future.completeExceptionally(error); 62 | } else { 63 | future.complete(data); 64 | } 65 | }); 66 | } 67 | })); 68 | 69 | executorService.scheduleAtFixedRate(() -> { 70 | futureMap.entrySet() 71 | .stream() 72 | .filter(e -> System.currentTimeMillis() > e.getValue().expireTime) 73 | .forEach((e) -> { 74 | try { 75 | CompletableFuture future = e.getValue().future; 76 | if (future.isCancelled()) { 77 | return; 78 | } 79 | redissonClient.getBucket("device:message:reply:".concat(e.getKey())) 80 | .getAndDeleteAsync() 81 | .whenComplete((o, throwable) -> { 82 | if (o != null) { 83 | future.complete(o); 84 | } else { 85 | future.complete(ErrorCode.TIME_OUT); 86 | } 87 | }); 88 | 89 | } finally { 90 | log.info("设备消息[{}]超时未返回", e.getKey()); 91 | futureMap.remove(e.getKey()); 92 | } 93 | }); 94 | }, 1, 5, TimeUnit.SECONDS); 95 | } 96 | 97 | @Override 98 | public void handleDeviceCheck(String serviceId, Consumer consumer) { 99 | redissonClient 100 | .getTopic("device:state:check:".concat(serviceId)) 101 | .addListener(String.class, (channel, msg) -> { 102 | if (StringUtils.isEmpty(msg)) { 103 | return; 104 | } 105 | consumer.accept(msg); 106 | RSemaphore semaphore = redissonClient 107 | .getSemaphore("device:state:check:semaphore:".concat(msg)); 108 | semaphore.expireAsync(5, TimeUnit.SECONDS) 109 | .thenRun(semaphore::releaseAsync); 110 | }); 111 | } 112 | 113 | @Override 114 | public void handleMessage(String serverId, Consumer deviceMessageConsumer) { 115 | localConsumer.put(serverId, deviceMessageConsumer); 116 | redissonClient.getTopic("device:message:accept:".concat(serverId)) 117 | .addListener(DeviceMessage.class, (channel, message) -> { 118 | if (log.isDebugEnabled()) { 119 | log.debug("接收到发往设备的消息:{}", message.toJson()); 120 | } 121 | deviceMessageConsumer.accept(message); 122 | }); 123 | } 124 | 125 | @AllArgsConstructor 126 | @Getter 127 | private class MessageFuture { 128 | private String messageId; 129 | 130 | private CompletableFuture future; 131 | 132 | private long expireTime; 133 | } 134 | 135 | @Override 136 | public CompletionStage handleReply(String messageId, long timeout, TimeUnit timeUnit) { 137 | CompletableFuture future = new CompletableFuture<>(); 138 | futureMap.put(messageId, new MessageFuture(messageId, future, System.currentTimeMillis() + timeUnit.toMillis(timeout))); 139 | 140 | return future; 141 | } 142 | 143 | public CompletionStage send(String serverId, DeviceMessage message) { 144 | Consumer consumer = localConsumer.get(serverId); 145 | if (consumer != null) { 146 | consumer.accept(message); 147 | return CompletableFuture.completedFuture(1L); 148 | } 149 | return redissonClient.getTopic("device:message:accept:".concat(serverId)) 150 | .publishAsync(message); 151 | } 152 | 153 | @Override 154 | public CompletionStage reply(DeviceMessageReply message) { 155 | MessageFuture future = futureMap.get(message.getMessageId()); 156 | 157 | if (null != future) { 158 | futureMap.remove(message.getMessageId()); 159 | future.getFuture().complete(message); 160 | return CompletableFuture.completedFuture(true); 161 | } 162 | 163 | RBucket bucket = redissonClient.getBucket("device:message:reply:".concat(message.getMessageId())); 164 | 165 | return CompletableFuture.runAsync(() -> bucket.set(message, replyExpireTimeSeconds, TimeUnit.SECONDS)) 166 | .thenApply(nil -> true) 167 | .whenComplete((success, error) -> { 168 | long num = replyTopic.publish(message.getMessageId()); 169 | if (num <= 0) { 170 | log.warn("消息回复[{}]没有任何服务消费", message.getMessageId()); 171 | } 172 | }); 173 | 174 | } 175 | 176 | @Override 177 | public CompletionStage markMessageAsync(String messageId) { 178 | RBucket bucket = redissonClient.getBucket("async-msg:".concat(messageId)); 179 | return bucket.setAsync(true, asyncFlagExpireTimeSeconds, TimeUnit.SECONDS); 180 | } 181 | 182 | @Override 183 | public CompletionStage messageIsAsync(String messageId, boolean reset) { 184 | RBucket bucket = redissonClient.getBucket("async-msg:".concat(messageId)); 185 | if (reset) { 186 | return bucket.getAndDeleteAsync(); 187 | } else { 188 | return bucket.getAsync() 189 | .whenComplete((s, err) -> { 190 | if (s != null) { 191 | bucket.expireAsync(replyExpireTimeSeconds, TimeUnit.SECONDS); 192 | } 193 | }); 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/registry/redis/RedissonDeviceMessageSender.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.registry.redis; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.jetlinks.core.device.DeviceMessageSender; 8 | import org.jetlinks.core.device.DeviceOperation; 9 | import org.jetlinks.core.device.registry.DeviceMessageHandler; 10 | import org.jetlinks.core.enums.ErrorCode; 11 | import org.jetlinks.core.message.*; 12 | import org.jetlinks.core.message.exception.FunctionUndefinedException; 13 | import org.jetlinks.core.message.exception.ParameterUndefinedException; 14 | import org.jetlinks.core.message.function.FunctionInvokeMessage; 15 | import org.jetlinks.core.message.function.FunctionInvokeMessageReply; 16 | import org.jetlinks.core.message.function.FunctionParameter; 17 | import org.jetlinks.core.message.interceptor.DeviceMessageSenderInterceptor; 18 | import org.jetlinks.core.message.property.ReadPropertyMessage; 19 | import org.jetlinks.core.message.property.ReadPropertyMessageReply; 20 | import org.jetlinks.core.message.property.WritePropertyMessage; 21 | import org.jetlinks.core.message.property.WritePropertyMessageReply; 22 | import org.jetlinks.core.metadata.FunctionMetadata; 23 | import org.jetlinks.core.metadata.PropertyMetadata; 24 | import org.jetlinks.core.metadata.ValidateResult; 25 | import org.jetlinks.core.utils.IdUtils; 26 | import org.redisson.api.RedissonClient; 27 | 28 | import java.util.*; 29 | import java.util.concurrent.CompletableFuture; 30 | import java.util.concurrent.CompletionStage; 31 | import java.util.concurrent.TimeUnit; 32 | import java.util.function.BiConsumer; 33 | import java.util.function.Consumer; 34 | import java.util.function.Function; 35 | import java.util.function.Supplier; 36 | import java.util.stream.Collectors; 37 | 38 | import static org.jetlinks.core.enums.ErrorCode.NO_REPLY; 39 | 40 | /** 41 | * @author zhouhao 42 | * @since 1.0.0 43 | */ 44 | @Slf4j 45 | public class RedissonDeviceMessageSender implements DeviceMessageSender { 46 | 47 | private RedissonClient redissonClient; 48 | 49 | protected Supplier connectionServerIdSupplier; 50 | 51 | protected Supplier> deviceStateChecker; 52 | 53 | protected String deviceId; 54 | 55 | protected DeviceOperation operation; 56 | 57 | //从元数据中获取异步 58 | @Getter 59 | @Setter 60 | private boolean asyncFromMetadata = Boolean.getBoolean("device.message.async.from-metadata"); 61 | 62 | @Getter 63 | @Setter 64 | protected DeviceMessageSenderInterceptor interceptor; 65 | 66 | private DeviceMessageHandler messageHandler; 67 | 68 | public RedissonDeviceMessageSender(String deviceId, 69 | RedissonClient redissonClient, 70 | DeviceMessageHandler messageHandler, 71 | DeviceOperation operation) { 72 | this.redissonClient = redissonClient; 73 | this.operation = operation; 74 | this.messageHandler = messageHandler; 75 | this.connectionServerIdSupplier = operation::getServerId; 76 | this.deviceStateChecker = operation::checkState; 77 | this.deviceId = deviceId; 78 | } 79 | 80 | //最大等待30秒 81 | @Getter 82 | @Setter 83 | private int maxSendAwaitSeconds = Integer.getInteger("device.message.await.max-seconds", 30); 84 | 85 | @SuppressWarnings("all") 86 | protected R convertReply(Object deviceReply, RepayableDeviceMessage message, Supplier replyNewInstance) { 87 | R reply = replyNewInstance.get(); 88 | if (deviceReply == null) { 89 | reply.error(NO_REPLY); 90 | } else if (deviceReply instanceof ErrorCode) { 91 | reply.error((ErrorCode) deviceReply); 92 | } else { 93 | if (reply.getClass().isAssignableFrom(deviceReply.getClass())) { 94 | return reply = (R) deviceReply; 95 | } else if (deviceReply instanceof String) { 96 | reply.fromJson(JSON.parseObject(String.valueOf(deviceReply))); 97 | } else if (deviceReply instanceof DeviceMessage) { 98 | reply.fromJson(((DeviceMessage) deviceReply).toJson()); 99 | } else { 100 | reply.error(ErrorCode.UNSUPPORTED_MESSAGE); 101 | log.warn("不支持的消息类型:{}", deviceReply.getClass()); 102 | } 103 | } 104 | if (message != null) { 105 | reply.from(message); 106 | } 107 | return reply; 108 | } 109 | 110 | 111 | public CompletionStage retrieveReply(String messageId, Supplier replyNewInstance) { 112 | return redissonClient 113 | .getBucket("device:message:reply:".concat(messageId)) 114 | .getAndDeleteAsync() 115 | .thenApply(deviceReply -> { 116 | R reply = convertReply(deviceReply, null, replyNewInstance); 117 | reply.messageId(messageId); 118 | return reply; 119 | }); 120 | } 121 | 122 | @Override 123 | @SuppressWarnings("all") 124 | public CompletionStage send(DeviceMessage requestMessage, Function replyMapping) { 125 | String serverId = connectionServerIdSupplier.get(); 126 | //设备当前没有连接到任何服务器 127 | if (serverId == null) { 128 | R reply = replyMapping.apply(null); 129 | if (reply != null) { 130 | reply.error(ErrorCode.CLIENT_OFFLINE); 131 | } 132 | if (null != reply) { 133 | reply.from(requestMessage); 134 | } 135 | return CompletableFuture.completedFuture(reply); 136 | } 137 | CompletableFuture future = new CompletableFuture<>(); 138 | if (interceptor != null) { 139 | requestMessage = interceptor.preSend(operation, requestMessage); 140 | } 141 | DeviceMessage message = requestMessage; 142 | //标记异步 143 | if (Headers.async.get(message).asBoolean().orElse(false)) { 144 | messageHandler.markMessageAsync(message.getMessageId()) 145 | .whenComplete((nil, error) -> { 146 | if (error != null) { 147 | log.error("设置异步消息标识[{}]失败", message, error); 148 | } 149 | }); 150 | } 151 | //在头中标记超时 152 | int timeout = message.getHeader("timeout") 153 | .map(Number.class::cast) 154 | .map(Number::intValue) 155 | .orElse(maxSendAwaitSeconds); 156 | 157 | //处理返回结果,_reply可能为null,ErroCode,Object 158 | BiConsumer doReply = (_reply, error) -> { 159 | 160 | CompletionStage reply = CompletableFuture 161 | .completedFuture(replyMapping.apply(error != null ? ErrorCode.SYSTEM_ERROR : _reply)) 162 | .thenApply(r -> { 163 | if (error != null) { 164 | r.addHeader("error", error.getMessage()); 165 | } 166 | return r; 167 | }); 168 | 169 | if (interceptor != null) { 170 | reply = reply.thenCompose(r -> interceptor.afterReply(operation, message, r)); 171 | } 172 | reply.whenComplete((r, throwable) -> { 173 | if (throwable != null) {//理论上不会出现 174 | future.completeExceptionally(throwable); 175 | } else { 176 | future.complete(r); 177 | } 178 | }); 179 | }; 180 | 181 | //监听返回 182 | CompletionStage stage = messageHandler.handleReply(message.getMessageId(), timeout, TimeUnit.SECONDS); 183 | 184 | stage.whenComplete(doReply); 185 | 186 | future.whenComplete((r, err) -> { 187 | if (future.isCancelled()) { 188 | log.info("取消等待设备[{}]消息[{}]返回", deviceId, message.getMessageId()); 189 | stage.toCompletableFuture().cancel(true); 190 | } 191 | }); 192 | //发送消息 193 | messageHandler.send(serverId, message) 194 | .whenComplete((deviceConnectedServerNumber, error) -> { 195 | if (error != null) { 196 | log.error("发送消息到设备网关服务失败:{}", message, error); 197 | doReply.accept(ErrorCode.SYSTEM_ERROR, error); 198 | return; 199 | } 200 | if (deviceConnectedServerNumber <= 0) { 201 | //没有任何服务消费此topic,可能所在服务器已经宕机,注册信息没有更新。 202 | //执行设备状态检查,尝试更新设备的真实状态 203 | if (deviceStateChecker != null) { 204 | deviceStateChecker.get() 205 | .whenComplete((state, err) -> { 206 | doReply.accept(ErrorCode.CLIENT_OFFLINE, err); 207 | }); 208 | } else { 209 | doReply.accept(ErrorCode.CLIENT_OFFLINE, null); 210 | } 211 | 212 | } 213 | //有多个相同名称的设备网关服务,可能是服务配置错误,启动了多台相同id的服务。 214 | if (deviceConnectedServerNumber > 1) { 215 | log.warn("存在多个相同的网关服务:{}", serverId); 216 | } 217 | }); 218 | 219 | return future; 220 | } 221 | 222 | @Override 223 | @SuppressWarnings("all") 224 | public CompletionStage send(RepayableDeviceMessage message) { 225 | return send(message, deviceReply -> convertReply(deviceReply, message, message::newReply)); 226 | } 227 | 228 | @Override 229 | public FunctionInvokeMessageSender invokeFunction(String function) { 230 | Objects.requireNonNull(function, "function"); 231 | FunctionInvokeMessage message = new FunctionInvokeMessage(); 232 | message.setTimestamp(System.currentTimeMillis()); 233 | message.setDeviceId(deviceId); 234 | message.setFunctionId(function); 235 | message.setMessageId(IdUtils.newUUID()); 236 | return new FunctionInvokeMessageSender() { 237 | boolean markAsync = false; 238 | 239 | @Override 240 | public FunctionInvokeMessageSender addParameter(String name, Object value) { 241 | message.addInput(name, value); 242 | return this; 243 | } 244 | 245 | @Override 246 | public FunctionInvokeMessageSender custom(Consumer messageConsumer) { 247 | messageConsumer.accept(message); 248 | return this; 249 | } 250 | 251 | @Override 252 | public FunctionInvokeMessageSender messageId(String messageId) { 253 | message.setMessageId(messageId); 254 | return this; 255 | } 256 | 257 | @Override 258 | public FunctionInvokeMessageSender setParameter(List parameter) { 259 | message.setInputs(parameter); 260 | return this; 261 | } 262 | 263 | @Override 264 | public FunctionInvokeMessageSender validate(BiConsumer resultConsumer) { 265 | //获取功能定义 266 | FunctionMetadata functionMetadata = operation.getMetadata().getFunction(function) 267 | .orElseThrow(() -> new FunctionUndefinedException(function, "功能[" + function + "]未定义")); 268 | List metadataInputs = functionMetadata.getInputs(); 269 | List inputs = message.getInputs(); 270 | 271 | if (inputs.size() != metadataInputs.size()) { 272 | 273 | log.warn("调用设备功能[{}]参数数量[需要{},传入{}]错误,功能:{}", function, metadataInputs.size(), inputs.size(), functionMetadata.toString()); 274 | throw new IllegalArgumentException("参数数量错误"); 275 | } 276 | 277 | //参数定义转为map,避免n*n循环 278 | Map properties = metadataInputs.stream() 279 | .collect(Collectors.toMap(PropertyMetadata::getId, Function.identity(), (t1, t2) -> t1)); 280 | 281 | for (FunctionParameter input : message.getInputs()) { 282 | PropertyMetadata metadata = Optional.ofNullable(properties.get(input.getName())) 283 | .orElseThrow(() -> new ParameterUndefinedException(input.getName(), "参数[" + input.getName() + "]未定义")); 284 | resultConsumer.accept(input, metadata.getValueType().validate(input.getValue())); 285 | } 286 | 287 | return this; 288 | } 289 | 290 | @Override 291 | public FunctionInvokeMessageSender header(String header, Object value) { 292 | message.addHeader(header, value); 293 | return this; 294 | } 295 | 296 | @Override 297 | public FunctionInvokeMessageSender async(Boolean async) { 298 | if (async != null) { 299 | custom(async ? Headers.async.setter() : Headers.async.clear()); 300 | } 301 | markAsync = true; 302 | return this; 303 | } 304 | 305 | @Override 306 | public CompletionStage retrieveReply() { 307 | return RedissonDeviceMessageSender.this.retrieveReply( 308 | Objects.requireNonNull(message.getMessageId(), "messageId can not be null"), 309 | FunctionInvokeMessageReply::new); 310 | } 311 | 312 | @Override 313 | public CompletionStage send() { 314 | 315 | //如果未明确指定是否异步,则获取元数据中定义的异步配置 316 | if (!markAsync && asyncFromMetadata && message.getHeader(Headers.async.getHeader()).isPresent()) { 317 | message.addHeader(Headers.async.getHeader(), 318 | operation.getMetadata() 319 | .getFunction(message.getFunctionId()) 320 | .map(FunctionMetadata::isAsync) 321 | .orElse(false)); 322 | } 323 | return RedissonDeviceMessageSender.this.send(message); 324 | } 325 | 326 | }; 327 | } 328 | 329 | @Override 330 | public ReadPropertyMessageSender readProperty(String... property) { 331 | 332 | ReadPropertyMessage message = new ReadPropertyMessage(); 333 | message.setDeviceId(deviceId); 334 | message.addProperties(Arrays.asList(property)); 335 | message.setMessageId(IdUtils.newUUID()); 336 | return new ReadPropertyMessageSender() { 337 | @Override 338 | public ReadPropertyMessageSender messageId(String messageId) { 339 | message.setMessageId(messageId); 340 | return this; 341 | } 342 | 343 | @Override 344 | public ReadPropertyMessageSender custom(Consumer messageConsumer) { 345 | messageConsumer.accept(message); 346 | return this; 347 | } 348 | 349 | @Override 350 | public ReadPropertyMessageSender read(List properties) { 351 | message.addProperties(properties); 352 | return this; 353 | } 354 | 355 | @Override 356 | public ReadPropertyMessageSender header(String header, Object value) { 357 | message.addHeader(header, value); 358 | return this; 359 | } 360 | 361 | @Override 362 | public CompletionStage retrieveReply() { 363 | return RedissonDeviceMessageSender.this.retrieveReply( 364 | Objects.requireNonNull(message.getMessageId(), "messageId can not be null"), 365 | ReadPropertyMessageReply::new); 366 | } 367 | 368 | @Override 369 | public CompletionStage send() { 370 | return RedissonDeviceMessageSender.this.send(message); 371 | } 372 | }; 373 | } 374 | 375 | @Override 376 | public WritePropertyMessageSender writeProperty() { 377 | 378 | WritePropertyMessage message = new WritePropertyMessage(); 379 | message.setDeviceId(deviceId); 380 | message.setMessageId(IdUtils.newUUID()); 381 | return new WritePropertyMessageSender() { 382 | @Override 383 | public WritePropertyMessageSender write(String property, Object value) { 384 | message.addProperty(property, value); 385 | return this; 386 | } 387 | 388 | @Override 389 | public WritePropertyMessageSender custom(Consumer messageConsumer) { 390 | messageConsumer.accept(message); 391 | return this; 392 | } 393 | 394 | @Override 395 | public WritePropertyMessageSender header(String header, Object value) { 396 | message.addHeader(header, value); 397 | return this; 398 | } 399 | 400 | @Override 401 | public CompletionStage retrieveReply() { 402 | return RedissonDeviceMessageSender.this.retrieveReply( 403 | Objects.requireNonNull(message.getMessageId(), "messageId can not be null"), 404 | WritePropertyMessageReply::new); 405 | } 406 | 407 | @Override 408 | public CompletionStage send() { 409 | return RedissonDeviceMessageSender.this.send(message); 410 | } 411 | }; 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/registry/redis/RedissonDeviceOperation.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.registry.redis; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import lombok.SneakyThrows; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.jetlinks.core.ProtocolSupport; 9 | import org.jetlinks.core.ProtocolSupports; 10 | import org.jetlinks.core.device.*; 11 | import org.jetlinks.core.device.registry.DeviceMessageHandler; 12 | import org.jetlinks.core.device.registry.DeviceRegistry; 13 | import org.jetlinks.core.message.DisconnectDeviceMessage; 14 | import org.jetlinks.core.message.DisconnectDeviceMessageReply; 15 | import org.jetlinks.core.message.interceptor.DeviceMessageSenderInterceptor; 16 | import org.jetlinks.core.metadata.DefaultValueWrapper; 17 | import org.jetlinks.core.metadata.DeviceMetadata; 18 | import org.jetlinks.core.metadata.NullValueWrapper; 19 | import org.jetlinks.core.metadata.ValueWrapper; 20 | import org.jetlinks.core.utils.IdUtils; 21 | import org.redisson.api.RFuture; 22 | import org.redisson.api.RMap; 23 | import org.redisson.api.RSemaphore; 24 | import org.redisson.api.RedissonClient; 25 | 26 | import java.util.*; 27 | import java.util.concurrent.CompletableFuture; 28 | import java.util.concurrent.CompletionStage; 29 | import java.util.concurrent.ConcurrentHashMap; 30 | import java.util.concurrent.TimeUnit; 31 | import java.util.function.Consumer; 32 | import java.util.stream.Collectors; 33 | import java.util.stream.Stream; 34 | 35 | /** 36 | * @author zhouhao 37 | * @since 1.0.0 38 | */ 39 | @Slf4j 40 | public class RedissonDeviceOperation implements DeviceOperation { 41 | 42 | private RedissonClient redissonClient; 43 | 44 | private RMap rMap; 45 | 46 | private Map localCache = new ConcurrentHashMap<>(32); 47 | private Map confCache = new ConcurrentHashMap<>(32); 48 | 49 | private ProtocolSupports protocolSupports; 50 | 51 | private DeviceRegistry registry; 52 | 53 | private String deviceId; 54 | 55 | private Consumer changedListener; 56 | 57 | @Setter 58 | @Getter 59 | private DeviceMessageSenderInterceptor interceptor; 60 | 61 | // private DeviceMessageHandler deviceMessageHandler; 62 | 63 | private RedissonDeviceMessageSender messageSender; 64 | 65 | public RedissonDeviceOperation(String deviceId, 66 | RedissonClient redissonClient, 67 | RMap rMap, 68 | ProtocolSupports protocolSupports, 69 | DeviceMessageHandler deviceMessageHandler, 70 | DeviceRegistry registry, 71 | DeviceMessageSenderInterceptor interceptor, 72 | Consumer changedListener) { 73 | this.deviceId = deviceId; 74 | this.redissonClient = redissonClient; 75 | this.rMap = rMap; 76 | this.protocolSupports = protocolSupports; 77 | this.registry = registry; 78 | // this.deviceMessageHandler = deviceMessageHandler; 79 | this.interceptor=interceptor; 80 | this.changedListener = (isConf) -> { 81 | clearCache(isConf); 82 | changedListener.accept(isConf); 83 | }; 84 | messageSender = new RedissonDeviceMessageSender(deviceId, redissonClient, deviceMessageHandler, this); 85 | messageSender.setInterceptor(interceptor); 86 | } 87 | 88 | 89 | 90 | void clearCache(boolean isConf) { 91 | if (isConf) { 92 | confCache.clear(); 93 | } else { 94 | localCache.clear(); 95 | } 96 | 97 | } 98 | 99 | @Override 100 | public String getDeviceId() { 101 | return deviceId; 102 | } 103 | 104 | @SuppressWarnings("all") 105 | private T tryGetFromLocalCache(String key) { 106 | Object val = localCache.computeIfAbsent(key, k -> Optional.ofNullable(rMap.get(k)).orElse(NullValue.instance)); 107 | if (val == NullValue.instance) { 108 | return null; 109 | } 110 | return (T) val; 111 | } 112 | 113 | private T tryGetFromConfCache(String key) { 114 | Object val = confCache.computeIfAbsent(key, k -> Optional.ofNullable(rMap.get(k)).orElse(NullValue.instance)); 115 | if (val == NullValue.instance) { 116 | return null; 117 | } 118 | return (T) val; 119 | } 120 | 121 | @Override 122 | public String getServerId() { 123 | String serverId = tryGetFromLocalCache("serverId"); 124 | if (serverId == null || serverId.isEmpty()) { 125 | return null; 126 | } 127 | return serverId; 128 | } 129 | 130 | @Override 131 | public String getSessionId() { 132 | String sessionId = tryGetFromLocalCache("sessionId"); 133 | if (sessionId == null || sessionId.isEmpty()) { 134 | return null; 135 | } 136 | return sessionId; 137 | } 138 | 139 | @Override 140 | public byte getState() { 141 | Byte state = tryGetFromLocalCache("state"); 142 | return state == null ? DeviceState.unknown : state; 143 | } 144 | 145 | @Override 146 | @SneakyThrows 147 | public void putState(byte state) { 148 | localCache.put("state", state); 149 | 150 | execute(rMap.fastPutAsync("state", state)); 151 | 152 | changedListener.accept(false); 153 | } 154 | 155 | private void execute(RFuture future) { 156 | try { 157 | //无论成功失败,最多等待一秒 158 | future.await(1, TimeUnit.SECONDS); 159 | } catch (Exception e) { 160 | log.error(e.getMessage(), e); 161 | } 162 | } 163 | 164 | @Override 165 | public CompletionStage checkState() { 166 | String serverId = getServerId(); 167 | if (serverId != null) { 168 | long subscribes = redissonClient 169 | .getTopic("device:state:check:".concat(serverId)) 170 | .publish(deviceId); 171 | if (subscribes <= 0) { 172 | //没有任何服务在监听话题,则认为服务已经不可用 173 | if (getState() == DeviceState.online) { 174 | log.debug("设备网关服务[{}]未正常运行,设备[{}]下线", serverId, deviceId); 175 | offline(); 176 | } 177 | } else { 178 | //等待检查返回,检查是异步的,需要等待检查完成的信号 179 | //一般检查速度很快,所以这里超过5秒则超时,继续执行接下来的逻辑 180 | try { 181 | RSemaphore semaphore = redissonClient 182 | .getSemaphore("device:state:check:semaphore:".concat(deviceId)); 183 | semaphore.expireAsync(5, TimeUnit.SECONDS); 184 | boolean success = semaphore.tryAcquire((int) subscribes, 2, TimeUnit.SECONDS); 185 | semaphore.deleteAsync(); 186 | if (!success) { 187 | log.debug("设备[{}]状态检查超时,设备网关服务:[{}]", deviceId, serverId); 188 | } 189 | } catch (InterruptedException ignore) { 190 | } 191 | } 192 | } else { 193 | if (getState() == DeviceState.online) { 194 | log.debug("设备[{}]未注册到任何设备网关服务", deviceId); 195 | offline(); 196 | } 197 | } 198 | return CompletableFuture.completedFuture(getState()); 199 | } 200 | 201 | @Override 202 | public long getOnlineTime() { 203 | return Optional.ofNullable(rMap.get("onlineTime")) 204 | .map(Long.class::cast) 205 | .orElse(-1L); 206 | } 207 | 208 | @Override 209 | public long getOfflineTime() { 210 | return Optional.ofNullable(rMap.get("offlineTime")) 211 | .map(Long.class::cast) 212 | .orElse(-1L); 213 | } 214 | 215 | @Override 216 | public void online(String serverId, String sessionId) { 217 | Map map = new HashMap<>(); 218 | map.put("serverId", serverId); 219 | map.put("sessionId", sessionId); 220 | map.put("state", DeviceState.online); 221 | map.put("onlineTime", System.currentTimeMillis()); 222 | localCache.putAll(map); 223 | 224 | execute(rMap.putAllAsync(map)); 225 | changedListener.accept(false); 226 | } 227 | 228 | @Override 229 | public void offline() { 230 | Map map = new HashMap<>(); 231 | map.put("state", DeviceState.offline); 232 | map.put("offlineTime", System.currentTimeMillis()); 233 | map.put("serverId", ""); 234 | map.put("sessionId", ""); 235 | localCache.putAll(map); 236 | 237 | execute(rMap.putAllAsync(map)); 238 | changedListener.accept(false); 239 | } 240 | 241 | @Override 242 | public CompletionStage disconnect() { 243 | DisconnectDeviceMessage message=new DisconnectDeviceMessage(); 244 | message.setDeviceId(deviceId); 245 | message.setMessageId(IdUtils.newUUID()); 246 | 247 | return messageSender 248 | .send(message) 249 | .thenApply(DisconnectDeviceMessageReply::isSuccess); 250 | } 251 | 252 | @Override 253 | public CompletionStage authenticate(AuthenticationRequest request) { 254 | try { 255 | return getProtocol() 256 | .authenticate(request, this); 257 | } catch (Throwable e) { 258 | CompletableFuture future = new CompletableFuture<>(); 259 | future.completeExceptionally(e); 260 | return future; 261 | } 262 | } 263 | 264 | @Override 265 | public DeviceMetadata getMetadata() { 266 | 267 | Map deviceInfo = rMap.getAll(new HashSet<>(Arrays.asList("metadata", "protocol", "productId"))); 268 | 269 | if (deviceInfo == null || deviceInfo.isEmpty()) { 270 | throw new NullPointerException("设备信息不存在"); 271 | } 272 | String metaJson = (String) deviceInfo.get("metadata"); 273 | //设备没有单独的元数据,则获取设备产品型号的元数据 274 | if (metaJson == null || metaJson.isEmpty()) { 275 | return registry.getProduct((String) deviceInfo.get("productId")).getMetadata(); 276 | } 277 | return protocolSupports 278 | .getProtocol((String) deviceInfo.get("protocol")) 279 | .getMetadataCodec() 280 | .decode(metaJson); 281 | } 282 | 283 | private String getProductId() { 284 | return tryGetFromLocalCache("productId"); 285 | } 286 | 287 | @Override 288 | public ProtocolSupport getProtocol() { 289 | 290 | String protocol = tryGetFromLocalCache("protocol"); 291 | 292 | if (protocol != null) { 293 | return protocolSupports.getProtocol(protocol); 294 | } else { 295 | return Optional.ofNullable(registry.getProduct(getProductId())) 296 | .map(DeviceProductOperation::getProtocol) 297 | .orElseThrow(() -> new UnsupportedOperationException("设备[" + deviceId + "]未配置协议以及产品信息")); 298 | } 299 | } 300 | 301 | @Override 302 | public DeviceMessageSender messageSender() { 303 | 304 | return messageSender; 305 | } 306 | 307 | @Override 308 | public DeviceInfo getDeviceInfo() { 309 | Object info = rMap.get("info"); 310 | if (info instanceof String) { 311 | return JSON.parseObject((String) info, DeviceInfo.class); 312 | } 313 | if (info instanceof DeviceInfo) { 314 | return ((DeviceInfo) info); 315 | } 316 | log.warn("设备信息反序列化错误:{}", info); 317 | return null; 318 | } 319 | 320 | @Override 321 | public void update(DeviceInfo deviceInfo) { 322 | Map all = new HashMap<>(); 323 | all.put("info", JSON.toJSONString(deviceInfo)); 324 | if (deviceInfo.getProtocol() != null) { 325 | all.put("protocol", deviceInfo.getProtocol()); 326 | } 327 | if (deviceInfo.getProductId() != null) { 328 | all.put("productId", deviceInfo.getProductId()); 329 | } 330 | execute(rMap.putAllAsync(all)); 331 | changedListener.accept(false); 332 | } 333 | 334 | @Override 335 | public void updateMetadata(String metadata) { 336 | changedListener.accept(false); 337 | rMap.fastPut("metadata", metadata); 338 | } 339 | 340 | private String createConfigKey(String key) { 341 | return "_cfg:".concat(key); 342 | } 343 | 344 | private String recoverConfigKey(String key) { 345 | return key.substring(5); 346 | } 347 | 348 | private Map recoverConfigMap(Map source) { 349 | return source.entrySet() 350 | .stream() 351 | .collect(Collectors.toMap(e -> recoverConfigKey(e.getKey()), Map.Entry::getValue)); 352 | } 353 | 354 | @Override 355 | public CompletionStage> getAllAsync(String... key) { 356 | return CompletableFuture.supplyAsync(()->getAll(key)); 357 | } 358 | 359 | @Override 360 | @SuppressWarnings("all") 361 | public Map getAll(String... key) { 362 | 363 | //获取全部 364 | if (key.length == 0) { 365 | Map productConf = registry.getProduct(getProductId()).getAll(); 366 | Map meConf = (Map) localCache.computeIfAbsent("__all", __ -> 367 | rMap.entrySet().stream() 368 | .filter(e -> e.getKey().startsWith("_cfg:")) 369 | .collect(Collectors.toMap(e -> recoverConfigKey(e.getKey()), Map.Entry::getValue))); 370 | Map all = new HashMap<>(); 371 | 372 | all.putAll(productConf); 373 | all.putAll(meConf); 374 | return all; 375 | } 376 | 377 | Set keSet = Stream.of(key) 378 | .map(this::createConfigKey) 379 | .collect(Collectors.toSet()); 380 | 381 | String cacheKey = String.valueOf(keSet.hashCode()); 382 | 383 | Object cache = confCache.get(cacheKey); 384 | 385 | if (cache instanceof Map) { 386 | return (Map) cache; 387 | } 388 | //null value 直接获取产品配置 389 | if (cache instanceof NullValue) { 390 | return registry.getProduct(getProductId()).getAll(key); 391 | } 392 | return Optional.of(rMap 393 | .getAll(keSet)) 394 | .map(mine -> { 395 | if (mine.isEmpty()) { 396 | confCache.put(cacheKey, NullValue.instance); 397 | return registry 398 | .getProduct(getProductId()) 399 | .getAll(key); 400 | } 401 | //只有一部分,尝试从产品中获取 402 | if (mine.size() != key.length) { 403 | String[] inProductKey = keSet 404 | .stream() 405 | .filter(k -> !mine.containsKey(k)) 406 | .map(this::recoverConfigKey) 407 | .toArray(String[]::new); 408 | 409 | return Optional.of(registry 410 | .getProduct(getProductId()) 411 | .getAll(inProductKey)) 412 | .map(productPart -> { 413 | Map minePart = recoverConfigMap(mine); 414 | minePart.putAll(productPart); 415 | return minePart; 416 | }).get(); 417 | } 418 | Map recover = Collections.unmodifiableMap(recoverConfigMap(mine)); 419 | confCache.put(cacheKey, recover); 420 | return recover; 421 | }).get(); 422 | } 423 | 424 | @Override 425 | public ValueWrapper get(String key) { 426 | String confKey = createConfigKey(key); 427 | Object val = tryGetFromConfCache(confKey); 428 | 429 | if (val == null) { 430 | String productId = getProductId(); 431 | if (null != productId) { 432 | //获取产品的配置 433 | return registry 434 | .getProduct(productId) 435 | .get(key); 436 | } 437 | return NullValueWrapper.instance; 438 | } 439 | 440 | return new DefaultValueWrapper(val); 441 | } 442 | 443 | @Override 444 | public void put(String key, Object value) { 445 | Objects.requireNonNull(value, "value"); 446 | rMap.fastPut(key = createConfigKey(key), value); 447 | confCache.put(key, value); 448 | changedListener.accept(true); 449 | } 450 | 451 | @Override 452 | @SuppressWarnings("all") 453 | public void putAll(Map conf) { 454 | if (conf == null || conf.isEmpty()) { 455 | return; 456 | } 457 | Map newMap = new HashMap<>(); 458 | for (Map.Entry entry : conf.entrySet()) { 459 | newMap.put(createConfigKey(entry.getKey()), entry.getValue()); 460 | } 461 | rMap.putAll(newMap); 462 | confCache.putAll(newMap); 463 | changedListener.accept(true); 464 | } 465 | 466 | @Override 467 | public Object remove(String key) { 468 | Object val = rMap.remove(key = createConfigKey(key)); 469 | confCache.remove(key); 470 | changedListener.accept(true); 471 | return val; 472 | } 473 | 474 | void delete() { 475 | changedListener.accept(true); 476 | rMap.delete(); 477 | confCache.clear(); 478 | localCache.clear(); 479 | 480 | } 481 | 482 | } 483 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/registry/redis/RedissonDeviceProductOperation.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.registry.redis; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.jetlinks.core.ProtocolSupport; 6 | import org.jetlinks.core.ProtocolSupports; 7 | import org.jetlinks.core.device.DeviceProductInfo; 8 | import org.jetlinks.core.device.DeviceProductOperation; 9 | import org.jetlinks.core.metadata.DefaultValueWrapper; 10 | import org.jetlinks.core.metadata.DeviceMetadata; 11 | import org.jetlinks.core.metadata.NullValueWrapper; 12 | import org.jetlinks.core.metadata.ValueWrapper; 13 | import org.redisson.api.RMap; 14 | 15 | import java.util.*; 16 | import java.util.concurrent.CompletableFuture; 17 | import java.util.concurrent.CompletionStage; 18 | import java.util.concurrent.ConcurrentHashMap; 19 | import java.util.stream.Collectors; 20 | import java.util.stream.Stream; 21 | 22 | /** 23 | * @author zhouhao 24 | * @since 1.0.0 25 | */ 26 | @Slf4j 27 | @SuppressWarnings("all") 28 | public class RedissonDeviceProductOperation implements DeviceProductOperation { 29 | 30 | private RMap rMap; 31 | 32 | private Map localCache = new ConcurrentHashMap<>(32); 33 | 34 | 35 | private ProtocolSupports protocolSupports; 36 | 37 | private Runnable cacheChangedListener; 38 | 39 | 40 | public RedissonDeviceProductOperation(RMap rMap, ProtocolSupports protocolSupports, Runnable cacheChangedListener) { 41 | this.rMap = rMap; 42 | this.protocolSupports = protocolSupports; 43 | this.cacheChangedListener = () -> { 44 | localCache.clear(); 45 | cacheChangedListener.run(); 46 | }; 47 | } 48 | 49 | void clearCache() { 50 | localCache.clear(); 51 | } 52 | 53 | @Override 54 | public DeviceMetadata getMetadata() { 55 | return getProtocol() 56 | .getMetadataCodec() 57 | .decode(tryGetFromLocalCache("metadata")); 58 | } 59 | 60 | @SuppressWarnings("all") 61 | private T tryGetFromLocalCache(String key) { 62 | Object val = localCache.computeIfAbsent(key, k -> Optional.ofNullable(rMap.get(k)).orElse(NullValue.instance)); 63 | if (val == NullValue.instance) { 64 | return null; 65 | } 66 | return (T) val; 67 | } 68 | 69 | @Override 70 | public void updateMetadata(String metadata) { 71 | rMap.fastPut("metadata", metadata); 72 | localCache.put("metadata", metadata); 73 | } 74 | 75 | @Override 76 | public DeviceProductInfo getInfo() { 77 | Object info = tryGetFromLocalCache("info"); 78 | if (info instanceof DeviceProductInfo) { 79 | return ((DeviceProductInfo) info); 80 | } 81 | 82 | if (info instanceof String) { 83 | return JSON.parseObject(((String) info), DeviceProductInfo.class); 84 | } 85 | log.warn("设备产品信息反序列化错误:{}", info); 86 | return null; 87 | } 88 | 89 | @Override 90 | public void update(DeviceProductInfo info) { 91 | Map all = new HashMap<>(); 92 | all.put("info", JSON.toJSONString(info)); 93 | if (info.getProtocol() != null) { 94 | all.put("protocol", info.getProtocol()); 95 | } 96 | rMap.putAll(all); 97 | localCache.putAll(all); 98 | cacheChangedListener.run(); 99 | } 100 | 101 | @Override 102 | public ProtocolSupport getProtocol() { 103 | return protocolSupports.getProtocol(tryGetFromLocalCache("protocol")); 104 | } 105 | 106 | private String createConfigKey(String key) { 107 | return "_cfg:".concat(key); 108 | } 109 | 110 | private String recoverConfigKey(String key) { 111 | return key.substring(5); 112 | } 113 | 114 | @Override 115 | public ValueWrapper get(String key) { 116 | Object conf = tryGetFromLocalCache(createConfigKey(key)); 117 | if (null == conf) { 118 | return NullValueWrapper.instance; 119 | } 120 | return new DefaultValueWrapper(conf); 121 | } 122 | 123 | @Override 124 | public CompletionStage> getAllAsync(String... key) { 125 | return CompletableFuture.supplyAsync(()->getAll(key)); 126 | } 127 | 128 | @Override 129 | @SuppressWarnings("all") 130 | public Map getAll(String... key) { 131 | 132 | //获取全部 133 | if (key.length == 0) { 134 | return (Map) localCache.computeIfAbsent("__all", __ -> { 135 | return rMap.entrySet().stream() 136 | .filter(e -> e.getKey().startsWith("_cfg:")) 137 | .collect(Collectors.toMap(e -> recoverConfigKey(e.getKey()), Map.Entry::getValue)); 138 | }); 139 | } 140 | 141 | Set keSet = Stream.of(key).map(this::createConfigKey).collect(Collectors.toSet()); 142 | 143 | String cacheKey = String.valueOf(keSet.hashCode()); 144 | 145 | Object cache = localCache.get(cacheKey); 146 | 147 | if (cache instanceof Map) { 148 | return (Map) cache; 149 | } 150 | if (cache instanceof NullValue) { 151 | return Collections.emptyMap(); 152 | } 153 | Map inRedis = Collections.unmodifiableMap(rMap.getAll(keSet) 154 | .entrySet() 155 | .stream() 156 | .collect(Collectors.toMap(e -> recoverConfigKey(e.getKey()), Map.Entry::getValue, (_1, _2) -> _1))); 157 | 158 | localCache.put(cacheKey, inRedis); 159 | 160 | return inRedis; 161 | } 162 | 163 | @Override 164 | public void putAll(Map conf) { 165 | if (conf == null || conf.isEmpty()) { 166 | return; 167 | } 168 | Map newMap = new HashMap<>(); 169 | for (Map.Entry entry : conf.entrySet()) { 170 | newMap.put(createConfigKey(entry.getKey()), entry.getValue()); 171 | } 172 | rMap.putAll(newMap); 173 | cacheChangedListener.run(); 174 | } 175 | 176 | @Override 177 | public void put(String key, Object value) { 178 | rMap.fastPut(createConfigKey(key), value); 179 | cacheChangedListener.run(); 180 | } 181 | 182 | @Override 183 | public Object remove(String key) { 184 | Object val = rMap.fastRemove(createConfigKey(key)); 185 | cacheChangedListener.run(); 186 | return val; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/registry/redis/RedissonDeviceRegistry.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.registry.redis; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.jetlinks.core.ProtocolSupports; 5 | import org.jetlinks.core.device.DeviceInfo; 6 | import org.jetlinks.core.device.DeviceOperation; 7 | import org.jetlinks.core.device.DeviceProductOperation; 8 | import org.jetlinks.core.device.DeviceState; 9 | import org.jetlinks.core.device.registry.DeviceMessageHandler; 10 | import org.jetlinks.core.device.registry.DeviceRegistry; 11 | import org.jetlinks.core.message.interceptor.DeviceMessageSenderInterceptor; 12 | import org.redisson.api.RTopic; 13 | import org.redisson.api.RedissonClient; 14 | import org.redisson.client.codec.StringCodec; 15 | 16 | import java.lang.ref.SoftReference; 17 | import java.util.Map; 18 | import java.util.Optional; 19 | import java.util.concurrent.ConcurrentHashMap; 20 | 21 | /** 22 | * @author zhouhao 23 | * @since 1.0.0 24 | */ 25 | @Slf4j 26 | public class RedissonDeviceRegistry implements DeviceRegistry { 27 | 28 | private RedissonClient client; 29 | 30 | private ProtocolSupports protocolSupports; 31 | 32 | private Map> localCache = new ConcurrentHashMap<>(1024); 33 | 34 | private Map> productLocalCache = new ConcurrentHashMap<>(128); 35 | 36 | private RTopic cacheChangedTopic; 37 | 38 | private final CompositeDeviceMessageSenderInterceptor interceptor = new CompositeDeviceMessageSenderInterceptor(); 39 | 40 | private DeviceMessageHandler messageHandler; 41 | 42 | public RedissonDeviceRegistry(RedissonClient client, 43 | DeviceMessageHandler handler, 44 | ProtocolSupports protocolSupports) { 45 | this.client = client; 46 | this.protocolSupports = protocolSupports; 47 | this.cacheChangedTopic = client.getTopic("device:registry:cache:changed", StringCodec.INSTANCE); 48 | this.messageHandler = handler; 49 | 50 | cacheChangedTopic.addListener(String.class, (t, id) -> { 51 | 52 | String[] split = id.split("[@]"); 53 | byte clearType = 1; 54 | if (split.length == 2) { 55 | id = split[0]; 56 | clearType = Byte.valueOf(split[1]); 57 | } else if (split.length > 2) { 58 | log.warn("本地缓存可能出错,id[{}]不合法", id); 59 | } 60 | boolean clearConf = clearType == 1; 61 | 62 | Optional.ofNullable(localCache.get(id)) 63 | .map(SoftReference::get) 64 | .ifPresent(cache -> cache.clearCache(clearConf)); 65 | 66 | Optional.ofNullable(productLocalCache.get(id)) 67 | .map(SoftReference::get) 68 | .ifPresent(RedissonDeviceProductOperation::clearCache); 69 | 70 | }); 71 | } 72 | 73 | 74 | public void addInterceptor(DeviceMessageSenderInterceptor interceptor) { 75 | this.interceptor.addInterceptor(interceptor); 76 | } 77 | 78 | @Override 79 | public DeviceProductOperation getProduct(String productId) { 80 | if (productId == null || productId.isEmpty()) { 81 | return null; 82 | } 83 | SoftReference reference = productLocalCache.get(productId); 84 | 85 | if (reference == null || reference.get() == null) { 86 | 87 | productLocalCache.put(productId, reference = new SoftReference<>(doGetProduct(productId))); 88 | 89 | } 90 | return reference.get(); 91 | } 92 | 93 | @Override 94 | public RedissonDeviceOperation getDevice(String deviceId) { 95 | SoftReference reference = localCache.get(deviceId); 96 | 97 | if (reference == null || reference.get() == null) { 98 | RedissonDeviceOperation operation = doGetOperation(deviceId); 99 | //unknown的设备不使用缓存 100 | if (operation.getState() == DeviceState.unknown) { 101 | return operation; 102 | } 103 | localCache.put(deviceId, reference = new SoftReference<>(operation)); 104 | 105 | } 106 | return reference.get(); 107 | } 108 | 109 | private RedissonDeviceProductOperation doGetProduct(String productId) { 110 | return new RedissonDeviceProductOperation(client.getMap("product:".concat(productId).concat(":reg")) 111 | , protocolSupports, 112 | () -> cacheChangedTopic.publishAsync(productId.concat("@-1"))); 113 | } 114 | 115 | private RedissonDeviceOperation doGetOperation(String deviceId) { 116 | RedissonDeviceOperation operation = new RedissonDeviceOperation(deviceId, client, 117 | client.getMap(deviceId.concat(":reg")), 118 | protocolSupports, 119 | messageHandler, 120 | this, 121 | interceptor, 122 | (isConf) -> cacheChangedTopic.publishAsync(deviceId.concat("@").concat(isConf ? "1" : "0"))); 123 | // operation.setInterceptor(interceptor); 124 | 125 | return operation; 126 | } 127 | 128 | @Override 129 | public DeviceOperation registry(DeviceInfo deviceInfo) { 130 | DeviceOperation operation = getDevice(deviceInfo.getId()); 131 | operation.update(deviceInfo); 132 | operation.putState(DeviceState.offline); 133 | return operation; 134 | } 135 | 136 | @Override 137 | public void unRegistry(String deviceId) { 138 | getDevice(deviceId).delete(); 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/registry/redis/lettuce/LettuceDeviceMessageHandler.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.registry.redis.lettuce; 2 | 3 | import io.lettuce.core.ScriptOutputType; 4 | import io.lettuce.core.api.StatefulRedisConnection; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.apache.commons.lang.StringUtils; 9 | import org.jetlinks.core.device.registry.DeviceMessageHandler; 10 | import org.jetlinks.core.message.DeviceMessage; 11 | import org.jetlinks.core.message.DeviceMessageReply; 12 | import org.jetlinks.lettuce.LettucePlus; 13 | import org.jetlinks.lettuce.codec.StringCodec; 14 | 15 | import java.util.Map; 16 | import java.util.Optional; 17 | import java.util.concurrent.CompletableFuture; 18 | import java.util.concurrent.CompletionStage; 19 | import java.util.concurrent.ConcurrentHashMap; 20 | import java.util.concurrent.TimeUnit; 21 | import java.util.function.Consumer; 22 | 23 | /** 24 | * @author zhouhao 25 | * @since 1.0.0 26 | */ 27 | @Slf4j 28 | public class LettuceDeviceMessageHandler implements DeviceMessageHandler { 29 | private LettucePlus plus; 30 | 31 | private Map futureMap = new ConcurrentHashMap<>(); 32 | 33 | private static int replyExpireTimeSeconds = Integer.getInteger("device.message.reply.expire-time-seconds", (int) TimeUnit.MINUTES.toSeconds(3)); 34 | 35 | private static int asyncFlagExpireTimeSeconds = Integer.getInteger("device.message.async-flag.expire-time-seconds", (int) TimeUnit.MINUTES.toSeconds(30)); 36 | 37 | private Map> localConsumer = new ConcurrentHashMap<>(); 38 | 39 | public LettuceDeviceMessageHandler(LettucePlus plus) { 40 | this.plus = plus; 41 | 42 | //监听消息返回 43 | this.plus.getTopic(StringCodec.getInstance(), "device:message:reply") 44 | .addListener((channel, msg) -> Optional.ofNullable(futureMap.remove(msg)) 45 | .map(MessageFuture::getFuture) 46 | .ifPresent(future -> tryComplete(msg, future))); 47 | 48 | //定时检查超时消息 49 | plus.getExecutor().scheduleAtFixedRate(() -> futureMap 50 | .entrySet() 51 | .stream() 52 | .filter(e -> System.currentTimeMillis() > e.getValue().expireTime) 53 | .forEach((e) -> { 54 | try { 55 | tryComplete(e.getKey(), e.getValue().future); 56 | } finally { 57 | log.info("设备消息[{}]超时未返回", e.getKey()); 58 | futureMap.remove(e.getKey()); 59 | } 60 | }), 1, 5, TimeUnit.SECONDS); 61 | } 62 | 63 | private void tryComplete(String messageId, CompletableFuture future) { 64 | if (!future.isCancelled()) { 65 | plus.getConnection() 66 | .thenApply(StatefulRedisConnection::async) 67 | .thenCompose(redis -> redis.get("device:message:reply:".concat(messageId))) 68 | .whenComplete((data, error) -> { 69 | if (error != null) { 70 | future.completeExceptionally(error); 71 | } else { 72 | future.complete(data); 73 | } 74 | }); 75 | } 76 | } 77 | 78 | @Override 79 | public void handleDeviceCheck(String serviceId, Consumer consumer) { 80 | plus.getTopic("device:state:check:".concat(serviceId)) 81 | .addListener((channel, msg) -> { 82 | if (StringUtils.isEmpty(msg)) { 83 | return; 84 | } 85 | consumer.accept(msg); 86 | }); 87 | } 88 | 89 | @Override 90 | public void handleMessage(String serverId, Consumer deviceMessageConsumer) { 91 | localConsumer.put(serverId, deviceMessageConsumer); 92 | 93 | plus.getTopic("device:message:accept:".concat(serverId)) 94 | .addListener((channel, message) -> { 95 | if (log.isDebugEnabled()) { 96 | log.debug("接收到发往设备的消息:{}", message.toJson()); 97 | } 98 | deviceMessageConsumer.accept(message); 99 | }); 100 | } 101 | 102 | @AllArgsConstructor 103 | @Getter 104 | private class MessageFuture { 105 | private String messageId; 106 | 107 | private CompletableFuture future; 108 | 109 | private long expireTime; 110 | } 111 | 112 | @Override 113 | public CompletionStage handleReply(String messageId, long timeout, TimeUnit timeUnit) { 114 | CompletableFuture future = new CompletableFuture<>(); 115 | futureMap.put(messageId, new MessageFuture(messageId, future, System.currentTimeMillis() + timeUnit.toMillis(timeout))); 116 | 117 | return future; 118 | } 119 | 120 | public CompletionStage send(String serverId, DeviceMessage message) { 121 | Consumer consumer = localConsumer.get(serverId); 122 | if (consumer != null) { 123 | consumer.accept(message); 124 | return CompletableFuture.completedFuture(1L); 125 | } 126 | return plus.getTopic("device:message:accept:".concat(serverId)).publish(message); 127 | } 128 | 129 | @Override 130 | public CompletionStage reply(DeviceMessageReply message) { 131 | String messageId = message.getMessageId(); 132 | MessageFuture future = futureMap.get(messageId); 133 | 134 | if (null != future) { 135 | futureMap.remove(messageId); 136 | future.getFuture().complete(message); 137 | return CompletableFuture.completedFuture(true); 138 | } 139 | String script = "" + 140 | "redis.call('setex',KEYS[1]," + replyExpireTimeSeconds + ",ARGV[1]);" + 141 | "return redis.call('publish',KEYS[2],KEYS[3]);"; 142 | 143 | return plus.eval(script, ScriptOutputType.INTEGER, 144 | new Object[]{"device:message:reply:".concat(messageId), "device:message:reply", messageId}, 145 | message) 146 | .thenApply((num) -> { 147 | if (num <= 0) { 148 | log.warn("消息回复[{}]没有任何服务消费", message.getMessageId()); 149 | } 150 | return num > 0; 151 | }).whenComplete((success, error) -> { 152 | if (error != null) { 153 | log.error("回复消息失败", error); 154 | } 155 | }); 156 | 157 | 158 | } 159 | 160 | 161 | @Override 162 | public CompletionStage markMessageAsync(String messageId) { 163 | return plus.getConnection() 164 | .thenApply(StatefulRedisConnection::async) 165 | .thenCompose(redis -> redis.setex("async-msg:".concat(messageId), asyncFlagExpireTimeSeconds, true)) 166 | .thenApply(str -> null); 167 | } 168 | 169 | @Override 170 | public CompletionStage messageIsAsync(String messageId, boolean reset) { 171 | 172 | return plus.getConnection() 173 | .thenApply(StatefulRedisConnection::async) 174 | .thenCompose(redis -> redis.get("async-msg:".concat(messageId))); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/registry/redis/lettuce/LettuceDeviceMessageSender.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.registry.redis.lettuce; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import io.lettuce.core.api.StatefulRedisConnection; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.jetlinks.core.device.DeviceMessageSender; 9 | import org.jetlinks.core.device.DeviceOperation; 10 | import org.jetlinks.core.device.registry.DeviceMessageHandler; 11 | import org.jetlinks.core.enums.ErrorCode; 12 | import org.jetlinks.core.message.*; 13 | import org.jetlinks.core.message.exception.FunctionUndefinedException; 14 | import org.jetlinks.core.message.exception.ParameterUndefinedException; 15 | import org.jetlinks.core.message.function.FunctionInvokeMessage; 16 | import org.jetlinks.core.message.function.FunctionInvokeMessageReply; 17 | import org.jetlinks.core.message.function.FunctionParameter; 18 | import org.jetlinks.core.message.interceptor.DeviceMessageSenderInterceptor; 19 | import org.jetlinks.core.message.property.ReadPropertyMessage; 20 | import org.jetlinks.core.message.property.ReadPropertyMessageReply; 21 | import org.jetlinks.core.message.property.WritePropertyMessage; 22 | import org.jetlinks.core.message.property.WritePropertyMessageReply; 23 | import org.jetlinks.core.metadata.FunctionMetadata; 24 | import org.jetlinks.core.metadata.PropertyMetadata; 25 | import org.jetlinks.core.metadata.ValidateResult; 26 | import org.jetlinks.core.utils.IdUtils; 27 | import org.jetlinks.lettuce.LettucePlus; 28 | import reactor.core.publisher.Flux; 29 | import reactor.core.publisher.Mono; 30 | import reactor.util.function.Tuple2; 31 | import reactor.util.function.Tuples; 32 | 33 | import java.util.*; 34 | import java.util.concurrent.CompletableFuture; 35 | import java.util.concurrent.CompletionStage; 36 | import java.util.concurrent.TimeUnit; 37 | import java.util.function.BiConsumer; 38 | import java.util.function.Consumer; 39 | import java.util.function.Function; 40 | import java.util.function.Supplier; 41 | import java.util.stream.Collectors; 42 | 43 | import static org.jetlinks.core.enums.ErrorCode.NO_REPLY; 44 | 45 | /** 46 | * @author zhouhao 47 | * @since 1.0.0 48 | */ 49 | @Slf4j 50 | public class LettuceDeviceMessageSender implements DeviceMessageSender { 51 | 52 | private LettucePlus plus; 53 | 54 | protected Supplier connectionServerIdSupplier; 55 | 56 | protected Supplier> deviceStateChecker; 57 | 58 | protected String deviceId; 59 | 60 | protected DeviceOperation operation; 61 | 62 | //从元数据中获取异步 63 | @Getter 64 | @Setter 65 | private boolean asyncFromMetadata = Boolean.getBoolean("device.message.async.from-metadata"); 66 | 67 | @Getter 68 | @Setter 69 | protected DeviceMessageSenderInterceptor interceptor; 70 | 71 | private DeviceMessageHandler messageHandler; 72 | 73 | public LettuceDeviceMessageSender(String deviceId, 74 | LettucePlus plus, 75 | DeviceMessageHandler messageHandler, 76 | DeviceOperation operation) { 77 | this.plus = plus; 78 | this.operation = operation; 79 | this.messageHandler = messageHandler; 80 | this.connectionServerIdSupplier = operation::getServerId; 81 | this.deviceStateChecker = operation::checkState; 82 | this.deviceId = deviceId; 83 | } 84 | 85 | //最大等待30秒 86 | @Getter 87 | @Setter 88 | private int maxSendAwaitSeconds = Integer.getInteger("device.message.await.max-seconds", 30); 89 | 90 | @SuppressWarnings("all") 91 | protected R convertReply(Object deviceReply, RepayableDeviceMessage message, Supplier replyNewInstance) { 92 | R reply = replyNewInstance.get(); 93 | if (deviceReply == null) { 94 | reply.error(NO_REPLY); 95 | } else if (deviceReply instanceof ErrorCode) { 96 | reply.error((ErrorCode) deviceReply); 97 | } else { 98 | if (reply.getClass().isAssignableFrom(deviceReply.getClass())) { 99 | return reply = (R) deviceReply; 100 | } else if (deviceReply instanceof String) { 101 | reply.fromJson(JSON.parseObject(String.valueOf(deviceReply))); 102 | } else if (deviceReply instanceof DeviceMessage) { 103 | reply.fromJson(((DeviceMessage) deviceReply).toJson()); 104 | } else { 105 | reply.error(ErrorCode.UNSUPPORTED_MESSAGE); 106 | log.warn("不支持的消息类型:{}", deviceReply.getClass()); 107 | } 108 | } 109 | if (message != null) { 110 | reply.from(message); 111 | } 112 | return reply; 113 | } 114 | 115 | 116 | public CompletionStage retrieveReply(String messageId, Supplier replyNewInstance) { 117 | return plus 118 | .getConnection() 119 | .thenApply(StatefulRedisConnection::async) 120 | .thenCompose(redis -> redis.get("device:message:reply:".concat(messageId))) 121 | .thenApply(deviceReply -> { 122 | R reply = convertReply(deviceReply, null, replyNewInstance); 123 | reply.messageId(messageId); 124 | return reply; 125 | }); 126 | } 127 | 128 | @Override 129 | @SuppressWarnings("all") 130 | public Flux send(DeviceMessage requestMessage, Function replyMapping) { 131 | String serverId = connectionServerIdSupplier.get(); 132 | //设备当前没有连接到任何服务器 133 | if (serverId == null) { 134 | R reply = replyMapping.apply(null); 135 | if (reply != null) { 136 | reply.error(ErrorCode.CLIENT_OFFLINE); 137 | } 138 | if (null != reply) { 139 | reply.from(requestMessage); 140 | } 141 | return Flux.just(reply); 142 | } 143 | CompletableFuture future = new CompletableFuture<>(); 144 | if (interceptor != null) { 145 | requestMessage = interceptor.preSend(operation, requestMessage); 146 | } 147 | DeviceMessage message = requestMessage; 148 | //标记异步 149 | if (Headers.async.get(message).asBoolean().orElse(false)) { 150 | messageHandler.markMessageAsync(message.getMessageId()) 151 | .whenComplete((nil, error) -> { 152 | if (error != null) { 153 | log.error("设置异步消息标识[{}]失败", message, error); 154 | } 155 | }); 156 | } 157 | //在头中标记超时 158 | int timeout = message.getHeader("timeout") 159 | .map(Number.class::cast) 160 | .map(Number::intValue) 161 | .orElse(maxSendAwaitSeconds); 162 | 163 | //处理返回结果,_reply可能为null,ErroCode,Object 164 | BiConsumer doReply = (_reply, error) -> { 165 | 166 | CompletionStage reply = CompletableFuture 167 | .completedFuture(replyMapping.apply(error != null ? ErrorCode.SYSTEM_ERROR : _reply)) 168 | .thenApply(r -> { 169 | if (error != null) { 170 | r.addHeader("error", error.getMessage()); 171 | } 172 | return r; 173 | }); 174 | 175 | if (interceptor != null) { 176 | reply = reply.thenCompose(r -> interceptor.afterReply(operation, message, r)); 177 | } 178 | reply.whenComplete((r, throwable) -> { 179 | if (throwable != null) {//理论上不会出现 180 | future.completeExceptionally(throwable); 181 | } else { 182 | future.complete(r); 183 | } 184 | }); 185 | }; 186 | 187 | //监听返回 188 | CompletionStage stage = messageHandler.handleReply(message.getMessageId(), timeout, TimeUnit.SECONDS); 189 | 190 | stage.whenComplete(doReply); 191 | 192 | future.whenComplete((r, err) -> { 193 | if (future.isCancelled()) { 194 | log.info("取消等待设备[{}]消息[{}]返回", deviceId, message.getMessageId()); 195 | stage.toCompletableFuture().cancel(true); 196 | } 197 | }); 198 | //发送消息 199 | messageHandler.send(serverId, message) 200 | .whenComplete((deviceConnectedServerNumber, error) -> { 201 | if (error != null) { 202 | log.error("发送消息到设备网关服务失败:{}", message, error); 203 | doReply.accept(ErrorCode.SYSTEM_ERROR, error); 204 | return; 205 | } 206 | if (deviceConnectedServerNumber <= 0) { 207 | //没有任何服务消费此topic,可能所在服务器已经宕机,注册信息没有更新。 208 | //执行设备状态检查,尝试更新设备的真实状态 209 | if (deviceStateChecker != null) { 210 | deviceStateChecker.get() 211 | .whenComplete((state, err) -> { 212 | doReply.accept(ErrorCode.CLIENT_OFFLINE, null); 213 | }); 214 | } else { 215 | doReply.accept(ErrorCode.CLIENT_OFFLINE, null); 216 | } 217 | } 218 | //有多个相同名称的设备网关服务,可能是服务配置错误,启动了多台相同id的服务。 219 | if (deviceConnectedServerNumber > 1) { 220 | log.warn("存在多个相同的网关服务:{}", serverId); 221 | } 222 | }); 223 | 224 | return future; 225 | } 226 | 227 | @Override 228 | @SuppressWarnings("all") 229 | public Flux send(RepayableDeviceMessage message) { 230 | return send(message, deviceReply -> convertReply(deviceReply, message, message::newReply)); 231 | } 232 | 233 | @Override 234 | public FunctionInvokeMessageSender invokeFunction(String function) { 235 | Objects.requireNonNull(function, "function"); 236 | FunctionInvokeMessage message = new FunctionInvokeMessage(); 237 | message.setTimestamp(System.currentTimeMillis()); 238 | message.setDeviceId(deviceId); 239 | message.setFunctionId(function); 240 | message.setMessageId(IdUtils.newUUID()); 241 | return new FunctionInvokeMessageSender() { 242 | boolean markAsync = false; 243 | 244 | @Override 245 | public FunctionInvokeMessageSender addParameter(String name, Object value) { 246 | message.addInput(name, value); 247 | return this; 248 | } 249 | 250 | @Override 251 | public FunctionInvokeMessageSender custom(Consumer messageConsumer) { 252 | messageConsumer.accept(message); 253 | return this; 254 | } 255 | 256 | @Override 257 | public FunctionInvokeMessageSender messageId(String messageId) { 258 | message.setMessageId(messageId); 259 | return this; 260 | } 261 | 262 | @Override 263 | public FunctionInvokeMessageSender setParameter(List parameter) { 264 | message.setInputs(parameter); 265 | return this; 266 | } 267 | 268 | @Override 269 | public Flux> validate(BiConsumer resultConsumer) { 270 | return operation 271 | .getMetadata() 272 | .flatMap(metadata -> Mono.justOrEmpty(metadata.getFunction(function))) 273 | .switchIfEmpty(Mono.error(() -> new FunctionUndefinedException(function, "功能[" + function + "]未定义"))) 274 | .flatMapMany(functionMetadata -> { 275 | List metadataInputs = functionMetadata.getInputs(); 276 | List inputs = message.getInputs(); 277 | 278 | if (inputs.size() != metadataInputs.size()) { 279 | log.warn("调用设备功能[{}]参数数量[需要{},传入{}]错误,功能:{}", function, metadataInputs.size(), inputs.size(), functionMetadata.toString()); 280 | throw new IllegalArgumentException("参数数量错误"); 281 | } 282 | 283 | //参数定义转为map,避免n*n循环 284 | Map properties = metadataInputs.stream() 285 | .collect(Collectors.toMap(PropertyMetadata::getId, Function.identity(), (t1, t2) -> t1)); 286 | return Flux.create(sink -> { 287 | try { 288 | for (FunctionParameter input : message.getInputs()) { 289 | PropertyMetadata metadata = Optional.ofNullable(properties.get(input.getName())) 290 | .orElseThrow(() -> new ParameterUndefinedException(input.getName(), "参数[" + input.getName() + "]未定义")); 291 | 292 | sink.next(Tuples.of(input, metadata.getValueType().validate(input.getValue()))); 293 | } 294 | sink.complete(); 295 | } catch (Exception e) { 296 | sink.error(e); 297 | } 298 | 299 | }); 300 | }); 301 | 302 | } 303 | 304 | @Override 305 | public FunctionInvokeMessageSender header(String header, Object value) { 306 | message.addHeader(header, value); 307 | return this; 308 | } 309 | 310 | @Override 311 | public FunctionInvokeMessageSender async(Boolean async) { 312 | if (async != null) { 313 | custom(async ? Headers.async.setter() : Headers.async.clear()); 314 | } 315 | markAsync = true; 316 | return this; 317 | } 318 | 319 | @Override 320 | public CompletionStage retrieveReply() { 321 | return LettuceDeviceMessageSender.this.retrieveReply( 322 | Objects.requireNonNull(message.getMessageId(), "messageId can not be null"), 323 | FunctionInvokeMessageReply::new); 324 | } 325 | 326 | @Override 327 | public Flux send() { 328 | 329 | //如果未明确指定是否异步,则获取元数据中定义的异步配置 330 | if (!markAsync && asyncFromMetadata && message.getHeader(Headers.async.getHeader()).isPresent()) { 331 | message.addHeader(Headers.async.getHeader(), 332 | operation.getMetadata() 333 | .getFunction(message.getFunctionId()) 334 | .map(FunctionMetadata::isAsync) 335 | .orElse(false)); 336 | } 337 | return LettuceDeviceMessageSender.this.send(message); 338 | } 339 | 340 | }; 341 | } 342 | 343 | @Override 344 | public ReadPropertyMessageSender readProperty(String... property) { 345 | 346 | ReadPropertyMessage message = new ReadPropertyMessage(); 347 | message.setDeviceId(deviceId); 348 | message.addProperties(Arrays.asList(property)); 349 | message.setMessageId(IdUtils.newUUID()); 350 | return new ReadPropertyMessageSender() { 351 | @Override 352 | public ReadPropertyMessageSender messageId(String messageId) { 353 | message.setMessageId(messageId); 354 | return this; 355 | } 356 | 357 | @Override 358 | public ReadPropertyMessageSender custom(Consumer messageConsumer) { 359 | messageConsumer.accept(message); 360 | return this; 361 | } 362 | 363 | @Override 364 | public ReadPropertyMessageSender read(List properties) { 365 | message.addProperties(properties); 366 | return this; 367 | } 368 | 369 | @Override 370 | public ReadPropertyMessageSender header(String header, Object value) { 371 | message.addHeader(header, value); 372 | return this; 373 | } 374 | 375 | @Override 376 | public CompletionStage retrieveReply() { 377 | return LettuceDeviceMessageSender.this.retrieveReply( 378 | Objects.requireNonNull(message.getMessageId(), "messageId can not be null"), 379 | ReadPropertyMessageReply::new); 380 | } 381 | 382 | @Override 383 | public CompletionStage send() { 384 | return LettuceDeviceMessageSender.this.send(message); 385 | } 386 | }; 387 | } 388 | 389 | @Override 390 | public WritePropertyMessageSender writeProperty() { 391 | 392 | WritePropertyMessage message = new WritePropertyMessage(); 393 | message.setDeviceId(deviceId); 394 | message.setMessageId(IdUtils.newUUID()); 395 | return new WritePropertyMessageSender() { 396 | @Override 397 | public WritePropertyMessageSender write(String property, Object value) { 398 | message.addProperty(property, value); 399 | return this; 400 | } 401 | 402 | @Override 403 | public WritePropertyMessageSender custom(Consumer messageConsumer) { 404 | messageConsumer.accept(message); 405 | return this; 406 | } 407 | 408 | @Override 409 | public WritePropertyMessageSender header(String header, Object value) { 410 | message.addHeader(header, value); 411 | return this; 412 | } 413 | 414 | @Override 415 | public Mono retrieveReply() { 416 | return LettuceDeviceMessageSender.this.retrieveReply( 417 | Objects.requireNonNull(message.getMessageId(), "messageId can not be null"), 418 | WritePropertyMessageReply::new); 419 | } 420 | 421 | @Override 422 | public Mono send() { 423 | return LettuceDeviceMessageSender.this.send(message); 424 | } 425 | }; 426 | } 427 | } 428 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/registry/redis/lettuce/LettuceDeviceOperation.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.registry.redis.lettuce; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import io.lettuce.core.KeyValue; 5 | import io.lettuce.core.Value; 6 | import io.lettuce.core.api.StatefulRedisConnection; 7 | import io.lettuce.core.api.async.RedisAsyncCommands; 8 | import lombok.Getter; 9 | import lombok.Setter; 10 | import lombok.SneakyThrows; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.jetlinks.core.ProtocolSupport; 13 | import org.jetlinks.core.ProtocolSupports; 14 | import org.jetlinks.core.device.*; 15 | import org.jetlinks.core.device.registry.DeviceMessageHandler; 16 | import org.jetlinks.core.device.registry.DeviceRegistry; 17 | import org.jetlinks.core.message.DisconnectDeviceMessage; 18 | import org.jetlinks.core.message.DisconnectDeviceMessageReply; 19 | import org.jetlinks.core.message.interceptor.DeviceMessageSenderInterceptor; 20 | import org.jetlinks.core.metadata.DefaultValueWrapper; 21 | import org.jetlinks.core.metadata.DeviceMetadata; 22 | import org.jetlinks.core.metadata.NullValueWrapper; 23 | import org.jetlinks.core.metadata.ValueWrapper; 24 | import org.jetlinks.core.utils.IdUtils; 25 | import org.jetlinks.lettuce.LettucePlus; 26 | import org.jetlinks.registry.redis.NullValue; 27 | 28 | import java.util.*; 29 | import java.util.concurrent.CompletableFuture; 30 | import java.util.concurrent.CompletionStage; 31 | import java.util.concurrent.ConcurrentHashMap; 32 | import java.util.concurrent.TimeUnit; 33 | import java.util.function.Consumer; 34 | import java.util.function.Function; 35 | import java.util.stream.Collectors; 36 | import java.util.stream.Stream; 37 | 38 | /** 39 | * @author zhouhao 40 | * @since 1.0.0 41 | */ 42 | @Slf4j 43 | public class LettuceDeviceOperation implements DeviceOperation { 44 | 45 | private LettucePlus plus; 46 | 47 | private Map localCache = new ConcurrentHashMap<>(32); 48 | private Map confCache = new ConcurrentHashMap<>(32); 49 | 50 | private ProtocolSupports protocolSupports; 51 | 52 | private DeviceRegistry registry; 53 | 54 | private String deviceId; 55 | 56 | private Consumer changedListener; 57 | 58 | @Setter 59 | @Getter 60 | private DeviceMessageSenderInterceptor interceptor; 61 | 62 | private LettuceDeviceMessageSender messageSender; 63 | 64 | private String redisKey; 65 | 66 | public LettuceDeviceOperation(String deviceId, 67 | LettucePlus plus, 68 | ProtocolSupports protocolSupports, 69 | DeviceMessageHandler deviceMessageHandler, 70 | DeviceRegistry registry, 71 | DeviceMessageSenderInterceptor interceptor, 72 | Consumer changedListener) { 73 | this.redisKey = deviceId.concat(":reg"); 74 | this.deviceId = deviceId; 75 | this.plus = plus; 76 | this.protocolSupports = protocolSupports; 77 | this.registry = registry; 78 | this.interceptor = interceptor; 79 | this.changedListener = (isConf) -> { 80 | clearCache(isConf); 81 | changedListener.accept(isConf); 82 | }; 83 | messageSender = new LettuceDeviceMessageSender(deviceId, plus, deviceMessageHandler, this); 84 | messageSender.setInterceptor(interceptor); 85 | } 86 | 87 | protected CompletionStage> getAsyncRedis() { 88 | return plus 89 | .getConnection() 90 | .thenApply(StatefulRedisConnection::async); 91 | } 92 | 93 | void clearCache(boolean isConf) { 94 | if (isConf) { 95 | confCache.clear(); 96 | } else { 97 | localCache.clear(); 98 | } 99 | 100 | } 101 | 102 | @Override 103 | public String getDeviceId() { 104 | return deviceId; 105 | } 106 | 107 | 108 | @SneakyThrows 109 | private T executeSync(Function, CompletionStage> function) { 110 | return this.getAsyncRedis() 111 | .thenCompose(function) 112 | .toCompletableFuture() 113 | .get(10, TimeUnit.SECONDS); 114 | } 115 | 116 | private void executeAsync(Consumer> consumer) { 117 | this.getAsyncRedis() 118 | .thenAccept(consumer); 119 | } 120 | 121 | @SuppressWarnings("all") 122 | private T tryGetFromLocalCache(String key) { 123 | Object val = localCache.get(key); 124 | if (val == NullValue.instance) { 125 | return null; 126 | } 127 | if (val != null) { 128 | return (T) val; 129 | } else { 130 | localCache.put(key, Optional.ofNullable(val = executeSync(redis -> redis.hget(redisKey, key))).orElse(NullValue.instance)); 131 | } 132 | return (T) val; 133 | } 134 | 135 | private T tryGetFromConfCache(String key) { 136 | Object val = confCache.get(key); 137 | if (val == NullValue.instance) { 138 | return null; 139 | } 140 | if (val != null) { 141 | return (T) val; 142 | } else { 143 | confCache.put(key, Optional.ofNullable(val = executeSync(redis -> redis.hget(redisKey, key))).orElse(NullValue.instance)); 144 | } 145 | return (T) val; 146 | } 147 | 148 | @Override 149 | public String getServerId() { 150 | String serverId = tryGetFromLocalCache("serverId"); 151 | if (serverId == null || serverId.isEmpty()) { 152 | return null; 153 | } 154 | return serverId; 155 | } 156 | 157 | @Override 158 | public String getSessionId() { 159 | String sessionId = tryGetFromLocalCache("sessionId"); 160 | if (sessionId == null || sessionId.isEmpty()) { 161 | return null; 162 | } 163 | return sessionId; 164 | } 165 | 166 | @Override 167 | public byte getState() { 168 | Byte state = tryGetFromLocalCache("state"); 169 | return state == null ? DeviceState.unknown : state; 170 | } 171 | 172 | @Override 173 | @SneakyThrows 174 | public void putState(byte state) { 175 | localCache.put("state", state); 176 | 177 | executeAsync(redis -> redis.hset(redisKey, "state", state).thenRun(() -> changedListener.accept(false))); 178 | 179 | } 180 | 181 | @Override 182 | public CompletionStage checkState() { 183 | String serverId = getServerId(); 184 | if (serverId != null) { 185 | try { 186 | return plus.getTopic("device:state:check:".concat(serverId)) 187 | .publish(deviceId) 188 | .thenApply((subscribes) -> { 189 | if (subscribes <= 0) { 190 | //没有任何服务在监听话题,则认为服务已经不可用 191 | if (getState() == DeviceState.online) { 192 | log.debug("设备网关服务[{}]未正常运行,设备[{}]下线", serverId, deviceId); 193 | offline(); 194 | return DeviceState.offline; 195 | } 196 | } 197 | return getState(); 198 | }); 199 | } catch (Exception e) { 200 | log.error("检查设备状态失败", e); 201 | } 202 | } else { 203 | if (getState() == DeviceState.online) { 204 | log.debug("设备[{}]未注册到任何设备网关服务", deviceId); 205 | offline(); 206 | } 207 | } 208 | return CompletableFuture.completedFuture(getState()); 209 | } 210 | 211 | @Override 212 | public long getOnlineTime() { 213 | return Optional.ofNullable(executeSync(redis -> redis.hget(redisKey, "onlineTime"))) 214 | .map(Long.class::cast) 215 | .orElse(-1L); 216 | } 217 | 218 | @Override 219 | public long getOfflineTime() { 220 | return Optional.ofNullable(executeSync(redis -> redis.hget(redisKey, "offlineTime"))) 221 | .map(Long.class::cast) 222 | .orElse(-1L); 223 | } 224 | 225 | @Override 226 | public void online(String serverId, String sessionId) { 227 | Map map = new HashMap<>(); 228 | map.put("serverId", serverId); 229 | map.put("sessionId", sessionId); 230 | map.put("state", DeviceState.online); 231 | map.put("onlineTime", System.currentTimeMillis()); 232 | localCache.putAll(map); 233 | 234 | this.executeAsync(redis -> redis.hmset(redisKey, map).thenRun(() -> changedListener.accept(false))); 235 | } 236 | 237 | @Override 238 | public void offline() { 239 | Map map = new HashMap<>(); 240 | map.put("state", DeviceState.offline); 241 | map.put("offlineTime", System.currentTimeMillis()); 242 | map.put("serverId", ""); 243 | map.put("sessionId", ""); 244 | localCache.putAll(map); 245 | 246 | this.executeAsync(redis -> redis.hmset(redisKey, map).thenRun(() -> changedListener.accept(false))); 247 | } 248 | 249 | @Override 250 | public CompletionStage disconnect() { 251 | DisconnectDeviceMessage message=new DisconnectDeviceMessage(); 252 | message.setDeviceId(deviceId); 253 | message.setMessageId(IdUtils.newUUID()); 254 | 255 | return messageSender 256 | .send(message) 257 | .thenApply(DisconnectDeviceMessageReply::isSuccess); 258 | } 259 | 260 | @Override 261 | public CompletionStage authenticate(AuthenticationRequest request) { 262 | try { 263 | return getProtocol() 264 | .authenticate(request, this); 265 | } catch (Throwable e) { 266 | CompletableFuture future = new CompletableFuture<>(); 267 | future.completeExceptionally(e); 268 | return future; 269 | } 270 | } 271 | 272 | @Override 273 | public DeviceMetadata getMetadata() { 274 | 275 | Map deviceInfo = this.>> 276 | executeSync(redis -> redis.hmget(redisKey, "metadata", "protocol", "productId")) 277 | .stream() 278 | .filter(Value::hasValue) 279 | .collect(Collectors.toMap(KeyValue::getKey, KeyValue::getValue)); 280 | 281 | if (deviceInfo.isEmpty()) { 282 | throw new NullPointerException("设备信息不存在"); 283 | } 284 | String metaJson = (String) deviceInfo.get("metadata"); 285 | //设备没有单独的元数据,则获取设备产品型号的元数据 286 | if (metaJson == null || metaJson.isEmpty()) { 287 | return registry.getProduct((String) deviceInfo.get("productId")).getMetadata(); 288 | } 289 | return protocolSupports 290 | .getProtocol((String) deviceInfo.get("protocol")) 291 | .getMetadataCodec() 292 | .decode(metaJson); 293 | } 294 | 295 | private String getProductId() { 296 | return tryGetFromLocalCache("productId"); 297 | } 298 | 299 | @Override 300 | public ProtocolSupport getProtocol() { 301 | 302 | String protocol = tryGetFromLocalCache("protocol"); 303 | 304 | if (protocol != null) { 305 | return protocolSupports.getProtocol(protocol); 306 | } else { 307 | return Optional.ofNullable(registry.getProduct(getProductId())) 308 | .map(DeviceProductOperation::getProtocol) 309 | .orElseThrow(() -> new UnsupportedOperationException("设备[" + deviceId + "]未配置协议以及产品信息")); 310 | } 311 | } 312 | 313 | @Override 314 | public DeviceMessageSender messageSender() { 315 | 316 | return messageSender; 317 | } 318 | 319 | @Override 320 | public DeviceInfo getDeviceInfo() { 321 | Object info = tryGetFromLocalCache("info"); 322 | if (info instanceof String) { 323 | return JSON.parseObject((String) info, DeviceInfo.class); 324 | } 325 | if (info instanceof DeviceInfo) { 326 | return ((DeviceInfo) info); 327 | } 328 | log.warn("设备信息反序列化错误:{}", info); 329 | return null; 330 | } 331 | 332 | @Override 333 | public void update(DeviceInfo deviceInfo) { 334 | Map all = new HashMap<>(); 335 | all.put("info", JSON.toJSONString(deviceInfo)); 336 | localCache.put("info", deviceInfo); 337 | if (deviceInfo.getProtocol() != null) { 338 | all.put("protocol", deviceInfo.getProtocol()); 339 | localCache.put("protocol", deviceInfo.getProtocol()); 340 | } 341 | if (deviceInfo.getProductId() != null) { 342 | all.put("productId", deviceInfo.getProductId()); 343 | localCache.put("productId", deviceInfo.getProtocol()); 344 | } 345 | 346 | this.executeAsync(redis -> redis.hmset(redisKey, all).thenRun(() -> changedListener.accept(false))); 347 | } 348 | 349 | @Override 350 | public void updateMetadata(String metadata) { 351 | localCache.put("metadata", metadata); 352 | 353 | this.executeAsync(redis -> redis.hset(redisKey, "metadata", metadata) 354 | .thenRun(() -> changedListener.accept(false))); 355 | } 356 | 357 | private String createConfigKey(String key) { 358 | return "_cfg:".concat(key); 359 | } 360 | 361 | private String recoverConfigKey(String key) { 362 | return key.substring(5); 363 | } 364 | 365 | private Map recoverConfigMap(Map source) { 366 | return source.entrySet() 367 | .stream() 368 | .collect(Collectors.toMap(e -> recoverConfigKey(e.getKey()), Map.Entry::getValue)); 369 | } 370 | 371 | @Override 372 | @SuppressWarnings("all") 373 | public CompletionStage> getAllAsync(String... key) { 374 | //获取全部 375 | if (key.length == 0) { 376 | Map productConf = registry.getProduct(getProductId()).getAll(); 377 | Map meConf = (Map) localCache.computeIfAbsent("__all", __ -> 378 | this.> 379 | executeSync(redis -> redis.hgetall(redisKey)) 380 | .entrySet() 381 | .stream() 382 | .filter(e -> e.getKey().startsWith("_cfg:")) 383 | .collect(Collectors.toMap(e -> recoverConfigKey(e.getKey()), Map.Entry::getValue))); 384 | 385 | Map all = new HashMap<>(); 386 | 387 | all.putAll(productConf); 388 | all.putAll(meConf); 389 | return CompletableFuture.completedFuture(all); 390 | } 391 | 392 | Set keSet = Stream.of(key) 393 | .map(this::createConfigKey) 394 | .collect(Collectors.toSet()); 395 | 396 | String cacheKey = String.valueOf(keSet.hashCode()); 397 | 398 | Object cache = confCache.get(cacheKey); 399 | 400 | if (cache instanceof Map) { 401 | return CompletableFuture.completedFuture((Map) cache); 402 | } 403 | //null value 直接获取产品配置 404 | if (cache instanceof NullValue) { 405 | return registry.getProduct(getProductId()).getAllAsync(key); 406 | } 407 | 408 | return this.getAsyncRedis() 409 | .thenCompose(async -> async.hgetall(redisKey)) 410 | .thenApply(all -> all.entrySet().stream() 411 | .filter(e -> e.getKey().startsWith("_cfg:")) 412 | .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))) 413 | .thenApply(mine -> { 414 | if (mine.isEmpty()) { 415 | confCache.put(cacheKey, NullValue.instance); 416 | return registry 417 | .getProduct(getProductId()) 418 | .getAll(key); 419 | } 420 | //只有一部分,尝试从产品中获取 421 | if (mine.size() != key.length) { 422 | String[] inProductKey = keSet 423 | .stream() 424 | .filter(k -> !mine.containsKey(k)) 425 | .map(this::recoverConfigKey) 426 | .toArray(String[]::new); 427 | 428 | return Optional.of(registry 429 | .getProduct(getProductId()) 430 | .getAll(inProductKey)) 431 | .map(productPart -> { 432 | Map minePart = recoverConfigMap(mine); 433 | minePart.putAll(productPart); 434 | return minePart; 435 | }).get(); 436 | } 437 | Map recover = Collections.unmodifiableMap(recoverConfigMap(mine)); 438 | confCache.put(cacheKey, recover); 439 | return recover; 440 | }); 441 | } 442 | 443 | @Override 444 | @SuppressWarnings("all") 445 | @SneakyThrows 446 | public Map getAll(String... key) { 447 | return getAllAsync(key).toCompletableFuture() 448 | .get(10, TimeUnit.SECONDS); 449 | } 450 | 451 | @Override 452 | public ValueWrapper get(String key) { 453 | String confKey = createConfigKey(key); 454 | Object val = tryGetFromConfCache(confKey); 455 | 456 | if (val == null) { 457 | String productId = getProductId(); 458 | if (null != productId) { 459 | //获取产品的配置 460 | return registry.getProduct(productId).get(key); 461 | } 462 | return NullValueWrapper.instance; 463 | } 464 | 465 | return new DefaultValueWrapper(val); 466 | } 467 | 468 | @Override 469 | public void put(String key, Object value) { 470 | Objects.requireNonNull(value, "value"); 471 | String realKey = createConfigKey(key); 472 | confCache.remove("__all"); 473 | confCache.put(realKey, value); 474 | this.executeAsync(redis -> redis.hset(redisKey, realKey, value) 475 | .thenRun(() -> changedListener.accept(true))); 476 | } 477 | 478 | @Override 479 | @SuppressWarnings("all") 480 | public void putAll(Map conf) { 481 | if (conf == null || conf.isEmpty()) { 482 | return; 483 | } 484 | confCache.remove("__all"); 485 | Map newMap = new HashMap<>(); 486 | for (Map.Entry entry : conf.entrySet()) { 487 | newMap.put(createConfigKey(entry.getKey()), entry.getValue()); 488 | } 489 | 490 | this.executeAsync(redis -> redis.hmset(redisKey, newMap).thenRun(() -> changedListener.accept(true))); 491 | 492 | confCache.putAll(newMap); 493 | } 494 | 495 | @Override 496 | public Object remove(String key) { 497 | confCache.remove("__all"); 498 | Object val = get(key).value().orElse(null); 499 | 500 | String configKey = createConfigKey(key); 501 | 502 | executeAsync(redis -> redis.hdel(redisKey, configKey)); 503 | 504 | confCache.remove(configKey); 505 | changedListener.accept(true); 506 | return val; 507 | } 508 | 509 | void delete() { 510 | executeAsync(redis -> redis.del(redisKey)); 511 | changedListener.accept(true); 512 | confCache.clear(); 513 | localCache.clear(); 514 | } 515 | 516 | } 517 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/registry/redis/lettuce/LettuceDeviceProductOperation.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.registry.redis.lettuce; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import io.lettuce.core.KeyValue; 5 | import io.lettuce.core.Value; 6 | import io.lettuce.core.api.StatefulRedisConnection; 7 | import io.lettuce.core.api.async.RedisAsyncCommands; 8 | import lombok.SneakyThrows; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.jetlinks.core.ProtocolSupport; 11 | import org.jetlinks.core.ProtocolSupports; 12 | import org.jetlinks.core.device.DeviceProductInfo; 13 | import org.jetlinks.core.device.DeviceProductOperation; 14 | import org.jetlinks.core.metadata.DefaultValueWrapper; 15 | import org.jetlinks.core.metadata.DeviceMetadata; 16 | import org.jetlinks.core.metadata.NullValueWrapper; 17 | import org.jetlinks.core.metadata.ValueWrapper; 18 | import org.jetlinks.lettuce.LettucePlus; 19 | import org.jetlinks.registry.redis.NullValue; 20 | 21 | import java.util.*; 22 | import java.util.concurrent.CompletableFuture; 23 | import java.util.concurrent.CompletionStage; 24 | import java.util.concurrent.ConcurrentHashMap; 25 | import java.util.concurrent.TimeUnit; 26 | import java.util.function.Consumer; 27 | import java.util.function.Function; 28 | import java.util.stream.Collectors; 29 | import java.util.stream.Stream; 30 | 31 | /** 32 | * @author zhouhao 33 | * @since 1.0.0 34 | */ 35 | @Slf4j 36 | @SuppressWarnings("all") 37 | public class LettuceDeviceProductOperation implements DeviceProductOperation { 38 | 39 | private Map localCache = new ConcurrentHashMap<>(32); 40 | 41 | 42 | private ProtocolSupports protocolSupports; 43 | 44 | private Runnable cacheChangedListener; 45 | 46 | private LettucePlus plus; 47 | 48 | private String redisKey; 49 | 50 | public LettuceDeviceProductOperation(String redisKey, 51 | LettucePlus plus, 52 | ProtocolSupports protocolSupports, 53 | Runnable cacheChangedListener) { 54 | this.plus = plus; 55 | this.protocolSupports = protocolSupports; 56 | this.redisKey = redisKey; 57 | this.cacheChangedListener = () -> { 58 | localCache.clear(); 59 | cacheChangedListener.run(); 60 | }; 61 | } 62 | 63 | void clearCache() { 64 | localCache.clear(); 65 | } 66 | 67 | @Override 68 | public DeviceMetadata getMetadata() { 69 | return getProtocol() 70 | .getMetadataCodec() 71 | .decode(tryGetFromLocalCache("metadata")); 72 | } 73 | 74 | @SuppressWarnings("all") 75 | private T tryGetFromLocalCache(String key) { 76 | Object val = localCache.get(key); 77 | if (val == NullValue.instance) { 78 | return null; 79 | } 80 | if (val != null) { 81 | return (T) val; 82 | } else { 83 | localCache.put(key, Optional.ofNullable(val = executeSync(redis -> redis.hget(redisKey, key))).orElse(NullValue.instance)); 84 | } 85 | return (T) val; 86 | } 87 | 88 | @SneakyThrows 89 | private T executeSync(Function, CompletionStage> function) { 90 | return this.getAsyncRedis() 91 | .thenCompose(function) 92 | .toCompletableFuture() 93 | .get(10, TimeUnit.SECONDS); 94 | } 95 | 96 | private void executeAsync(Consumer> consumer) { 97 | this.getAsyncRedis() 98 | .thenAccept(consumer); 99 | } 100 | 101 | protected CompletionStage> getAsyncRedis() { 102 | return plus 103 | .getConnection() 104 | .thenApply(StatefulRedisConnection::async); 105 | } 106 | 107 | @Override 108 | public void updateMetadata(String metadata) { 109 | executeAsync(redis -> redis.hset(redisKey, "metadata", metadata)); 110 | 111 | localCache.put("metadata", metadata); 112 | } 113 | 114 | @Override 115 | public DeviceProductInfo getInfo() { 116 | Object info = tryGetFromLocalCache("info"); 117 | if (info instanceof DeviceProductInfo) { 118 | return ((DeviceProductInfo) info); 119 | } 120 | 121 | if (info instanceof String) { 122 | return JSON.parseObject(((String) info), DeviceProductInfo.class); 123 | } 124 | log.warn("设备产品信息反序列化错误:{}", info); 125 | return null; 126 | } 127 | 128 | @Override 129 | public void update(DeviceProductInfo info) { 130 | Map all = new HashMap<>(); 131 | all.put("info", JSON.toJSONString(info)); 132 | if (info.getProtocol() != null) { 133 | all.put("protocol", info.getProtocol()); 134 | } 135 | executeAsync(redis -> redis.hmset(redisKey, (Map) all).thenRun(this.cacheChangedListener::run)); 136 | 137 | localCache.putAll(all); 138 | } 139 | 140 | @Override 141 | public ProtocolSupport getProtocol() { 142 | return protocolSupports.getProtocol(tryGetFromLocalCache("protocol")); 143 | } 144 | 145 | private String createConfigKey(String key) { 146 | return "_cfg:".concat(key); 147 | } 148 | 149 | private String recoverConfigKey(String key) { 150 | return key.substring(5); 151 | } 152 | 153 | @Override 154 | public ValueWrapper get(String key) { 155 | Object conf = tryGetFromLocalCache(createConfigKey(key)); 156 | if (null == conf) { 157 | return NullValueWrapper.instance; 158 | } 159 | return new DefaultValueWrapper(conf); 160 | } 161 | 162 | @Override 163 | public CompletionStage> getAllAsync(String... key) { 164 | if (key.length == 0) { 165 | if (localCache.containsKey("__all")) { 166 | return CompletableFuture.completedFuture((Map) localCache.get("__all")); 167 | } else { 168 | return this.getAsyncRedis() 169 | .thenCompose(redis -> { 170 | return redis.hgetall(redisKey); 171 | }) 172 | .thenApply(all -> { 173 | return all 174 | .entrySet() 175 | .stream() 176 | .filter(kv -> String.valueOf(kv.getKey()).startsWith("_cfg:")) 177 | .collect(Collectors.toMap(e -> recoverConfigKey(String.valueOf(e.getKey())), Map.Entry::getValue)); 178 | }).whenComplete((all, error) -> { 179 | if (all != null) { 180 | localCache.put("__all", all); 181 | } 182 | }); 183 | } 184 | } 185 | 186 | Set keSet = Stream.of(key).map(this::createConfigKey).collect(Collectors.toSet()); 187 | 188 | String cacheKey = String.valueOf(keSet.hashCode()); 189 | 190 | Object cache = localCache.get(cacheKey); 191 | 192 | if (cache instanceof Map) { 193 | return CompletableFuture.completedFuture((Map) cache); 194 | } 195 | if (cache instanceof NullValue) { 196 | return CompletableFuture.completedFuture(Collections.emptyMap()); 197 | } 198 | return getAsyncRedis() 199 | .thenCompose(redis -> { 200 | return redis.hmget(redisKey, keSet.toArray()); 201 | }) 202 | .thenApply(kv -> kv.stream() 203 | .filter(Value::hasValue) 204 | .collect(Collectors.toMap(e -> recoverConfigKey(String.valueOf(e.getKey())), KeyValue::getValue, (_1, _2) -> _1))) 205 | .whenComplete((map, err) -> { 206 | if (map != null) { 207 | localCache.put(cacheKey, map); 208 | } 209 | }); 210 | } 211 | 212 | @Override 213 | @SuppressWarnings("all") 214 | @SneakyThrows 215 | public Map getAll(String... key) { 216 | 217 | return getAllAsync(key).toCompletableFuture().get(10, TimeUnit.SECONDS); 218 | } 219 | 220 | @Override 221 | public void putAll(Map conf) { 222 | if (conf == null || conf.isEmpty()) { 223 | return; 224 | } 225 | Map newMap = new HashMap<>(); 226 | for (Map.Entry entry : conf.entrySet()) { 227 | newMap.put(createConfigKey(entry.getKey()), entry.getValue()); 228 | } 229 | localCache.putAll(newMap); 230 | 231 | this.executeAsync(redis -> { 232 | redis.hmset(redisKey, newMap) 233 | .thenRun(this.cacheChangedListener::run); 234 | }); 235 | } 236 | 237 | @Override 238 | public void put(String key, Object value) { 239 | String configKey = createConfigKey(key); 240 | localCache.put(configKey, value); 241 | executeAsync(redis -> redis.hset(redisKey, createConfigKey(key), value).thenRun(this.cacheChangedListener::run)); 242 | } 243 | 244 | @Override 245 | public Object remove(String key) { 246 | Object val = get(key).value().orElse(null); 247 | 248 | String configKey = createConfigKey(key); 249 | localCache.remove(configKey); 250 | 251 | executeAsync(redis -> redis.hdel(redisKey, key).thenRun(cacheChangedListener::run)); 252 | 253 | return val; 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/main/java/org/jetlinks/registry/redis/lettuce/LettuceDeviceRegistry.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.registry.redis.lettuce; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.jetlinks.core.ProtocolSupports; 5 | import org.jetlinks.core.device.DeviceInfo; 6 | import org.jetlinks.core.device.DeviceOperation; 7 | import org.jetlinks.core.device.DeviceState; 8 | import org.jetlinks.core.device.registry.DeviceMessageHandler; 9 | import org.jetlinks.core.device.registry.DeviceRegistry; 10 | import org.jetlinks.core.message.interceptor.DeviceMessageSenderInterceptor; 11 | import org.jetlinks.lettuce.LettucePlus; 12 | import org.jetlinks.lettuce.RedisTopic; 13 | import org.jetlinks.registry.redis.CompositeDeviceMessageSenderInterceptor; 14 | 15 | import java.lang.ref.SoftReference; 16 | import java.util.Map; 17 | import java.util.Optional; 18 | import java.util.concurrent.ConcurrentHashMap; 19 | 20 | /** 21 | * @author zhouhao 22 | * @since 1.0.0 23 | */ 24 | @Slf4j 25 | public class LettuceDeviceRegistry implements DeviceRegistry { 26 | 27 | private LettucePlus plus; 28 | 29 | private ProtocolSupports protocolSupports; 30 | 31 | private Map> localCache = new ConcurrentHashMap<>(1024); 32 | 33 | private Map> productLocalCache = new ConcurrentHashMap<>(128); 34 | 35 | private RedisTopic cacheChangedTopic; 36 | 37 | private final CompositeDeviceMessageSenderInterceptor interceptor = new CompositeDeviceMessageSenderInterceptor(); 38 | 39 | private DeviceMessageHandler messageHandler; 40 | 41 | public LettuceDeviceRegistry(LettucePlus plus, 42 | DeviceMessageHandler handler, 43 | ProtocolSupports protocolSupports) { 44 | this.plus = plus; 45 | this.protocolSupports = protocolSupports; 46 | this.cacheChangedTopic = plus.getTopic("device:registry:cache:changed"); 47 | this.messageHandler = handler; 48 | 49 | cacheChangedTopic.addListener((t, id) -> { 50 | 51 | String[] split = id.split("[@]"); 52 | byte clearType = 1; 53 | if (split.length == 2) { 54 | id = split[0]; 55 | clearType = Byte.valueOf(split[1]); 56 | } else if (split.length > 2) { 57 | log.warn("本地缓存可能出错,id[{}]不合法", id); 58 | } 59 | boolean clearConf = clearType == 1; 60 | 61 | Optional.ofNullable(localCache.get(id)) 62 | .map(SoftReference::get) 63 | .ifPresent(cache -> cache.clearCache(clearConf)); 64 | 65 | Optional.ofNullable(productLocalCache.get(id)) 66 | .map(SoftReference::get) 67 | .ifPresent(LettuceDeviceProductOperation::clearCache); 68 | 69 | }); 70 | } 71 | 72 | public void addInterceptor(DeviceMessageSenderInterceptor interceptor) { 73 | this.interceptor.addInterceptor(interceptor); 74 | } 75 | 76 | @Override 77 | public LettuceDeviceProductOperation getProduct(String productId) { 78 | if (productId == null || productId.isEmpty()) { 79 | return null; 80 | } 81 | SoftReference reference = productLocalCache.get(productId); 82 | 83 | if (reference == null || reference.get() == null) { 84 | 85 | productLocalCache.put(productId, reference = new SoftReference<>(doGetProduct(productId))); 86 | 87 | } 88 | return reference.get(); 89 | } 90 | 91 | @Override 92 | public LettuceDeviceOperation getDevice(String deviceId) { 93 | SoftReference reference = localCache.get(deviceId); 94 | 95 | if (reference == null || reference.get() == null) { 96 | LettuceDeviceOperation operation = doGetOperation(deviceId); 97 | //unknown的设备不使用缓存 98 | if (operation.getState() == DeviceState.unknown) { 99 | return operation; 100 | } 101 | localCache.put(deviceId, reference = new SoftReference<>(operation)); 102 | 103 | } 104 | return reference.get(); 105 | } 106 | 107 | private LettuceDeviceProductOperation doGetProduct(String productId) { 108 | return new LettuceDeviceProductOperation("product:".concat(productId).concat(":reg") 109 | , plus 110 | , protocolSupports, 111 | () -> cacheChangedTopic.publish(productId.concat("@-1"))); 112 | } 113 | 114 | private LettuceDeviceOperation doGetOperation(String deviceId) { 115 | return new LettuceDeviceOperation(deviceId, plus, 116 | protocolSupports, 117 | messageHandler, 118 | this, 119 | interceptor, 120 | (isConf) -> cacheChangedTopic.publish(deviceId.concat("@").concat(isConf ? "1" : "0"))); 121 | } 122 | 123 | @Override 124 | public DeviceOperation registry(DeviceInfo deviceInfo) { 125 | DeviceOperation operation = getDevice(deviceInfo.getId()); 126 | operation.update(deviceInfo); 127 | operation.putState(DeviceState.offline); 128 | return operation; 129 | } 130 | 131 | @Override 132 | public void unRegistry(String deviceId) { 133 | LettuceDeviceOperation operation = getDevice(deviceId); 134 | operation.putState(DeviceState.unknown); 135 | 136 | operation.disconnect() 137 | .whenComplete((r, err) -> operation.delete()); 138 | 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /src/test/java/org/jetlinks/registry/redis/MockProtocolSupports.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.registry.redis; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import io.netty.buffer.Unpooled; 6 | import org.jetlinks.core.ProtocolSupport; 7 | import org.jetlinks.core.ProtocolSupports; 8 | import org.jetlinks.core.device.AuthenticationRequest; 9 | import org.jetlinks.core.device.AuthenticationResponse; 10 | import org.jetlinks.core.device.DeviceOperation; 11 | import org.jetlinks.core.message.CommonDeviceMessageReply; 12 | import org.jetlinks.core.message.DeviceMessage; 13 | import org.jetlinks.core.message.codec.*; 14 | import org.jetlinks.core.metadata.DeviceMetadataCodec; 15 | import reactor.core.publisher.Mono; 16 | 17 | import javax.annotation.Nonnull; 18 | import java.nio.charset.StandardCharsets; 19 | 20 | /** 21 | * @author zhouhao 22 | * @since 1.0.0 23 | */ 24 | public class MockProtocolSupports implements ProtocolSupports { 25 | @Override 26 | public Mono getProtocol(String protocol) { 27 | return Mono.just(new ProtocolSupport() { 28 | @Override 29 | @Nonnull 30 | public String getId() { 31 | return "mock"; 32 | } 33 | 34 | @Override 35 | public String getName() { 36 | return "模拟协议"; 37 | } 38 | 39 | @Override 40 | public String getDescription() { 41 | return ""; 42 | } 43 | 44 | @Override 45 | @Nonnull 46 | public DeviceMessageCodec getMessageCodec() { 47 | 48 | return new DeviceMessageCodec() { 49 | @Override 50 | public Mono encode(Transport transport, MessageEncodeContext context) { 51 | return Mono.just(EncodedMessage.mqtt(context.getMessage().getDeviceId(), "command", 52 | Unpooled.copiedBuffer(context.getMessage().toJson().toJSONString().getBytes()))); 53 | } 54 | 55 | @Override 56 | public Mono decode(Transport transport, MessageDecodeContext context) { 57 | JSONObject jsonObject = JSON.parseObject(context.getMessage().getByteBuf().toString(StandardCharsets.UTF_8)); 58 | if ("read-property".equals(jsonObject.get("type"))) { 59 | // return jsonObject.toJavaObject(GettingPropertyMessageReply.class); 60 | } 61 | return Mono.just(jsonObject.toJavaObject(CommonDeviceMessageReply.class)); 62 | } 63 | }; 64 | } 65 | 66 | @Override 67 | @Nonnull 68 | public DeviceMetadataCodec getMetadataCodec() { 69 | throw new UnsupportedOperationException(); 70 | } 71 | 72 | @Override 73 | @Nonnull 74 | public Mono authenticate(@Nonnull AuthenticationRequest request, @Nonnull DeviceOperation deviceOperation) { 75 | return Mono.just(AuthenticationResponse.success()); 76 | } 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/org/jetlinks/registry/redis/RedissonDeviceOperationTest.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.registry.redis; 2 | 3 | import lombok.SneakyThrows; 4 | import org.jetlinks.core.device.DeviceMessageSender; 5 | import org.jetlinks.core.device.DeviceOperation; 6 | import org.jetlinks.core.device.DeviceState; 7 | import org.jetlinks.core.enums.ErrorCode; 8 | import org.jetlinks.core.message.*; 9 | import org.jetlinks.core.message.exception.FunctionUndefinedException; 10 | import org.jetlinks.core.message.exception.IllegalParameterException; 11 | import org.jetlinks.core.message.exception.ParameterUndefinedException; 12 | import org.jetlinks.core.message.function.FunctionInvokeMessageReply; 13 | import org.jetlinks.core.message.interceptor.DeviceMessageSenderInterceptor; 14 | import org.jetlinks.core.message.property.ReadPropertyMessage; 15 | import org.jetlinks.core.message.property.ReadPropertyMessageReply; 16 | import org.jetlinks.supports.official.JetLinksProtocolSupport; 17 | import org.junit.After; 18 | import org.junit.Assert; 19 | import org.junit.Before; 20 | import org.junit.Test; 21 | import org.redisson.api.RedissonClient; 22 | import org.springframework.core.io.ClassPathResource; 23 | import org.springframework.util.StreamUtils; 24 | 25 | import java.nio.charset.StandardCharsets; 26 | import java.util.concurrent.*; 27 | import java.util.concurrent.atomic.AtomicReference; 28 | 29 | import static io.vavr.API.Try; 30 | 31 | public class RedissonDeviceOperationTest { 32 | 33 | RedissonClient client; 34 | 35 | public RedissonDeviceRegistry registry; 36 | 37 | @Before 38 | public void init() { 39 | client = RedissonHelper.newRedissonClient(); 40 | JetLinksProtocolSupport jetLinksProtocolSupport = new JetLinksProtocolSupport(); 41 | 42 | registry = new RedissonDeviceRegistry(client, new RedissonDeviceMessageHandler(client), protocol -> jetLinksProtocolSupport); 43 | 44 | registry.addInterceptor(new DeviceMessageSenderInterceptor() { 45 | @Override 46 | public DeviceMessage preSend(DeviceOperation device, DeviceMessage message) { 47 | return message; 48 | } 49 | 50 | @Override 51 | public CompletionStage afterReply(DeviceOperation device, DeviceMessage message, R reply) { 52 | reply.addHeader("ts", System.currentTimeMillis()); 53 | return CompletableFuture.completedFuture(reply); 54 | } 55 | }); 56 | } 57 | 58 | @After 59 | public void close(){ 60 | client.shutdown(); 61 | } 62 | //设备网关服务宕机 63 | //场景: 设备网关服务宕机,未及时更新设备状态信息。继续往设备发消息时,会执行设备状态检查,更新状态 64 | @Test 65 | @SneakyThrows 66 | public void testServerOfflineCheckState() { 67 | DeviceOperation operation = registry.getDevice("test2"); 68 | 69 | try { 70 | //模拟上线 71 | operation.online("test2-server", "test"); 72 | 73 | Assert.assertEquals(operation.getState(), DeviceState.online); 74 | 75 | //模拟发送一条消息,该设备实际上并不在线。应该会自动执行状态检查 76 | FunctionInvokeMessageReply reply = operation.messageSender() 77 | .invokeFunction("test") 78 | .trySend(10, TimeUnit.SECONDS) 79 | .recover(TimeoutException.class, (__) -> FunctionInvokeMessageReply.create().error(ErrorCode.TIME_OUT)) 80 | .get(); 81 | 82 | Assert.assertFalse(reply.isSuccess()); 83 | 84 | //调用了设备状态检查并自动更新了设备状态 85 | Assert.assertEquals(operation.getState(), DeviceState.offline); 86 | Assert.assertNull(operation.getServerId()); 87 | } finally { 88 | registry.unRegistry("test2"); 89 | } 90 | 91 | } 92 | 93 | @Test 94 | public void testSendOfflineServer() { 95 | DeviceOperation operation = registry.getDevice("test2"); 96 | operation.online("test3-server", "test"); 97 | 98 | Assert.assertEquals(operation.getState(),DeviceState.online); 99 | 100 | Assert.assertEquals(operation.messageSender() 101 | .readProperty("test") 102 | .custom(Headers.async.setter()) 103 | .trySend(10, TimeUnit.SECONDS) 104 | .map(CommonDeviceMessageReply::getCode) 105 | .get(), ErrorCode.CLIENT_OFFLINE.name()); 106 | 107 | Assert.assertEquals(operation.getState(),DeviceState.offline); 108 | 109 | } 110 | 111 | //设备网关服务正常运行,但是设备未连接到当前网关服务 112 | //场景: 设备网关宕机,未及时将设备更新为离线。当网关服务重新启动后,设备其实已经没有连接到这台服务器了。 113 | @Test 114 | @SneakyThrows 115 | public void testServerOnlineNotConnectCheckState() { 116 | 117 | try { 118 | CountDownLatch latch = new CountDownLatch(1); 119 | 120 | 121 | DeviceOperation operation = registry.getDevice("test2"); 122 | //模拟上线 123 | operation.online("test2-server", "test"); 124 | 125 | Assert.assertEquals(operation.getState(), DeviceState.online); 126 | 127 | //消息处理器 128 | RedissonDeviceMessageHandler handler = new RedissonDeviceMessageHandler(client); 129 | 130 | handler.handleDeviceCheck("test2-server", deviceId -> { 131 | 132 | //模拟设备并没有连接到本服务器,修改设备状态离线. 133 | operation.offline(); 134 | latch.countDown(); 135 | 136 | }); 137 | 138 | //主动调用设备状态检查 139 | operation.checkState(); 140 | 141 | //调用了设备状态检查 142 | Assert.assertTrue(latch.await(10, TimeUnit.SECONDS)); 143 | Assert.assertEquals(operation.getState(), DeviceState.offline); 144 | } finally { 145 | registry.unRegistry("test2"); 146 | } 147 | } 148 | 149 | @Test 150 | @SneakyThrows 151 | public void testSendAndReplyMessage() { 152 | try { 153 | DeviceOperation operation = registry.getDevice("test2"); 154 | operation.online("test-server", "12"); 155 | DeviceMessageSender sender = operation.messageSender(); 156 | 157 | 158 | RedissonDeviceMessageHandler handler = new RedissonDeviceMessageHandler(client); 159 | 160 | handler.markMessageAsync("testId").toCompletableFuture().get(10, TimeUnit.SECONDS); 161 | Assert.assertTrue(handler.messageIsAsync("testId").toCompletableFuture().get(10, TimeUnit.SECONDS)); 162 | 163 | AtomicReference messageReference = new AtomicReference<>(); 164 | //处理发往设备的消息 165 | handler.handleMessage("test-server", message -> { 166 | messageReference.set(message); 167 | 168 | if (message instanceof RepayableDeviceMessage) { 169 | try { 170 | Thread.sleep(2000); 171 | } catch (Exception e) { 172 | 173 | } 174 | //模拟设备回复消息 175 | DeviceMessageReply reply = ((RepayableDeviceMessage) message).newReply(); 176 | reply.from(message); 177 | reply.error(ErrorCode.REQUEST_HANDLING); 178 | handler.reply(reply); 179 | } 180 | 181 | }); 182 | //发送消息s 183 | ReadPropertyMessageReply reply = sender.readProperty("test") 184 | .messageId("test-message") 185 | .send() 186 | .toCompletableFuture() 187 | .get(10, TimeUnit.SECONDS); 188 | Assert.assertNotNull(messageReference.get()); 189 | Assert.assertTrue(messageReference.get() instanceof ReadPropertyMessage); 190 | Assert.assertNotNull(reply); 191 | System.out.println(reply); 192 | 193 | messageReference.set(null); 194 | 195 | sender.readProperty("test") 196 | .messageId("test-retrieve-msg") 197 | .trySend(1, TimeUnit.MILLISECONDS); 198 | 199 | TimeUnit.SECONDS.sleep(5); 200 | ReadPropertyMessageReply retrieve = sender.readProperty("test") 201 | .messageId("test-retrieve-msg") 202 | .retrieveReply() 203 | .toCompletableFuture() 204 | .get(1, TimeUnit.SECONDS); 205 | Assert.assertNotNull(messageReference.get()); 206 | Assert.assertNotNull(retrieve); 207 | Assert.assertEquals(retrieve.getCode(), ErrorCode.REQUEST_HANDLING.name()); 208 | System.out.println(retrieve); 209 | 210 | } finally { 211 | registry.unRegistry("test2"); 212 | } 213 | 214 | } 215 | 216 | 217 | @Test 218 | @SneakyThrows 219 | public void testValidateParameter() { 220 | try { 221 | DeviceOperation operation = registry.getDevice("test3"); 222 | String metaData = StreamUtils.copyToString(new ClassPathResource("testValidateParameter.meta.json").getInputStream(), StandardCharsets.UTF_8); 223 | operation.updateMetadata(metaData); 224 | 225 | Assert.assertNotNull(operation.getMetadata()); 226 | 227 | //function未定义 228 | 229 | Assert.assertTrue(Try(() -> operation.messageSender().invokeFunction("getSysInfoUndefined").validate()) 230 | .map(r -> false) 231 | .recover(FunctionUndefinedException.class, true) 232 | .recover(err -> false) 233 | .get()); 234 | //参数错误 235 | Assert.assertTrue(Try(() -> operation.messageSender().invokeFunction("getSysInfo").validate()) 236 | .map(r -> false) 237 | .recover(IllegalArgumentException.class, true) 238 | .recover(err -> false) 239 | .get()); 240 | 241 | //参数未定义 242 | Assert.assertTrue(Try(() -> operation.messageSender() 243 | .invokeFunction("getSysInfo") 244 | .addParameter("test", "123") 245 | .validate()) 246 | .map(r -> false) 247 | .recover(ParameterUndefinedException.class, true) 248 | .recover(err -> false) 249 | .get()); 250 | 251 | //参数值类型错误 252 | Assert.assertTrue(Try(() -> operation.messageSender() 253 | .invokeFunction("getSysInfo") 254 | .addParameter("useCache", "2") 255 | .validate()) 256 | .map(r -> false) 257 | .recover(IllegalParameterException.class, true) 258 | .recover(err -> false) 259 | .get()); 260 | 261 | //通过 262 | operation.messageSender() 263 | .invokeFunction("getSysInfo") 264 | .addParameter("useCache", "1") 265 | .validate(); 266 | 267 | operation.messageSender() 268 | .invokeFunction("getSysInfo") 269 | .addParameter("useCache", "0") 270 | .validate(); 271 | } finally { 272 | registry.unRegistry("test3"); 273 | } 274 | } 275 | 276 | } -------------------------------------------------------------------------------- /src/test/java/org/jetlinks/registry/redis/RedissonDeviceRegistryTest.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.registry.redis; 2 | 3 | import io.vavr.control.Try; 4 | import lombok.SneakyThrows; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.jetlinks.core.device.*; 7 | import org.jetlinks.core.device.registry.DeviceMessageHandler; 8 | import org.jetlinks.core.message.CommonDeviceMessageReply; 9 | import org.jetlinks.core.message.DeviceMessage; 10 | import org.jetlinks.core.message.DeviceMessageReply; 11 | import org.jetlinks.core.message.interceptor.DeviceMessageSenderInterceptor; 12 | import org.junit.After; 13 | import org.junit.Assert; 14 | import org.junit.Before; 15 | import org.junit.Test; 16 | import org.redisson.api.RedissonClient; 17 | 18 | import java.util.*; 19 | import java.util.concurrent.CompletableFuture; 20 | import java.util.concurrent.CompletionStage; 21 | import java.util.concurrent.TimeUnit; 22 | 23 | 24 | /** 25 | * @author zhouhao 26 | * @since 1.0.0 27 | */ 28 | @Slf4j 29 | public class RedissonDeviceRegistryTest { 30 | 31 | private RedissonDeviceRegistry registry; 32 | 33 | private DeviceMessageHandler messageHandler; 34 | RedissonClient client; 35 | 36 | @Before 37 | public void init() { 38 | client = RedissonHelper.newRedissonClient(); 39 | 40 | 41 | messageHandler = new RedissonDeviceMessageHandler(client); 42 | registry = new RedissonDeviceRegistry(client, messageHandler, new MockProtocolSupports()); 43 | registry.addInterceptor(new DeviceMessageSenderInterceptor() { 44 | @Override 45 | public DeviceMessage preSend(DeviceOperation device, DeviceMessage message) { 46 | return message; 47 | } 48 | 49 | @Override 50 | public CompletionStage afterReply(DeviceOperation device, DeviceMessage message, R reply) { 51 | return CompletableFuture.supplyAsync(() -> { 52 | log.debug("reply Interceptor :{}", reply); 53 | return reply; 54 | }); 55 | } 56 | }); 57 | } 58 | 59 | @After 60 | public void after() { 61 | client.shutdown(); 62 | } 63 | 64 | public DeviceInfo newDeviceInfo() { 65 | DeviceInfo deviceInfo = new DeviceInfo(); 66 | deviceInfo.setId(UUID.randomUUID().toString()); 67 | deviceInfo.setCreatorId("admin"); 68 | deviceInfo.setCreatorName("admin"); 69 | deviceInfo.setProjectId("p001"); 70 | deviceInfo.setProjectName("项目1"); 71 | deviceInfo.setProtocol("jet-links"); 72 | deviceInfo.setType((byte) 0); 73 | return deviceInfo; 74 | } 75 | 76 | @Test 77 | @SneakyThrows 78 | public void testConfig() { 79 | DeviceInfo info = newDeviceInfo(); 80 | DeviceProductInfo productInfo = new DeviceProductInfo(); 81 | productInfo.setId("test"); 82 | productInfo.setName("测试"); 83 | productInfo.setProjectId("test"); 84 | productInfo.setProtocol("jet-links"); 85 | info.setProductId(productInfo.getId()); 86 | info.setProductName(productInfo.getName()); 87 | 88 | DeviceProductOperation productOperation = registry.getProduct(productInfo.getId()); 89 | productOperation.update(productInfo); 90 | productOperation.put("test_config", "1234"); 91 | productOperation.put("test_config__", "aaa"); 92 | try { 93 | Assert.assertNotNull(productOperation.getProtocol()); 94 | 95 | DeviceOperation operation = registry.registry(info); 96 | 97 | Assert.assertNotNull(operation.getProtocol()); 98 | 99 | Assert.assertEquals(operation.get("test_config").asString().orElse(null), "1234"); 100 | 101 | Map conf = operation.getAll("test_config"); 102 | System.out.println(conf); 103 | Assert.assertEquals(conf.get("test_config"), "1234"); 104 | 105 | operation.put("test_config", "2345"); 106 | operation.put("test_config2", 1234); 107 | 108 | Assert.assertEquals(operation.get("test_config").asString().orElse(null), "2345"); 109 | conf = operation.getAll("test_config", "test_config__", "test_config2"); 110 | System.out.println(conf); 111 | Assert.assertEquals(conf.get("test_config"), "2345"); 112 | Assert.assertEquals(conf.get("test_config2"), 1234); 113 | Assert.assertEquals(conf.get("test_config__"), "aaa"); 114 | 115 | Map all = operation.getAll(); 116 | Assert.assertEquals(all.get("test_config"), "2345"); 117 | Assert.assertEquals(all.get("test_config2"), 1234); 118 | Assert.assertEquals(all.get("test_config__"), "aaa"); 119 | System.out.println(all); 120 | 121 | Assert.assertEquals(operation.remove("test_config"), "2345"); 122 | Assert.assertTrue(operation.get("test_config").isPresent()); 123 | 124 | operation.putAll(all); 125 | Assert.assertEquals(all.get("test_config"), "2345"); 126 | 127 | operation.putAll(null); 128 | operation.putAll(Collections.emptyMap()); 129 | 130 | Assert.assertFalse(Try.of(() -> { 131 | 132 | operation.put("test", null); 133 | 134 | return true; 135 | }).recover(NullPointerException.class, false).get()); 136 | 137 | 138 | } finally { 139 | registry.unRegistry(info.getId()); 140 | } 141 | } 142 | 143 | @Test 144 | public void testRegistry() { 145 | DeviceInfo info = newDeviceInfo(); 146 | try { 147 | DeviceOperation operation = registry.registry(info); 148 | Assert.assertNotNull(operation); 149 | Assert.assertEquals(operation.getState(), DeviceState.offline); 150 | operation.online("server-01", "session-01"); 151 | Assert.assertEquals(operation.getState(), DeviceState.online); 152 | Assert.assertEquals(operation.getServerId(), "server-01"); 153 | Assert.assertEquals(operation.getSessionId(), "session-01"); 154 | Assert.assertTrue(operation.isOnline()); 155 | operation.offline(); 156 | Assert.assertFalse(operation.isOnline()); 157 | Assert.assertNull(operation.getServerId()); 158 | Assert.assertNull(operation.getSessionId()); 159 | } finally { 160 | registry.unRegistry(info.getId()); 161 | DeviceOperation operation = registry.getDevice(info.getId()); 162 | Assert.assertEquals(operation.getState(), DeviceState.unknown); 163 | } 164 | } 165 | 166 | @Test 167 | public void benchmarkTest() { 168 | int size = 1000; 169 | 170 | List operations = new ArrayList<>(size); 171 | long time = System.currentTimeMillis(); 172 | System.out.println("RAM:" + Runtime.getRuntime().totalMemory() / 1024 / 1024 + "MB"); 173 | for (int i = 0; i < size; i++) { 174 | DeviceInfo info = newDeviceInfo(); 175 | DeviceOperation operation = registry.registry(info); 176 | operations.add(operation); 177 | } 178 | long completeTime = System.currentTimeMillis(); 179 | log.debug("registry {} devices use {}ms", size, completeTime - time); 180 | 181 | time = System.currentTimeMillis(); 182 | for (DeviceOperation operation : operations) { 183 | operation.authenticate(new AuthenticationRequest() { 184 | }); 185 | operation.online("server_01", "session_0"); 186 | operation.getDeviceInfo(); 187 | } 188 | completeTime = System.currentTimeMillis(); 189 | log.debug("online {} devices use {}ms", size, completeTime - time); 190 | System.out.println("RAM:" + Runtime.getRuntime().totalMemory() / 1024 / 1024 + "MB"); 191 | for (DeviceOperation operation : operations) { 192 | registry.unRegistry(operation.getDeviceInfo().getId()); 193 | } 194 | } 195 | 196 | @Test 197 | @SneakyThrows 198 | public void testSendMessage() { 199 | DeviceInfo device = newDeviceInfo(); 200 | device.setId("test"); 201 | //注册 202 | DeviceOperation operation = registry.registry(device); 203 | 204 | //上线 205 | operation.online("test", "test"); 206 | 207 | messageHandler.handleMessage("test", msg -> { 208 | CommonDeviceMessageReply reply = msg.toJson().toJavaObject(CommonDeviceMessageReply.class); 209 | reply.setSuccess(true); 210 | reply.setMessage("成功"); 211 | messageHandler.reply(reply); 212 | }); 213 | 214 | CommonDeviceMessageReply reply = operation.messageSender() 215 | .invokeFunction("test") 216 | .send() 217 | .toCompletableFuture() 218 | .get(1, TimeUnit.SECONDS); 219 | Assert.assertNotNull(reply); 220 | Assert.assertTrue(reply.isSuccess()); 221 | 222 | long time = System.currentTimeMillis(); 223 | 224 | long len = 100; 225 | for (int i = 0; i < len; i++) { 226 | reply = operation.messageSender() 227 | .invokeFunction("test") 228 | .send() 229 | .toCompletableFuture() 230 | .get(5, TimeUnit.SECONDS); 231 | Assert.assertNotNull(reply); 232 | Assert.assertTrue(reply.isSuccess()); 233 | } 234 | System.out.println("执行" + len + "次消息收发,耗时:" + (System.currentTimeMillis() - time) + "ms"); 235 | registry.unRegistry(device.getId()); 236 | } 237 | 238 | } -------------------------------------------------------------------------------- /src/test/java/org/jetlinks/registry/redis/RedissonHelper.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.registry.redis; 2 | 3 | import lombok.SneakyThrows; 4 | import org.redisson.Redisson; 5 | import org.redisson.api.RMap; 6 | import org.redisson.api.RedissonClient; 7 | import org.redisson.config.Config; 8 | 9 | import java.util.concurrent.*; 10 | 11 | /** 12 | * @author zhouhao 13 | * @since 1.0.0 14 | */ 15 | public class RedissonHelper { 16 | 17 | public static RedissonClient newRedissonClient() { 18 | Config config = new Config(); 19 | config.useSingleServer() 20 | .setAddress(System.getProperty("redis.host", "redis://127.0.0.1:6379")) 21 | .setDatabase(5) 22 | .setTimeout(10000) 23 | .setRetryAttempts(1000) 24 | .setRetryInterval(100) 25 | .setConnectionPoolSize(32) 26 | .setConnectTimeout(10000); 27 | config.setThreads(32); 28 | config.setNettyThreads(32); 29 | 30 | return Redisson.create(config); 31 | } 32 | 33 | @SneakyThrows 34 | public static void main(String[] args) { 35 | RedissonClient client = newRedissonClient(); 36 | ExecutorService executorService = Executors.newFixedThreadPool(32); 37 | 38 | RMap map = client.getMap("test-map"); 39 | map.put("key1", "value1"); 40 | map.put("key2", "value2"); 41 | map.put("key3", "value3"); 42 | CountDownLatch latch = new CountDownLatch(1000); 43 | long startWith = System.currentTimeMillis(); 44 | 45 | for (int i = 0; i < 1000; i++) { 46 | CompletableFuture 47 | .supplyAsync(() -> map.get("key1"), executorService) 48 | .thenRun(latch::countDown); 49 | } 50 | System.out.println("executorService:" + (System.currentTimeMillis() - startWith) + "ms"); 51 | latch.await(); 52 | 53 | CountDownLatch latch2 = new CountDownLatch(1000); 54 | startWith = System.currentTimeMillis(); 55 | for (int i = 0; i < 1000; i++) { 56 | map.getAsync("key1") 57 | .thenRun(latch2::countDown); 58 | } 59 | latch2.await(); 60 | System.out.println("async:" + (System.currentTimeMillis() - startWith) + "ms"); 61 | executorService.shutdown(); 62 | client.shutdown(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/org/jetlinks/registry/redis/lettuce/LettuceDeviceOperationTest.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.registry.redis.lettuce; 2 | 3 | import lombok.SneakyThrows; 4 | import org.jetlinks.core.device.DeviceMessageSender; 5 | import org.jetlinks.core.device.DeviceOperation; 6 | import org.jetlinks.core.device.DeviceState; 7 | import org.jetlinks.core.enums.ErrorCode; 8 | import org.jetlinks.core.message.*; 9 | import org.jetlinks.core.message.exception.FunctionUndefinedException; 10 | import org.jetlinks.core.message.exception.IllegalParameterException; 11 | import org.jetlinks.core.message.exception.ParameterUndefinedException; 12 | import org.jetlinks.core.message.function.FunctionInvokeMessageReply; 13 | import org.jetlinks.core.message.interceptor.DeviceMessageSenderInterceptor; 14 | import org.jetlinks.core.message.property.ReadPropertyMessage; 15 | import org.jetlinks.core.message.property.ReadPropertyMessageReply; 16 | import org.jetlinks.lettuce.LettucePlus; 17 | import org.jetlinks.lettuce.supports.DefaultLettucePlus; 18 | import org.jetlinks.supports.official.JetLinksProtocolSupport; 19 | import org.junit.After; 20 | import org.junit.Assert; 21 | import org.junit.Before; 22 | import org.junit.Test; 23 | import org.springframework.core.io.ClassPathResource; 24 | import org.springframework.util.StreamUtils; 25 | 26 | import java.nio.charset.StandardCharsets; 27 | import java.util.concurrent.*; 28 | import java.util.concurrent.atomic.AtomicReference; 29 | 30 | import static io.vavr.API.Try; 31 | 32 | public class LettuceDeviceOperationTest { 33 | 34 | private LettucePlus client; 35 | 36 | public LettuceDeviceRegistry registry; 37 | 38 | @Before 39 | @SneakyThrows 40 | public void init() { 41 | client = DefaultLettucePlus.standalone(RedisClientHelper.createRedisClient()); 42 | 43 | JetLinksProtocolSupport jetLinksProtocolSupport = new JetLinksProtocolSupport(); 44 | 45 | registry = new LettuceDeviceRegistry(client, new LettuceDeviceMessageHandler(client), protocol -> jetLinksProtocolSupport); 46 | 47 | registry.addInterceptor(new DeviceMessageSenderInterceptor() { 48 | @Override 49 | public DeviceMessage preSend(DeviceOperation device, DeviceMessage message) { 50 | return message; 51 | } 52 | 53 | @Override 54 | public CompletionStage afterReply(DeviceOperation device, DeviceMessage message, R reply) { 55 | reply.addHeader("ts", System.currentTimeMillis()); 56 | return CompletableFuture.completedFuture(reply); 57 | } 58 | }); 59 | } 60 | 61 | @SneakyThrows 62 | @After 63 | public void cleanDb() { 64 | client.getConnection() 65 | .toCompletableFuture() 66 | .get() 67 | .sync() 68 | .flushdb(); 69 | client.shutdown(); 70 | } 71 | 72 | 73 | //设备网关服务宕机 74 | //场景: 设备网关服务宕机,未及时更新设备状态信息。继续往设备发消息时,会执行设备状态检查,更新状态 75 | @Test 76 | @SneakyThrows 77 | public void testServerOfflineCheckState() { 78 | DeviceOperation operation = registry.getDevice("test2"); 79 | 80 | try { 81 | //模拟上线 82 | operation.online("test2-server", "test"); 83 | 84 | Assert.assertEquals(operation.getState(), DeviceState.online); 85 | 86 | //模拟发送一条消息,该设备实际上并不在线。应该会自动执行状态检查 87 | FunctionInvokeMessageReply reply = operation.messageSender() 88 | .invokeFunction("test") 89 | .trySend(10, TimeUnit.SECONDS) 90 | .recover(TimeoutException.class, (__) -> FunctionInvokeMessageReply.create().error(ErrorCode.TIME_OUT)) 91 | .get(); 92 | 93 | Assert.assertFalse(reply.isSuccess()); 94 | 95 | //调用了设备状态检查并自动更新了设备状态 96 | Assert.assertEquals(operation.getState(), DeviceState.offline); 97 | Assert.assertNull(operation.getServerId()); 98 | } finally { 99 | registry.unRegistry("test2"); 100 | } 101 | 102 | } 103 | 104 | @Test 105 | public void testSendOfflineServer() { 106 | DeviceOperation operation = registry.getDevice("test2"); 107 | operation.online("test3-server", "test"); 108 | 109 | Assert.assertEquals(operation.getState(), DeviceState.online); 110 | 111 | Assert.assertEquals(operation.messageSender() 112 | .readProperty("test") 113 | .custom(Headers.async.setter()) 114 | .trySend(10, TimeUnit.SECONDS) 115 | .map(CommonDeviceMessageReply::getCode) 116 | .get(), ErrorCode.CLIENT_OFFLINE.name()); 117 | 118 | Assert.assertEquals(operation.getState(), DeviceState.offline); 119 | 120 | } 121 | 122 | //设备网关服务正常运行,但是设备未连接到当前网关服务 123 | //场景: 设备网关宕机,未及时将设备更新为离线。当网关服务重新启动后,设备其实已经没有连接到这台服务器了。 124 | @Test 125 | @SneakyThrows 126 | public void testServerOnlineNotConnectCheckState() { 127 | 128 | try { 129 | CountDownLatch latch = new CountDownLatch(1); 130 | 131 | 132 | DeviceOperation operation = registry.getDevice("test2"); 133 | //模拟上线 134 | operation.online("test2-server", "test"); 135 | 136 | Assert.assertEquals(operation.getState(), DeviceState.online); 137 | 138 | //消息处理器 139 | LettuceDeviceMessageHandler handler = new LettuceDeviceMessageHandler(client); 140 | 141 | handler.handleDeviceCheck("test2-server", deviceId -> { 142 | 143 | //模拟设备并没有连接到本服务器,修改设备状态离线. 144 | operation.offline(); 145 | latch.countDown(); 146 | 147 | }); 148 | 149 | //主动调用设备状态检查 150 | operation.checkState(); 151 | 152 | //调用了设备状态检查 153 | Assert.assertTrue(latch.await(10, TimeUnit.SECONDS)); 154 | Assert.assertEquals(operation.getState(), DeviceState.offline); 155 | } finally { 156 | registry.unRegistry("test2"); 157 | } 158 | } 159 | 160 | @Test 161 | @SneakyThrows 162 | public void testSendAndReplyMessage() { 163 | try { 164 | DeviceOperation operation = registry.getDevice("test2"); 165 | operation.online("test-server", "12"); 166 | DeviceMessageSender sender = operation.messageSender(); 167 | 168 | 169 | LettuceDeviceMessageHandler handler = new LettuceDeviceMessageHandler(client); 170 | 171 | handler.markMessageAsync("testId").toCompletableFuture().get(10, TimeUnit.SECONDS); 172 | Assert.assertTrue(handler.messageIsAsync("testId").toCompletableFuture().get(10, TimeUnit.SECONDS)); 173 | 174 | AtomicReference messageReference = new AtomicReference<>(); 175 | //处理发往设备的消息 176 | handler.handleMessage("test-server", message -> { 177 | messageReference.set(message); 178 | 179 | if (message instanceof RepayableDeviceMessage) { 180 | try { 181 | Thread.sleep(2000); 182 | } catch (Exception e) { 183 | 184 | } 185 | //模拟设备回复消息 186 | DeviceMessageReply reply = ((RepayableDeviceMessage) message).newReply(); 187 | reply.from(message); 188 | reply.error(ErrorCode.REQUEST_HANDLING); 189 | handler.reply(reply); 190 | } 191 | 192 | }); 193 | //发送消息s 194 | ReadPropertyMessageReply reply = sender.readProperty("test") 195 | .messageId("test-message") 196 | .send() 197 | .toCompletableFuture() 198 | .get(10, TimeUnit.SECONDS); 199 | Assert.assertNotNull(messageReference.get()); 200 | Assert.assertTrue(messageReference.get() instanceof ReadPropertyMessage); 201 | Assert.assertNotNull(reply); 202 | System.out.println(reply); 203 | 204 | messageReference.set(null); 205 | 206 | sender.readProperty("test") 207 | .messageId("test-retrieve-msg") 208 | .trySend(1, TimeUnit.MILLISECONDS); 209 | 210 | TimeUnit.SECONDS.sleep(5); 211 | ReadPropertyMessageReply retrieve = sender.readProperty("test") 212 | .messageId("test-retrieve-msg") 213 | .retrieveReply() 214 | .toCompletableFuture() 215 | .get(1, TimeUnit.SECONDS); 216 | Assert.assertNotNull(messageReference.get()); 217 | Assert.assertNotNull(retrieve); 218 | Assert.assertEquals(retrieve.getCode(), ErrorCode.REQUEST_HANDLING.name()); 219 | System.out.println(retrieve); 220 | 221 | } finally { 222 | registry.unRegistry("test2"); 223 | } 224 | 225 | } 226 | 227 | 228 | @Test 229 | @SneakyThrows 230 | public void testValidateParameter() { 231 | try { 232 | DeviceOperation operation = registry.getDevice("test3"); 233 | String metaData = StreamUtils.copyToString(new ClassPathResource("testValidateParameter.meta.json").getInputStream(), StandardCharsets.UTF_8); 234 | operation.updateMetadata(metaData); 235 | Thread.sleep(100); 236 | Assert.assertNotNull(operation.getMetadata()); 237 | 238 | //function未定义 239 | 240 | Assert.assertTrue(Try(() -> operation.messageSender().invokeFunction("getSysInfoUndefined").validate()) 241 | .map(r -> false) 242 | .recover(FunctionUndefinedException.class, true) 243 | .recover(err -> false) 244 | .get()); 245 | //参数错误 246 | Assert.assertTrue(Try(() -> operation.messageSender().invokeFunction("getSysInfo").validate()) 247 | .map(r -> false) 248 | .recover(IllegalArgumentException.class, true) 249 | .recover(err -> false) 250 | .get()); 251 | 252 | //参数未定义 253 | Assert.assertTrue(Try(() -> operation.messageSender() 254 | .invokeFunction("getSysInfo") 255 | .addParameter("test", "123") 256 | .validate()) 257 | .map(r -> false) 258 | .recover(ParameterUndefinedException.class, true) 259 | .recover(err -> false) 260 | .get()); 261 | 262 | //参数值类型错误 263 | Assert.assertTrue(Try(() -> operation.messageSender() 264 | .invokeFunction("getSysInfo") 265 | .addParameter("useCache", "2") 266 | .validate()) 267 | .map(r -> false) 268 | .recover(IllegalParameterException.class, true) 269 | .recover(err -> false) 270 | .get()); 271 | 272 | //通过 273 | operation.messageSender() 274 | .invokeFunction("getSysInfo") 275 | .addParameter("useCache", "1") 276 | .validate(); 277 | 278 | operation.messageSender() 279 | .invokeFunction("getSysInfo") 280 | .addParameter("useCache", "0") 281 | .validate(); 282 | } finally { 283 | registry.unRegistry("test3"); 284 | } 285 | } 286 | 287 | } -------------------------------------------------------------------------------- /src/test/java/org/jetlinks/registry/redis/lettuce/LettuceDeviceRegistryTest.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.registry.redis.lettuce; 2 | 3 | import io.vavr.control.Try; 4 | import lombok.SneakyThrows; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.jetlinks.core.device.*; 7 | import org.jetlinks.core.device.registry.DeviceMessageHandler; 8 | import org.jetlinks.core.message.CommonDeviceMessageReply; 9 | import org.jetlinks.core.message.DeviceMessage; 10 | import org.jetlinks.core.message.DeviceMessageReply; 11 | import org.jetlinks.core.message.interceptor.DeviceMessageSenderInterceptor; 12 | import org.jetlinks.lettuce.LettucePlus; 13 | import org.jetlinks.lettuce.supports.DefaultLettucePlus; 14 | import org.jetlinks.registry.redis.MockProtocolSupports; 15 | import org.junit.After; 16 | import org.junit.Assert; 17 | import org.junit.Before; 18 | import org.junit.Test; 19 | 20 | import java.util.*; 21 | import java.util.concurrent.*; 22 | 23 | 24 | /** 25 | * @author zhouhao 26 | * @since 1.0.0 27 | */ 28 | @Slf4j 29 | public class LettuceDeviceRegistryTest { 30 | 31 | private LettuceDeviceRegistry registry; 32 | 33 | private DeviceMessageHandler messageHandler; 34 | private LettucePlus client; 35 | 36 | @Before 37 | public void init() { 38 | client = DefaultLettucePlus.standalone(RedisClientHelper.createRedisClient()); 39 | 40 | messageHandler = new LettuceDeviceMessageHandler(client); 41 | 42 | registry = new LettuceDeviceRegistry(client, messageHandler, new MockProtocolSupports()); 43 | 44 | registry.addInterceptor(new DeviceMessageSenderInterceptor() { 45 | @Override 46 | public DeviceMessage preSend(DeviceOperation device, DeviceMessage message) { 47 | return message; 48 | } 49 | 50 | @Override 51 | public CompletionStage afterReply(DeviceOperation device, DeviceMessage message, R reply) { 52 | return CompletableFuture.supplyAsync(() -> { 53 | log.debug("reply Interceptor :{}", reply); 54 | return reply; 55 | }); 56 | } 57 | }); 58 | } 59 | 60 | @SneakyThrows 61 | @After 62 | public void cleanDb() { 63 | client.getConnection() 64 | .toCompletableFuture() 65 | .get() 66 | .sync() 67 | .flushdb(); 68 | client.shutdown(); 69 | } 70 | 71 | 72 | public DeviceInfo newDeviceInfo() { 73 | DeviceInfo deviceInfo = new DeviceInfo(); 74 | deviceInfo.setId(UUID.randomUUID().toString()); 75 | deviceInfo.setCreatorId("admin"); 76 | deviceInfo.setCreatorName("admin"); 77 | deviceInfo.setProjectId("p001"); 78 | deviceInfo.setProjectName("项目1"); 79 | deviceInfo.setProtocol("jet-links"); 80 | deviceInfo.setType((byte) 0); 81 | return deviceInfo; 82 | } 83 | 84 | @Test 85 | @SneakyThrows 86 | public void testConfig() { 87 | DeviceInfo info = newDeviceInfo(); 88 | DeviceProductInfo productInfo = new DeviceProductInfo(); 89 | productInfo.setId("test"); 90 | productInfo.setName("测试"); 91 | productInfo.setProjectId("test"); 92 | productInfo.setProtocol("jet-links"); 93 | info.setProductId(productInfo.getId()); 94 | info.setProductName(productInfo.getName()); 95 | 96 | DeviceProductOperation productOperation = registry.getProduct(productInfo.getId()); 97 | productOperation.update(productInfo); 98 | productOperation.put("test_config", "1234"); 99 | productOperation.put("test_config__", "aaa"); 100 | try { 101 | Assert.assertNotNull(productOperation.getProtocol()); 102 | 103 | DeviceOperation operation = registry.registry(info); 104 | 105 | Assert.assertNotNull(operation.getProtocol()); 106 | 107 | Assert.assertEquals(operation.get("test_config").asString().orElse(null), "1234"); 108 | 109 | Map conf = operation.getAll("test_config"); 110 | System.out.println(conf); 111 | Assert.assertEquals(conf.get("test_config"), "1234"); 112 | 113 | operation.put("test_config", "2345"); 114 | operation.put("test_config2", 1234); 115 | 116 | Assert.assertEquals(operation.get("test_config").asString().orElse(null), "2345"); 117 | conf = operation.getAll("test_config", "test_config__", "test_config2"); 118 | System.out.println(conf); 119 | Assert.assertEquals(conf.get("test_config"), "2345"); 120 | Assert.assertEquals(conf.get("test_config2"), 1234); 121 | Assert.assertEquals(conf.get("test_config__"), "aaa"); 122 | 123 | Map all = operation.getAll(); 124 | Assert.assertEquals(all.get("test_config"), "2345"); 125 | Assert.assertEquals(all.get("test_config2"), 1234); 126 | Assert.assertEquals(all.get("test_config__"), "aaa"); 127 | System.out.println(all); 128 | 129 | Assert.assertEquals(operation.remove("test_config"), "2345"); 130 | Assert.assertTrue(operation.get("test_config").isPresent()); 131 | 132 | operation.putAll(all); 133 | Assert.assertEquals(all.get("test_config"), "2345"); 134 | 135 | operation.putAll(null); 136 | operation.putAll(Collections.emptyMap()); 137 | 138 | Assert.assertFalse(Try.of(() -> { 139 | 140 | operation.put("test", null); 141 | 142 | return true; 143 | }).recover(NullPointerException.class, false).get()); 144 | 145 | 146 | } finally { 147 | registry.unRegistry(info.getId()); 148 | } 149 | } 150 | 151 | @Test 152 | @SneakyThrows 153 | public void testBenchmark() { 154 | DeviceInfo info = newDeviceInfo(); 155 | DeviceProductInfo productInfo = new DeviceProductInfo(); 156 | productInfo.setId("test2"); 157 | productInfo.setName("测试"); 158 | productInfo.setProjectId("test"); 159 | productInfo.setProtocol("jet-links"); 160 | info.setProductId(productInfo.getId()); 161 | 162 | registry.registry(info); 163 | 164 | registry.getProduct(productInfo.getId()).update(productInfo); 165 | 166 | registry.getProduct(productInfo.getId()).put("test", "1234"); 167 | Thread.sleep(100); 168 | 169 | long time = System.currentTimeMillis(); 170 | 171 | CountDownLatch latch = new CountDownLatch(1000); 172 | ExecutorService executorService = Executors.newFixedThreadPool(32); 173 | for (int i = 0; i < 1000; i++) { 174 | int fi = i; 175 | CompletableFuture.runAsync(() -> { 176 | try { 177 | registry.getDevice(info.getId()).put("test", 123); 178 | registry.getDevice(info.getId()).get("test:" + fi); 179 | } catch (Exception e) { 180 | e.printStackTrace(); 181 | } finally { 182 | latch.countDown(); 183 | } 184 | }, executorService); 185 | } 186 | latch.await(30, TimeUnit.SECONDS); 187 | executorService.shutdown(); 188 | System.out.println(System.currentTimeMillis() - time); 189 | } 190 | 191 | @Test 192 | @SneakyThrows 193 | public void testRegistry() { 194 | DeviceInfo info = newDeviceInfo(); 195 | try { 196 | DeviceOperation operation = registry.registry(info); 197 | Assert.assertNotNull(operation); 198 | Assert.assertEquals(operation.getState(), DeviceState.offline); 199 | operation.online("server-01", "session-01"); 200 | Assert.assertEquals(operation.getState(), DeviceState.online); 201 | Assert.assertEquals(operation.getServerId(), "server-01"); 202 | Assert.assertEquals(operation.getSessionId(), "session-01"); 203 | Assert.assertTrue(operation.isOnline()); 204 | operation.offline(); 205 | Assert.assertFalse(operation.isOnline()); 206 | Assert.assertNull(operation.getServerId()); 207 | Assert.assertNull(operation.getSessionId()); 208 | } finally { 209 | registry.unRegistry(info.getId()); 210 | Thread.sleep(500); 211 | DeviceOperation operation = registry.getDevice(info.getId()); 212 | Assert.assertEquals(operation.getState(), DeviceState.unknown); 213 | } 214 | } 215 | 216 | @Test 217 | public void benchmarkTest() { 218 | int size = 1000; 219 | 220 | List operations = new ArrayList<>(size); 221 | long time = System.currentTimeMillis(); 222 | System.out.println("RAM:" + Runtime.getRuntime().totalMemory() / 1024 / 1024 + "MB"); 223 | for (int i = 0; i < size; i++) { 224 | DeviceInfo info = newDeviceInfo(); 225 | DeviceOperation operation = registry.registry(info); 226 | operations.add(operation); 227 | } 228 | long completeTime = System.currentTimeMillis(); 229 | log.debug("registry {} devices use {}ms", size, completeTime - time); 230 | 231 | time = System.currentTimeMillis(); 232 | for (DeviceOperation operation : operations) { 233 | operation.authenticate(new AuthenticationRequest() { 234 | }); 235 | operation.online("server_01", "session_0"); 236 | operation.getDeviceInfo(); 237 | } 238 | completeTime = System.currentTimeMillis(); 239 | log.debug("online {} devices use {}ms", size, completeTime - time); 240 | System.out.println("RAM:" + Runtime.getRuntime().totalMemory() / 1024 / 1024 + "MB"); 241 | for (DeviceOperation operation : operations) { 242 | registry.unRegistry(operation.getDeviceInfo().getId()); 243 | } 244 | } 245 | 246 | @Test 247 | @SneakyThrows 248 | public void testSendMessage() { 249 | DeviceInfo device = newDeviceInfo(); 250 | device.setId("test"); 251 | //注册 252 | DeviceOperation operation = registry.registry(device); 253 | 254 | //上线 255 | operation.online("test", "test"); 256 | 257 | messageHandler.handleMessage("test", msg -> { 258 | CommonDeviceMessageReply reply = msg.toJson().toJavaObject(CommonDeviceMessageReply.class); 259 | reply.setSuccess(true); 260 | reply.setMessage("成功"); 261 | messageHandler.reply(reply); 262 | }); 263 | 264 | CommonDeviceMessageReply reply = operation.messageSender() 265 | .invokeFunction("test") 266 | .send() 267 | .toCompletableFuture() 268 | .get(1, TimeUnit.SECONDS); 269 | Assert.assertNotNull(reply); 270 | Assert.assertTrue(reply.isSuccess()); 271 | 272 | long time = System.currentTimeMillis(); 273 | 274 | long len = 100; 275 | for (int i = 0; i < len; i++) { 276 | reply = operation.messageSender() 277 | .invokeFunction("test") 278 | .send() 279 | .toCompletableFuture() 280 | .get(5, TimeUnit.SECONDS); 281 | Assert.assertNotNull(reply); 282 | Assert.assertTrue(reply.isSuccess()); 283 | } 284 | System.out.println("执行" + len + "次消息收发,耗时:" + (System.currentTimeMillis() - time) + "ms"); 285 | registry.unRegistry(device.getId()); 286 | } 287 | 288 | } -------------------------------------------------------------------------------- /src/test/java/org/jetlinks/registry/redis/lettuce/RedisClientHelper.java: -------------------------------------------------------------------------------- 1 | package org.jetlinks.registry.redis.lettuce; 2 | 3 | import io.lettuce.core.RedisClient; 4 | import io.lettuce.core.RedisURI; 5 | 6 | public class RedisClientHelper { 7 | 8 | 9 | public static RedisClient createRedisClient() { 10 | RedisURI uri = RedisURI.create(System.getProperty("redis.host", "redis://127.0.0.1:6379")); 11 | uri.setDatabase(8); 12 | 13 | return RedisClient.create(uri); 14 | } 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |           12 | %-4relative [%thread] %-5level %logger{35} - %msg %n 13 |       14 | 15 | 16 | 17 | 18 |   19 | 20 | 21 | -------------------------------------------------------------------------------- /src/test/resources/testValidateParameter.meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "test", 3 | "name": "测试", 4 | "functions": [ 5 | { 6 | "id": "getSysInfo", 7 | "name": "获取系统信息", 8 | "inputs": [ 9 | { 10 | "id": "useCache", 11 | "name": "是否使用缓存", 12 | "valueType": { 13 | "type": "boolean", 14 | "trueValue": 1, 15 | "falseValue": 0, 16 | "trueText": "开启", 17 | "falseText": "关闭" 18 | } 19 | } 20 | ] 21 | } 22 | ] 23 | } --------------------------------------------------------------------------------