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 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  
... ...