You need to sign in before continuing.

Commit 842bbb55a2db85d805a05a71b4191c9f97fb4eee

Authored by YunaiV
1 parent 37760883

feat(iot): 支持设备网关与业务服务合并部署

- 网关提供普通依赖包,保留 exec 独立启动包
- 区分网关与业务 Bean,按配置启用远程设备 API
- 修复本地消息总线并发注册并补充回归用例
yudao-module-iot/yudao-module-iot-biz/pom.xml
@@ -31,6 +31,12 @@ @@ -31,6 +31,12 @@
31 <artifactId>yudao-module-iot-core</artifactId> 31 <artifactId>yudao-module-iot-core</artifactId>
32 <version>${revision}</version> 32 <version>${revision}</version>
33 </dependency> 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 <dependency> 41 <dependency>
36 <groupId>cn.iocoder.boot</groupId> 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,10 +8,10 @@ import lombok.extern.slf4j.Slf4j;
8 import org.springframework.context.ApplicationContext; 8 import org.springframework.context.ApplicationContext;
9 import org.springframework.context.event.EventListener; 9 import org.springframework.context.event.EventListener;
10 10
11 -import java.util.ArrayList;  
12 -import java.util.HashMap;  
13 import java.util.List; 11 import java.util.List;
14 import java.util.Map; 12 import java.util.Map;
  13 +import java.util.concurrent.ConcurrentHashMap;
  14 +import java.util.concurrent.CopyOnWriteArrayList;
15 15
16 /** 16 /**
17 * 本地的 {@link IotMessageBus} 实现类 17 * 本地的 {@link IotMessageBus} 实现类
@@ -30,7 +30,7 @@ public class IotLocalMessageBus implements IotMessageBus { @@ -30,7 +30,7 @@ public class IotLocalMessageBus implements IotMessageBus {
30 * 订阅者映射表 30 * 订阅者映射表
31 * Key: topic 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 @Override 35 @Override
36 public void post(String topic, Object message) { 36 public void post(String topic, Object message) {
@@ -40,7 +40,7 @@ public class IotLocalMessageBus implements IotMessageBus { @@ -40,7 +40,7 @@ public class IotLocalMessageBus implements IotMessageBus {
40 @Override 40 @Override
41 public void register(IotMessageSubscriber<?> subscriber) { 41 public void register(IotMessageSubscriber<?> subscriber) {
42 String topic = subscriber.getTopic(); 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 topicSubscribers.add(subscriber); 44 topicSubscribers.add(subscriber);
45 log.info("[register][topic({}/{}) 注册消费者({})成功]", 45 log.info("[register][topic({}/{}) 注册消费者({})成功]",
46 topic, subscriber.getGroup(), subscriber.getClass().getName()); 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,7 +10,12 @@ import org.springframework.boot.test.context.SpringBootTest;
10 import org.springframework.context.annotation.Import; 10 import org.springframework.context.annotation.Import;
11 import org.springframework.test.context.TestPropertySource; 11 import org.springframework.test.context.TestPropertySource;
12 12
  13 +import java.util.ArrayList;
  14 +import java.util.List;
13 import java.util.concurrent.CountDownLatch; 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 import java.util.concurrent.TimeUnit; 19 import java.util.concurrent.TimeUnit;
15 import java.util.concurrent.atomic.AtomicInteger; 20 import java.util.concurrent.atomic.AtomicInteger;
16 21
@@ -174,4 +179,69 @@ public class LocalIotMessageBusIntegrationTest { @@ -174,4 +179,69 @@ public class LocalIotMessageBusIntegrationTest {
174 log.info("[测试] 多主题测试完成"); 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 \ No newline at end of file 248 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/pom.xml
@@ -77,7 +77,7 @@ @@ -77,7 +77,7 @@
77 <!-- 设置构建的 jar 包名 --> 77 <!-- 设置构建的 jar 包名 -->
78 <finalName>${project.artifactId}</finalName> 78 <finalName>${project.artifactId}</finalName>
79 <plugins> 79 <plugins>
80 - <!-- 打包 --> 80 + <!-- 打包:普通 jar 供 biz 引入,-exec.jar 独立运行 -->
81 <plugin> 81 <plugin>
82 <groupId>org.springframework.boot</groupId> 82 <groupId>org.springframework.boot</groupId>
83 <artifactId>spring-boot-maven-plugin</artifactId> 83 <artifactId>spring-boot-maven-plugin</artifactId>
@@ -87,6 +87,10 @@ @@ -87,6 +87,10 @@
87 <goals> 87 <goals>
88 <goal>repackage</goal> <!-- 将引入的 jar 打入其中 --> 88 <goal>repackage</goal> <!-- 将引入的 jar 打入其中 -->
89 </goals> 89 </goals>
  90 + <configuration>
  91 + <!-- 可执行 jar 使用 exec 分类,主 artifact 保留为普通可依赖 jar -->
  92 + <classifier>exec</classifier>
  93 + </configuration>
90 </execution> 94 </execution>
91 </executions> 95 </executions>
92 </plugin> 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,7 +21,7 @@ import static cn.iocoder.yudao.framework.common.util.cache.CacheUtils.buildAsync
21 * 21 *
22 * @author 芋道源码 22 * @author 芋道源码
23 */ 23 */
24 -@Service 24 +@Service("iotGatewayDeviceServiceImpl") // 显式 bean 名:避免与 biz 的 IotDeviceServiceImpl 同名冲突
25 @Slf4j 25 @Slf4j
26 public class IotDeviceServiceImpl implements IotDeviceService { 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,7 +43,7 @@ import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.*;
43 * 43 *
44 * @author 芋道源码 44 * @author 芋道源码
45 */ 45 */
46 -@Service 46 +@Service("iotGatewayDeviceMessageServiceImpl") // 显式 bean 名:避免与 biz 的 IotDeviceMessageServiceImpl 同名冲突
47 @Slf4j 47 @Slf4j
48 public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { 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,10 +15,10 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
15 import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; 15 import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
16 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; 16 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
17 17
18 -import java.util.List;  
19 import jakarta.annotation.PostConstruct; 18 import jakarta.annotation.PostConstruct;
20 import jakarta.annotation.Resource; 19 import jakarta.annotation.Resource;
21 import lombok.extern.slf4j.Slf4j; 20 import lombok.extern.slf4j.Slf4j;
  21 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
22 import org.springframework.boot.web.client.RestTemplateBuilder; 22 import org.springframework.boot.web.client.RestTemplateBuilder;
23 import org.springframework.core.ParameterizedTypeReference; 23 import org.springframework.core.ParameterizedTypeReference;
24 import org.springframework.http.HttpEntity; 24 import org.springframework.http.HttpEntity;
@@ -35,9 +35,12 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC @@ -35,9 +35,12 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC
35 /** 35 /**
36 * Iot 设备信息 Service 实现类:调用远程的 device http 接口,进行设备认证、设备获取等 36 * Iot 设备信息 Service 实现类:调用远程的 device http 接口,进行设备认证、设备获取等
37 * 37 *
  38 + * 注意:仅配置 yudao.iot.gateway.rpc.url 时创建(独立部署);合并部署时,使用 biz 的 IotDeviceCommonApi 实现
  39 + *
38 * @author 芋道源码 40 * @author 芋道源码
39 */ 41 */
40 @Service 42 @Service
  43 +@ConditionalOnProperty(prefix = "yudao.iot.gateway.rpc", name = "url")
41 @Slf4j 44 @Slf4j
42 public class IotDeviceApiImpl implements IotDeviceCommonApi { 45 public class IotDeviceApiImpl implements IotDeviceCommonApi {
43 46