Commit 842bbb55a2db85d805a05a71b4191c9f97fb4eee
1 parent
37760883
feat(iot): 支持设备网关与业务服务合并部署
- 网关提供普通依赖包,保留 exec 独立启动包 - 区分网关与业务 Bean,按配置启用远程设备 API - 修复本地消息总线并发注册并补充回归用例
Showing
7 changed files
with
91 additions
and
8 deletions
yudao-module-iot/yudao-module-iot-biz/pom.xml
| ... | ... | @@ -31,6 +31,12 @@ |
| 31 | 31 | <artifactId>yudao-module-iot-core</artifactId> |
| 32 | 32 | <version>${revision}</version> |
| 33 | 33 | </dependency> |
| 34 | + <!-- 可选:与 biz 合并部署时引入 gateway,并将网关配置合并至 yudao-server 的 application.yaml,消息总线配置为 local。参见 https://t.zsxq.com/FtbO6 --> | |
| 35 | +<!-- <dependency>--> | |
| 36 | +<!-- <groupId>cn.iocoder.boot</groupId>--> | |
| 37 | +<!-- <artifactId>yudao-module-iot-gateway</artifactId>--> | |
| 38 | +<!-- <version>${revision}</version>--> | |
| 39 | +<!-- </dependency>--> | |
| 34 | 40 | |
| 35 | 41 | <dependency> |
| 36 | 42 | <groupId>cn.iocoder.boot</groupId> | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/local/IotLocalMessageBus.java
| ... | ... | @@ -8,10 +8,10 @@ import lombok.extern.slf4j.Slf4j; |
| 8 | 8 | import org.springframework.context.ApplicationContext; |
| 9 | 9 | import org.springframework.context.event.EventListener; |
| 10 | 10 | |
| 11 | -import java.util.ArrayList; | |
| 12 | -import java.util.HashMap; | |
| 13 | 11 | import java.util.List; |
| 14 | 12 | import java.util.Map; |
| 13 | +import java.util.concurrent.ConcurrentHashMap; | |
| 14 | +import java.util.concurrent.CopyOnWriteArrayList; | |
| 15 | 15 | |
| 16 | 16 | /** |
| 17 | 17 | * 本地的 {@link IotMessageBus} 实现类 |
| ... | ... | @@ -30,7 +30,7 @@ public class IotLocalMessageBus implements IotMessageBus { |
| 30 | 30 | * 订阅者映射表 |
| 31 | 31 | * Key: topic |
| 32 | 32 | */ |
| 33 | - private final Map<String, List<IotMessageSubscriber<?>>> subscribers = new HashMap<>(); | |
| 33 | + private final Map<String, List<IotMessageSubscriber<?>>> subscribers = new ConcurrentHashMap<>(); | |
| 34 | 34 | |
| 35 | 35 | @Override |
| 36 | 36 | public void post(String topic, Object message) { |
| ... | ... | @@ -40,7 +40,7 @@ public class IotLocalMessageBus implements IotMessageBus { |
| 40 | 40 | @Override |
| 41 | 41 | public void register(IotMessageSubscriber<?> subscriber) { |
| 42 | 42 | String topic = subscriber.getTopic(); |
| 43 | - List<IotMessageSubscriber<?>> topicSubscribers = subscribers.computeIfAbsent(topic, k -> new ArrayList<>()); | |
| 43 | + List<IotMessageSubscriber<?>> topicSubscribers = subscribers.computeIfAbsent(topic, k -> new CopyOnWriteArrayList<>()); | |
| 44 | 44 | topicSubscribers.add(subscriber); |
| 45 | 45 | log.info("[register][topic({}/{}) 注册消费者({})成功]", |
| 46 | 46 | topic, subscriber.getGroup(), subscriber.getClass().getName()); | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/messagebus/core/local/LocalIotMessageBusIntegrationTest.java
| ... | ... | @@ -10,7 +10,12 @@ import org.springframework.boot.test.context.SpringBootTest; |
| 10 | 10 | import org.springframework.context.annotation.Import; |
| 11 | 11 | import org.springframework.test.context.TestPropertySource; |
| 12 | 12 | |
| 13 | +import java.util.ArrayList; | |
| 14 | +import java.util.List; | |
| 13 | 15 | import java.util.concurrent.CountDownLatch; |
| 16 | +import java.util.concurrent.ExecutorService; | |
| 17 | +import java.util.concurrent.Executors; | |
| 18 | +import java.util.concurrent.Future; | |
| 14 | 19 | import java.util.concurrent.TimeUnit; |
| 15 | 20 | import java.util.concurrent.atomic.AtomicInteger; |
| 16 | 21 | |
| ... | ... | @@ -174,4 +179,69 @@ public class LocalIotMessageBusIntegrationTest { |
| 174 | 179 | log.info("[测试] 多主题测试完成"); |
| 175 | 180 | } |
| 176 | 181 | |
| 182 | + /** | |
| 183 | + * 1 topic 16 subscriber 并发注册 | |
| 184 | + */ | |
| 185 | + @Test | |
| 186 | + public void testConcurrentRegister() throws Exception { | |
| 187 | + // 准备参数 | |
| 188 | + String topic = "concurrent-register-topic"; | |
| 189 | + String testMessage = "concurrent"; | |
| 190 | + int threadCount = 16; | |
| 191 | + List<AtomicInteger> receivedCounts = new ArrayList<>(threadCount); | |
| 192 | + List<IotMessageSubscriber<String>> subscribers = new ArrayList<>(threadCount); | |
| 193 | + for (int i = 0; i < threadCount; i++) { | |
| 194 | + AtomicInteger receivedCount = new AtomicInteger(0); | |
| 195 | + String group = "concurrent-group-" + i; | |
| 196 | + receivedCounts.add(receivedCount); | |
| 197 | + subscribers.add(new IotMessageSubscriber<>() { | |
| 198 | + | |
| 199 | + @Override | |
| 200 | + public String getTopic() { | |
| 201 | + return topic; | |
| 202 | + } | |
| 203 | + | |
| 204 | + @Override | |
| 205 | + public String getGroup() { | |
| 206 | + return group; | |
| 207 | + } | |
| 208 | + | |
| 209 | + @Override | |
| 210 | + public void onMessage(String message) { | |
| 211 | + assertEquals(testMessage, message); | |
| 212 | + receivedCount.incrementAndGet(); | |
| 213 | + } | |
| 214 | + | |
| 215 | + }); | |
| 216 | + } | |
| 217 | + | |
| 218 | + // 调用:16 个线程同时注册 | |
| 219 | + ExecutorService executor = Executors.newFixedThreadPool(threadCount); | |
| 220 | + try { | |
| 221 | + CountDownLatch startLatch = new CountDownLatch(1); | |
| 222 | + List<Future<?>> futures = new ArrayList<>(threadCount); | |
| 223 | + for (IotMessageSubscriber<String> subscriber : subscribers) { | |
| 224 | + futures.add(executor.submit(() -> { | |
| 225 | + startLatch.await(); | |
| 226 | + messageBus.register(subscriber); | |
| 227 | + return null; | |
| 228 | + })); | |
| 229 | + } | |
| 230 | + startLatch.countDown(); | |
| 231 | + // 注册时抛出的异常(例如 ConcurrentModificationException),会在 get 时抛出 | |
| 232 | + for (Future<?> future : futures) { | |
| 233 | + future.get(10, TimeUnit.SECONDS); | |
| 234 | + } | |
| 235 | + } finally { | |
| 236 | + executor.shutdownNow(); | |
| 237 | + } | |
| 238 | + // 调用:注册完成后,发送一次消息(本地总线同步投递) | |
| 239 | + messageBus.post(topic, testMessage); | |
| 240 | + | |
| 241 | + // 断言:每个订阅者都收到 1 条消息,没有因并发注册而丢失 | |
| 242 | + for (int i = 0; i < threadCount; i++) { | |
| 243 | + assertEquals(1, receivedCounts.get(i).get(), "订阅者 " + i + " 应该收到 1 条消息"); | |
| 244 | + } | |
| 245 | + } | |
| 246 | + | |
| 177 | 247 | } |
| 178 | 248 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/pom.xml
| ... | ... | @@ -77,7 +77,7 @@ |
| 77 | 77 | <!-- 设置构建的 jar 包名 --> |
| 78 | 78 | <finalName>${project.artifactId}</finalName> |
| 79 | 79 | <plugins> |
| 80 | - <!-- 打包 --> | |
| 80 | + <!-- 打包:普通 jar 供 biz 引入,-exec.jar 独立运行 --> | |
| 81 | 81 | <plugin> |
| 82 | 82 | <groupId>org.springframework.boot</groupId> |
| 83 | 83 | <artifactId>spring-boot-maven-plugin</artifactId> |
| ... | ... | @@ -87,6 +87,10 @@ |
| 87 | 87 | <goals> |
| 88 | 88 | <goal>repackage</goal> <!-- 将引入的 jar 打入其中 --> |
| 89 | 89 | </goals> |
| 90 | + <configuration> | |
| 91 | + <!-- 可执行 jar 使用 exec 分类,主 artifact 保留为普通可依赖 jar --> | |
| 92 | + <classifier>exec</classifier> | |
| 93 | + </configuration> | |
| 90 | 94 | </execution> |
| 91 | 95 | </executions> |
| 92 | 96 | </plugin> | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/IotDeviceServiceImpl.java
| ... | ... | @@ -21,7 +21,7 @@ import static cn.iocoder.yudao.framework.common.util.cache.CacheUtils.buildAsync |
| 21 | 21 | * |
| 22 | 22 | * @author 芋道源码 |
| 23 | 23 | */ |
| 24 | -@Service | |
| 24 | +@Service("iotGatewayDeviceServiceImpl") // 显式 bean 名:避免与 biz 的 IotDeviceServiceImpl 同名冲突 | |
| 25 | 25 | @Slf4j |
| 26 | 26 | public class IotDeviceServiceImpl implements IotDeviceService { |
| 27 | 27 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java
| ... | ... | @@ -43,7 +43,7 @@ import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.*; |
| 43 | 43 | * |
| 44 | 44 | * @author 芋道源码 |
| 45 | 45 | */ |
| 46 | -@Service | |
| 46 | +@Service("iotGatewayDeviceMessageServiceImpl") // 显式 bean 名:避免与 biz 的 IotDeviceMessageServiceImpl 同名冲突 | |
| 47 | 47 | @Slf4j |
| 48 | 48 | public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { |
| 49 | 49 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java
| ... | ... | @@ -15,10 +15,10 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; |
| 15 | 15 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; |
| 16 | 16 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; |
| 17 | 17 | |
| 18 | -import java.util.List; | |
| 19 | 18 | import jakarta.annotation.PostConstruct; |
| 20 | 19 | import jakarta.annotation.Resource; |
| 21 | 20 | import lombok.extern.slf4j.Slf4j; |
| 21 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | |
| 22 | 22 | import org.springframework.boot.web.client.RestTemplateBuilder; |
| 23 | 23 | import org.springframework.core.ParameterizedTypeReference; |
| 24 | 24 | import org.springframework.http.HttpEntity; |
| ... | ... | @@ -35,9 +35,12 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC |
| 35 | 35 | /** |
| 36 | 36 | * Iot 设备信息 Service 实现类:调用远程的 device http 接口,进行设备认证、设备获取等 |
| 37 | 37 | * |
| 38 | + * 注意:仅配置 yudao.iot.gateway.rpc.url 时创建(独立部署);合并部署时,使用 biz 的 IotDeviceCommonApi 实现 | |
| 39 | + * | |
| 38 | 40 | * @author 芋道源码 |
| 39 | 41 | */ |
| 40 | 42 | @Service |
| 43 | +@ConditionalOnProperty(prefix = "yudao.iot.gateway.rpc", name = "url") | |
| 41 | 44 | @Slf4j |
| 42 | 45 | public class IotDeviceApiImpl implements IotDeviceCommonApi { |
| 43 | 46 | ... | ... |