Commit 70a0df54e474b9f8264f92178f5ca7a79fa61482

Authored by YunaiV
1 parent 39fb31d4

feat:【IoT 物联网】增加消息总线(messagebus)的 rocketmq 实现

Showing 11 changed files with 514 additions and 129 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
... ... @@ -99,6 +99,18 @@ public class JsonUtils {
99 99 }
100 100 }
101 101  
  102 + public static <T> T parseObject(byte[] text, Type type) {
  103 + if (ArrayUtil.isEmpty(text)) {
  104 + return null;
  105 + }
  106 + try {
  107 + return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type));
  108 + } catch (IOException e) {
  109 + log.error("json parse err,json:{}", text, e);
  110 + throw new RuntimeException(e);
  111 + }
  112 + }
  113 +
102 114 /**
103 115 * 将字符串解析成指定类型的对象
104 116 * 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下,
... ...
yudao-module-iot/yudao-module-iot-core/yudao-module-iot-message-bus/src/main/java/cn/iocoder/yudao/module/iot/messagebus/config/IotMessageBusAutoConfiguration.java
... ... @@ -2,8 +2,12 @@ package cn.iocoder.yudao.module.iot.messagebus.config;
2 2  
3 3 import cn.iocoder.yudao.module.iot.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.messagebus.core.local.LocalIotMessageBus;
  5 +import cn.iocoder.yudao.module.iot.messagebus.core.rocketmq.RocketMQIotMessageBus;
5 6 import lombok.extern.slf4j.Slf4j;
  7 +import org.apache.rocketmq.spring.autoconfigure.RocketMQProperties;
  8 +import org.apache.rocketmq.spring.core.RocketMQTemplate;
6 9 import org.springframework.boot.autoconfigure.AutoConfiguration;
  10 +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
7 11 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
8 12 import org.springframework.boot.context.properties.EnableConfigurationProperties;
9 13 import org.springframework.context.ApplicationContext;
... ... @@ -34,4 +38,19 @@ public class IotMessageBusAutoConfiguration {
34 38  
35 39 }
36 40  
  41 + // ==================== RocketMQ 实现 ====================
  42 +
  43 + @Configuration
  44 + @ConditionalOnProperty(prefix = "yudao.iot.message-bus", name = "type", havingValue = "rocketmq")
  45 + @ConditionalOnClass(RocketMQTemplate.class)
  46 + public static class RocketMQIotMessageBusConfiguration {
  47 +
  48 + @Bean
  49 + public IotMessageBus rocketMQIotMessageBus(RocketMQProperties rocketMQProperties, RocketMQTemplate rocketMQTemplate) {
  50 + log.info("[rocketMQIotMessageBus][创建 RocketMQ IoT 消息总线]");
  51 + return new RocketMQIotMessageBus(rocketMQProperties, rocketMQTemplate);
  52 + }
  53 +
  54 + }
  55 +
37 56 }
38 57 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-core/yudao-module-iot-message-bus/src/main/java/cn/iocoder/yudao/module/iot/messagebus/core/AbstractIotMessageBus.java deleted
1   -package cn.iocoder.yudao.module.iot.messagebus.core;
2   -
3   -import cn.hutool.core.collection.CollUtil;
4   -import lombok.extern.slf4j.Slf4j;
5   -
6   -import java.util.Comparator;
7   -import java.util.List;
8   -import java.util.Map;
9   -import java.util.concurrent.ConcurrentHashMap;
10   -import java.util.concurrent.CopyOnWriteArrayList;
11   -
12   -/**
13   - * IoT 消息总线抽象基类
14   - *
15   - * 提供通用的订阅者管理功能
16   - *
17   - * @author 芋道源码
18   - */
19   -@Slf4j
20   -public abstract class AbstractIotMessageBus implements IotMessageBus {
21   -
22   - /**
23   - * 订阅者映射表
24   - * Key: topic
25   - */
26   - private final Map<String, List<IotMessageBusSubscriber<?>>> subscribers = new ConcurrentHashMap<>();
27   -
28   - @Override
29   - public void register(String topic, IotMessageBusSubscriber<?> subscriber) {
30   - // 执行注册
31   - doRegister(topic, subscriber);
32   -
33   - // 添加订阅者映射
34   - List<IotMessageBusSubscriber<?>> topicSubscribers = subscribers.computeIfAbsent(topic, k -> new CopyOnWriteArrayList<>());
35   - topicSubscribers.add(subscriber);
36   - topicSubscribers.sort(Comparator.comparingInt(IotMessageBusSubscriber::order));
37   - log.info("[register][topic({}) 注册订阅者({})成功]", topic, subscriber.getClass().getName());
38   - }
39   -
40   - /**
41   - * 注册订阅者
42   - *
43   - * @param topic 主题
44   - * @param subscriber 订阅者
45   - */
46   - protected abstract void doRegister(String topic, IotMessageBusSubscriber<?> subscriber);
47   -
48   - /**
49   - * 通知订阅者
50   - *
51   - * @param message 消息
52   - */
53   - @SuppressWarnings({"rawtypes", "unchecked"})
54   - protected void notifySubscribers(String topic, Object message) {
55   - List<IotMessageBusSubscriber<?>> topicSubscribers = subscribers.get(topic);
56   - if (CollUtil.isEmpty(topicSubscribers)) {
57   - return;
58   - }
59   - for (IotMessageBusSubscriber subscriber : topicSubscribers) {
60   - try {
61   - subscriber.onMessage(topic, message);
62   - } catch (Exception e) {
63   - log.error("[notifySubscribers][topic({}) message({}) 通知订阅者({})失败]",
64   - topic, e, subscriber.getClass().getName());
65   - }
66   - }
67   - }
68   -
69   -}
70 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-core/yudao-module-iot-message-bus/src/main/java/cn/iocoder/yudao/module/iot/messagebus/core/IotMessageBus.java
... ... @@ -20,9 +20,8 @@ public interface IotMessageBus {
20 20 /**
21 21 * 注册消息订阅者
22 22 *
23   - * @param topic 主题
24 23 * @param subscriber 订阅者
25 24 */
26   - void register(String topic, IotMessageBusSubscriber<?> subscriber);
  25 + void register(IotMessageBusSubscriber<?> subscriber);
27 26  
28 27 }
29 28 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-core/yudao-module-iot-message-bus/src/main/java/cn/iocoder/yudao/module/iot/messagebus/core/IotMessageBusSubscriber.java
... ... @@ -10,18 +10,20 @@ package cn.iocoder.yudao.module.iot.messagebus.core;
10 10 public interface IotMessageBusSubscriber<T> {
11 11  
12 12 /**
13   - * 处理接收到的消息
14   - *
15   - * @param topic 主题
16   - * @param message 消息内容
  13 + * @return 主题
  14 + */
  15 + String getTopic();
  16 +
  17 + /**
  18 + * @return 分组
17 19 */
18   - void onMessage(String topic, T message);
  20 + String getGroup();
19 21  
20 22 /**
21   - * 获取订阅者的顺序
  23 + * 处理接收到的消息
22 24 *
23   - * @return 顺序值
  25 + * @param message 消息内容
24 26 */
25   - int order();
  27 + void onMessage(T message);
26 28  
27 29 }
28 30 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-core/yudao-module-iot-message-bus/src/main/java/cn/iocoder/yudao/module/iot/messagebus/core/local/LocalIotMessageBus.java
1 1 package cn.iocoder.yudao.module.iot.messagebus.core.local;
2 2  
3   -import cn.iocoder.yudao.module.iot.messagebus.core.AbstractIotMessageBus;
  3 +import cn.hutool.core.collection.CollUtil;
4 4 import cn.iocoder.yudao.module.iot.messagebus.core.IotMessageBus;
5 5 import cn.iocoder.yudao.module.iot.messagebus.core.IotMessageBusSubscriber;
6 6 import lombok.RequiredArgsConstructor;
... ... @@ -8,6 +8,13 @@ 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 +import java.util.List;
  14 +import java.util.Map;
  15 +import java.util.concurrent.ConcurrentHashMap;
  16 +import java.util.concurrent.CopyOnWriteArrayList;
  17 +
11 18 /**
12 19 * 本地的 {@link IotMessageBus} 实现类
13 20 *
... ... @@ -17,23 +24,46 @@ import org.springframework.context.event.EventListener;
17 24 */
18 25 @RequiredArgsConstructor
19 26 @Slf4j
20   -public class LocalIotMessageBus extends AbstractIotMessageBus {
  27 +public class LocalIotMessageBus implements IotMessageBus {
21 28  
22 29 private final ApplicationContext applicationContext;
23 30  
  31 + /**
  32 + * 订阅者映射表
  33 + * Key: topic
  34 + */
  35 + private final Map<String, List<IotMessageBusSubscriber<?>>> subscribers = new HashMap<>();
  36 +
24 37 @Override
25 38 public void post(String topic, Object message) {
26 39 applicationContext.publishEvent(new LocalIotMessage(topic, message));
27 40 }
28 41  
29 42 @Override
30   - protected void doRegister(String topic, IotMessageBusSubscriber<?> subscriber) {
31   - // 无需实现,交给 Spring @EventListener 监听
  43 + public void register(IotMessageBusSubscriber<?> subscriber) {
  44 + String topic = subscriber.getTopic();
  45 + List<IotMessageBusSubscriber<?>> topicSubscribers = subscribers.computeIfAbsent(topic, k -> new ArrayList<>());
  46 + topicSubscribers.add(subscriber);
  47 + log.info("[register][topic({}/{}) 注册消费者({})成功]",
  48 + topic, subscriber.getGroup(), subscriber.getClass().getName());
32 49 }
33 50  
34 51 @EventListener
  52 + @SuppressWarnings({"unchecked", "rawtypes"})
35 53 public void onMessage(LocalIotMessage message) {
36   - notifySubscribers(message.getTopic(), message.getMessage());
  54 + String topic = message.getTopic();
  55 + List<IotMessageBusSubscriber<?>> topicSubscribers = subscribers.get(topic);
  56 + if (CollUtil.isEmpty(topicSubscribers)) {
  57 + return;
  58 + }
  59 + for (IotMessageBusSubscriber subscriber : topicSubscribers) {
  60 + try {
  61 + subscriber.onMessage(message.getMessage());
  62 + } catch (Exception ex) {
  63 + log.error("[onMessage][topic({}/{}) message({}) 消费者({}) 处理异常]",
  64 + subscriber.getTopic(), subscriber.getGroup(), message.getMessage(), subscriber.getClass().getName(), ex);
  65 + }
  66 + }
37 67 }
38 68  
39 69 }
40 70 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-core/yudao-module-iot-message-bus/src/main/java/cn/iocoder/yudao/module/iot/messagebus/core/rocketmq/RocketMQIotMessageBus.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.messagebus.core.rocketmq;
  2 +
  3 +import cn.hutool.core.util.TypeUtil;
  4 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  5 +import cn.iocoder.yudao.module.iot.messagebus.core.IotMessageBus;
  6 +import cn.iocoder.yudao.module.iot.messagebus.core.IotMessageBusSubscriber;
  7 +import lombok.RequiredArgsConstructor;
  8 +import lombok.SneakyThrows;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
  11 +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
  12 +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
  13 +import org.apache.rocketmq.client.producer.SendResult;
  14 +import org.apache.rocketmq.common.message.MessageExt;
  15 +import org.apache.rocketmq.spring.autoconfigure.RocketMQProperties;
  16 +import org.apache.rocketmq.spring.core.RocketMQTemplate;
  17 +
  18 +import jakarta.annotation.PreDestroy;
  19 +
  20 +import java.lang.reflect.Type;
  21 +import java.util.ArrayList;
  22 +import java.util.List;
  23 +
  24 +/**
  25 + * 基于 RocketMQ 的 {@link IotMessageBus} 实现类
  26 + *
  27 + * @author 芋道源码
  28 + */
  29 +@RequiredArgsConstructor
  30 +@Slf4j
  31 +public class RocketMQIotMessageBus implements IotMessageBus {
  32 +
  33 + private final RocketMQProperties rocketMQProperties;
  34 +
  35 + private final RocketMQTemplate rocketMQTemplate;
  36 +
  37 + /**
  38 + * 主题对应的消费者映射
  39 + */
  40 + private final List<DefaultMQPushConsumer> topicConsumers = new ArrayList<>();
  41 +
  42 + @Override
  43 + public void post(String topic, Object message) {
  44 + SendResult result = rocketMQTemplate.syncSend(topic, JsonUtils.toJsonString(message));
  45 + log.info("[post][topic({}) 发送消息({}) result({})]", topic, message, result);
  46 + }
  47 +
  48 + @Override
  49 + @SneakyThrows
  50 + public void register(IotMessageBusSubscriber<?> subscriber) {
  51 + Type type = TypeUtil.getTypeArgument(subscriber.getClass(), 0);
  52 + if (type == null) {
  53 + throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
  54 + }
  55 +
  56 + // 1.1 创建 DefaultMQPushConsumer
  57 + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
  58 + consumer.setNamesrvAddr(rocketMQProperties.getNameServer());
  59 + consumer.setConsumerGroup(subscriber.getGroup());
  60 + // 1.2 订阅主题
  61 + consumer.subscribe(subscriber.getTopic(), "*");
  62 + // 1.3 设置消息监听器
  63 + consumer.setMessageListener((MessageListenerConcurrently) (messages, context) -> {
  64 + for (MessageExt messageExt : messages) {
  65 + try {
  66 + byte[] body = messageExt.getBody();
  67 + subscriber.onMessage(JsonUtils.parseObject(body, type));
  68 + } catch (Exception ex) {
  69 + log.error("[onMessage][topic({}/{}) message({}) 消费者({}) 处理异常]",
  70 + subscriber.getTopic(), subscriber.getGroup(), messageExt, subscriber.getClass().getName(), ex);
  71 + return ConsumeConcurrentlyStatus.RECONSUME_LATER;
  72 + }
  73 + }
  74 + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
  75 + });
  76 + // 1.4 启动消费者
  77 + consumer.start();
  78 +
  79 + // 2. 保存消费者引用
  80 + topicConsumers.add(consumer);
  81 + }
  82 +
  83 + /**
  84 + * 销毁时关闭所有消费者
  85 + */
  86 + @PreDestroy
  87 + public void destroy() {
  88 + for (DefaultMQPushConsumer consumer : topicConsumers) {
  89 + try {
  90 + consumer.shutdown();
  91 + log.info("[destroy][关闭 group({}) 的消费者成功]", consumer.getConsumerGroup());
  92 + } catch (Exception e) {
  93 + log.error("[destroy]关闭 group({}) 的消费者异常]", consumer.getConsumerGroup(), e);
  94 + }
  95 + }
  96 + }
  97 +
  98 +}
0 99 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-core/yudao-module-iot-message-bus/src/test/java/cn/iocoder/yudao/module/iot/messagebus/core/TestMessage.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.messagebus.core;
  2 +
  3 +import lombok.Data;
  4 +
  5 +@Data
  6 +public class TestMessage {
  7 +
  8 + private String nickname;
  9 +
  10 + private Integer age;
  11 +
  12 +}
0 13 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-core/yudao-module-iot-message-bus/src/test/java/cn/iocoder/yudao/module/iot/messagebus/core/local/LocalIotMessageBusIntegrationTest.java
... ... @@ -6,7 +6,6 @@ import cn.iocoder.yudao.module.iot.messagebus.core.IotMessageBusSubscriber;
6 6 import jakarta.annotation.Resource;
7 7 import lombok.extern.slf4j.Slf4j;
8 8 import org.junit.jupiter.api.Test;
9   -import org.springframework.beans.factory.annotation.Autowired;
10 9 import org.springframework.boot.test.context.SpringBootTest;
11 10 import org.springframework.context.annotation.Import;
12 11 import org.springframework.test.context.TestPropertySource;
... ... @@ -51,17 +50,21 @@ public class LocalIotMessageBusIntegrationTest {
51 50 IotMessageBusSubscriber<String> subscriber1 = new IotMessageBusSubscriber<>() {
52 51  
53 52 @Override
54   - public void onMessage(String topic, String message) {
55   - log.info("[订阅者1] 收到消息 - Topic: {}, Message: {}", topic, message);
56   - subscriber1Count.incrementAndGet();
57   - assertEquals("test-topic", topic);
58   - assertEquals(testMessage, message);
59   - latch.countDown();
  53 + public String getTopic() {
  54 + return topic;
  55 + }
  56 +
  57 + @Override
  58 + public String getGroup() {
  59 + return "group1";
60 60 }
61 61  
62 62 @Override
63   - public int order() {
64   - return 1;
  63 + public void onMessage(String message) {
  64 + log.info("[订阅者1] 收到消息 - Topic: {}, Message: {}", getTopic(), message);
  65 + subscriber1Count.incrementAndGet();
  66 + assertEquals(testMessage, message);
  67 + latch.countDown();
65 68 }
66 69  
67 70 };
... ... @@ -69,35 +72,39 @@ public class LocalIotMessageBusIntegrationTest {
69 72 IotMessageBusSubscriber<String> subscriber2 = new IotMessageBusSubscriber<>() {
70 73  
71 74 @Override
72   - public void onMessage(String topic, String message) {
73   - log.info("[订阅者2] 收到消息 - Topic: {}, Message: {}", topic, message);
74   - subscriber2Count.incrementAndGet();
75   - assertEquals("test-topic", topic);
76   - assertEquals(testMessage, message);
77   - latch.countDown();
  75 + public String getTopic() {
  76 + return topic;
78 77 }
79 78  
80 79 @Override
81   - public int order() {
82   - return 0;
  80 + public String getGroup() {
  81 + return "group2";
  82 + }
  83 +
  84 + @Override
  85 + public void onMessage(String message) {
  86 + log.info("[订阅者2] 收到消息 - Topic: {}, Message: {}", getTopic(), message);
  87 + subscriber2Count.incrementAndGet();
  88 + assertEquals(testMessage, message);
  89 + latch.countDown();
83 90 }
84 91  
85 92 };
86 93 // 注册订阅者
87   - messageBus.register(topic, subscriber1);
88   - messageBus.register(topic, subscriber2);
  94 + messageBus.register(subscriber1);
  95 + messageBus.register(subscriber2);
89 96  
90 97 // 发送消息
91 98 log.info("[测试] 发送消息 - Topic: {}, Message: {}", topic, testMessage);
92 99 messageBus.post(topic, testMessage);
93   - // 等待消息处理完成(最多等待5秒)
94   - boolean completed = latch.await(5, TimeUnit.SECONDS);
  100 + // 等待消息处理完成(最多等待 10 秒)
  101 + boolean completed = latch.await(10, TimeUnit.SECONDS);
95 102  
96 103 // 验证结果
97 104 assertTrue(completed, "消息处理超时");
98   - assertEquals(1, subscriber1Count.get(), "订阅者1应该收到1条消息");
99   - assertEquals(1, subscriber2Count.get(), "订阅者2应该收到1条消息");
100   - log.info("[测试] 测试完成 - 订阅者1收到{}条消息,订阅者2收到{}条消息", subscriber1Count.get(), subscriber2Count.get());
  105 + assertEquals(1, subscriber1Count.get(), "订阅者 1 应该收到 1 条消息");
  106 + assertEquals(1, subscriber2Count.get(), "订阅者 2 应该收到 1 条消息");
  107 + log.info("[测试] 测试完成 - 订阅者 1 收到{}条消息,订阅者 2 收到{}条消息", subscriber1Count.get(), subscriber2Count.get());
101 108 }
102 109  
103 110 /**
... ... @@ -116,16 +123,20 @@ public class LocalIotMessageBusIntegrationTest {
116 123 IotMessageBusSubscriber<String> statusSubscriber = new IotMessageBusSubscriber<>() {
117 124  
118 125 @Override
119   - public void onMessage(String topic, String message) {
120   - log.info("[状态订阅者] 收到消息 - Topic: {}, Message: {}", topic, message);
121   - assertEquals(topic1, topic);
122   - assertEquals(message1, message);
123   - latch.countDown();
  126 + public String getTopic() {
  127 + return topic1;
124 128 }
125 129  
126 130 @Override
127   - public int order() {
128   - return 0;
  131 + public String getGroup() {
  132 + return "status-group";
  133 + }
  134 +
  135 + @Override
  136 + public void onMessage(String message) {
  137 + log.info("[状态订阅者] 收到消息 - Topic: {}, Message: {}", getTopic(), message);
  138 + assertEquals(message1, message);
  139 + latch.countDown();
129 140 }
130 141  
131 142 };
... ... @@ -133,28 +144,32 @@ public class LocalIotMessageBusIntegrationTest {
133 144 IotMessageBusSubscriber<String> dataSubscriber = new IotMessageBusSubscriber<>() {
134 145  
135 146 @Override
136   - public void onMessage(String topic, String message) {
137   - log.info("[数据订阅者] 收到消息 - Topic: {}, Message: {}", topic, message);
138   - assertEquals(topic2, topic);
139   - assertEquals(message2, message);
140   - latch.countDown();
  147 + public String getTopic() {
  148 + return topic2;
  149 + }
  150 +
  151 + @Override
  152 + public String getGroup() {
  153 + return "data-group";
141 154 }
142 155  
143 156 @Override
144   - public int order() {
145   - return 1;
  157 + public void onMessage(String message) {
  158 + log.info("[数据订阅者] 收到消息 - Topic: {}, Message: {}", getTopic(), message);
  159 + assertEquals(message2, message);
  160 + latch.countDown();
146 161 }
147 162  
148 163 };
149 164 // 注册订阅者到不同主题
150   - messageBus.register(topic1, statusSubscriber);
151   - messageBus.register(topic2, dataSubscriber);
  165 + messageBus.register(statusSubscriber);
  166 + messageBus.register(dataSubscriber);
152 167  
153 168 // 发送消息到不同主题
154 169 messageBus.post(topic1, message1);
155 170 messageBus.post(topic2, message2);
156 171 // 等待消息处理完成
157   - boolean completed = latch.await(5, TimeUnit.SECONDS);
  172 + boolean completed = latch.await(10, TimeUnit.SECONDS);
158 173 assertTrue(completed, "消息处理超时");
159 174 log.info("[测试] 多主题测试完成");
160 175 }
... ...
yudao-module-iot/yudao-module-iot-core/yudao-module-iot-message-bus/src/test/java/cn/iocoder/yudao/module/iot/messagebus/core/rocketmq/RocketMQIotMessageBusTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.messagebus.core.rocketmq;
  2 +
  3 +import cn.hutool.core.util.IdUtil;
  4 +import cn.iocoder.yudao.module.iot.messagebus.config.IotMessageBusAutoConfiguration;
  5 +import cn.iocoder.yudao.module.iot.messagebus.core.IotMessageBus;
  6 +import cn.iocoder.yudao.module.iot.messagebus.core.IotMessageBusSubscriber;
  7 +import cn.iocoder.yudao.module.iot.messagebus.core.TestMessage;
  8 +import jakarta.annotation.Resource;
  9 +import lombok.Data;
  10 +import lombok.extern.slf4j.Slf4j;
  11 +import org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration;
  12 +import org.apache.rocketmq.spring.core.RocketMQTemplate;
  13 +import org.junit.jupiter.api.Test;
  14 +import org.springframework.beans.factory.annotation.Autowired;
  15 +import org.springframework.boot.test.context.SpringBootTest;
  16 +import org.springframework.context.annotation.Import;
  17 +import org.springframework.test.context.TestPropertySource;
  18 +
  19 +import java.util.concurrent.CountDownLatch;
  20 +import java.util.concurrent.TimeUnit;
  21 +import java.util.concurrent.atomic.AtomicInteger;
  22 +import java.util.concurrent.atomic.AtomicReference;
  23 +
  24 +import static org.junit.jupiter.api.Assertions.*;
  25 +
  26 +/**
  27 + * {@link RocketMQIotMessageBus} 集成测试
  28 + *
  29 + * @author 芋道源码
  30 + */
  31 +@SpringBootTest(classes = RocketMQIotMessageBusTest.class)
  32 +@Import({RocketMQAutoConfiguration.class, IotMessageBusAutoConfiguration.class})
  33 +@TestPropertySource(properties = {
  34 + "yudao.iot.message-bus.type=rocketmq",
  35 + "rocketmq.name-server=127.0.0.1:9876",
  36 + "rocketmq.producer.group=test-rocketmq-group",
  37 + "rocketmq.producer.send-message-timeout=10000"
  38 +})
  39 +@Slf4j
  40 +public class RocketMQIotMessageBusTest {
  41 +
  42 + @Resource
  43 + private IotMessageBus messageBus;
  44 +
  45 + /**
  46 + * 1 topic 1 subscriber(string)
  47 + */
  48 + @Test
  49 + public void testSendMessageWithOneSubscriber() throws InterruptedException {
  50 + // 准备
  51 + String topic = "test-topic-" + IdUtil.simpleUUID();
  52 +// String topic = "test-topic-pojo";
  53 + String testMessage = "Hello IoT Message Bus!";
  54 + // 用于等待消息处理完成
  55 + CountDownLatch latch = new CountDownLatch(1);
  56 + // 用于记录接收到的消息
  57 + AtomicInteger subscriberCount = new AtomicInteger(0);
  58 + AtomicReference<String> subscriberMessageRef = new AtomicReference<>();
  59 +
  60 + // 发送消息(需要提前发,保证 RocketMQ 路由的创建)
  61 + log.info("[测试] 发送消息 - Topic: {}, Message: {}", topic, testMessage);
  62 + messageBus.post(topic, testMessage);
  63 +
  64 + // 创建订阅者
  65 + IotMessageBusSubscriber<String> subscriber1 = new IotMessageBusSubscriber<>() {
  66 +
  67 + @Override
  68 + public String getTopic() {
  69 + return topic;
  70 + }
  71 +
  72 + @Override
  73 + public String getGroup() {
  74 + return "test-topic-" + IdUtil.simpleUUID() + "-consumer";
  75 +// return "test-topic-consumer-01";
  76 + }
  77 +
  78 + @Override
  79 + public void onMessage(String message) {
  80 + log.info("[订阅者] 收到消息 - Topic: {}, Message: {}", getTopic(), message);
  81 + subscriberCount.incrementAndGet();
  82 + subscriberMessageRef.set(message);
  83 + assertEquals(testMessage, message);
  84 + latch.countDown();
  85 + }
  86 +
  87 + };
  88 + // 注册订阅者
  89 + messageBus.register(subscriber1);
  90 +
  91 + // 等待消息处理完成(最多等待 5 秒)
  92 + boolean completed = latch.await(10, TimeUnit.SECONDS);
  93 +
  94 + // 验证结果
  95 + assertTrue(completed, "消息处理超时");
  96 + assertEquals(1, subscriberCount.get(), "订阅者应该收到 1 条消息");
  97 + log.info("[测试] 测试完成 - 订阅者收到{}条消息", subscriberCount.get());
  98 + assertEquals(testMessage, subscriberMessageRef.get(), "接收到的消息内容不匹配");
  99 + }
  100 +
  101 + /**
  102 + * 1 topic 2 subscriber(pojo)
  103 + */
  104 + @Test
  105 + public void testSendMessageWithTwoSubscribers() throws InterruptedException {
  106 + // 准备
  107 + String topic = "test-topic-" + IdUtil.simpleUUID();
  108 +// String topic = "test-topic-pojo";
  109 + TestMessage testMessage = new TestMessage().setNickname("yunai").setAge(18);
  110 + // 用于等待消息处理完成
  111 + CountDownLatch latch = new CountDownLatch(2);
  112 + // 用于记录接收到的消息
  113 + AtomicInteger subscriber1Count = new AtomicInteger(0);
  114 + AtomicReference<TestMessage> subscriber1MessageRef = new AtomicReference<>();
  115 + AtomicInteger subscriber2Count = new AtomicInteger(0);
  116 + AtomicReference<TestMessage> subscriber2MessageRef = new AtomicReference<>();
  117 +
  118 + // 发送消息(需要提前发,保证 RocketMQ 路由的创建)
  119 + log.info("[测试] 发送消息 - Topic: {}, Message: {}", topic, testMessage);
  120 + messageBus.post(topic, testMessage);
  121 +
  122 + // 创建第一个订阅者
  123 + IotMessageBusSubscriber<TestMessage> subscriber1 = new IotMessageBusSubscriber<>() {
  124 +
  125 + @Override
  126 + public String getTopic() {
  127 + return topic;
  128 + }
  129 +
  130 + @Override
  131 + public String getGroup() {
  132 + return "test-topic-" + IdUtil.simpleUUID() + "-consumer";
  133 +// return "test-topic-consumer-01";
  134 + }
  135 +
  136 + @Override
  137 + public void onMessage(TestMessage message) {
  138 + log.info("[订阅者1] 收到消息 - Topic: {}, Message: {}", getTopic(), message);
  139 + subscriber1Count.incrementAndGet();
  140 + subscriber1MessageRef.set(message);
  141 + assertEquals(testMessage, message);
  142 + latch.countDown();
  143 + }
  144 +
  145 + };
  146 + // 创建第二个订阅者
  147 + IotMessageBusSubscriber<TestMessage> subscriber2 = new IotMessageBusSubscriber<>() {
  148 +
  149 + @Override
  150 + public String getTopic() {
  151 + return topic;
  152 + }
  153 +
  154 + @Override
  155 + public String getGroup() {
  156 + return "test-topic-" + IdUtil.simpleUUID() + "-consumer";
  157 +// return "test-topic-consumer-02";
  158 + }
  159 +
  160 + @Override
  161 + public void onMessage(TestMessage message) {
  162 + log.info("[订阅者2] 收到消息 - Topic: {}, Message: {}", getTopic(), message);
  163 + subscriber2Count.incrementAndGet();
  164 + subscriber2MessageRef.set(message);
  165 + assertEquals(testMessage, message);
  166 + latch.countDown();
  167 + }
  168 +
  169 + };
  170 + // 注册订阅者
  171 + messageBus.register(subscriber1);
  172 + messageBus.register(subscriber2);
  173 +
  174 + // 等待消息处理完成(最多等待 5 秒)
  175 + boolean completed = latch.await(10, TimeUnit.SECONDS);
  176 +
  177 + // 验证结果
  178 + assertTrue(completed, "消息处理超时");
  179 + assertEquals(1, subscriber1Count.get(), "订阅者 1 应该收到 1 条消息");
  180 + assertEquals(1, subscriber2Count.get(), "订阅者 2 应该收到 1 条消息");
  181 + log.info("[测试] 测试完成 - 订阅者 1 收到{}条消息,订阅者2收到{}条消息", subscriber1Count.get(), subscriber2Count.get());
  182 + assertEquals(testMessage, subscriber1MessageRef.get(), "接收到的消息内容不匹配");
  183 + assertEquals(testMessage, subscriber2MessageRef.get(), "接收到的消息内容不匹配");
  184 + }
  185 +
  186 + /**
  187 + * 2 topic 2 subscriber
  188 + */
  189 + @Test
  190 + public void testMultipleTopics() throws InterruptedException {
  191 + // 准备
  192 + String topic1 = "device-status-" + IdUtil.simpleUUID();
  193 + String topic2 = "device-data-" + IdUtil.simpleUUID();
  194 + String message1 = "设备在线";
  195 + String message2 = "温度:25°C";
  196 + CountDownLatch latch = new CountDownLatch(2);
  197 + AtomicInteger subscriber1Count = new AtomicInteger(0);
  198 + AtomicReference<String> subscriber1MessageRef = new AtomicReference<>();
  199 + AtomicInteger subscriber2Count = new AtomicInteger(0);
  200 + AtomicReference<String> subscriber2MessageRef = new AtomicReference<>();
  201 +
  202 +
  203 + // 发送消息到不同主题(需要提前发,保证 RocketMQ 路由的创建)
  204 + log.info("[测试] 发送消息 - Topic1: {}, Message1: {}", topic1, message1);
  205 + messageBus.post(topic1, message1);
  206 + log.info("[测试] 发送消息 - Topic2: {}, Message2: {}", topic2, message2);
  207 + messageBus.post(topic2, message2);
  208 +
  209 + // 创建订阅者 1 - 只订阅设备状态
  210 + IotMessageBusSubscriber<String> statusSubscriber = new IotMessageBusSubscriber<>() {
  211 +
  212 + @Override
  213 + public String getTopic() {
  214 + return topic1;
  215 + }
  216 +
  217 + @Override
  218 + public String getGroup() {
  219 + return "status-group-" + IdUtil.simpleUUID();
  220 + }
  221 +
  222 + @Override
  223 + public void onMessage(String message) {
  224 + log.info("[状态订阅者] 收到消息 - Topic: {}, Message: {}", getTopic(), message);
  225 + subscriber1Count.incrementAndGet();
  226 + subscriber1MessageRef.set(message);
  227 + assertEquals(message1, message);
  228 + latch.countDown();
  229 + }
  230 +
  231 + };
  232 + // 创建订阅者 2 - 只订阅设备数据
  233 + IotMessageBusSubscriber<String> dataSubscriber = new IotMessageBusSubscriber<>() {
  234 +
  235 + @Override
  236 + public String getTopic() {
  237 + return topic2;
  238 + }
  239 +
  240 + @Override
  241 + public String getGroup() {
  242 + return "data-group-" + IdUtil.simpleUUID();
  243 + }
  244 +
  245 + @Override
  246 + public void onMessage(String message) {
  247 + log.info("[数据订阅者] 收到消息 - Topic: {}, Message: {}", getTopic(), message);
  248 + subscriber2Count.incrementAndGet();
  249 + subscriber2MessageRef.set(message);
  250 + assertEquals(message2, message);
  251 + latch.countDown();
  252 + }
  253 +
  254 + };
  255 + // 注册订阅者到不同主题
  256 + messageBus.register(statusSubscriber);
  257 + messageBus.register(dataSubscriber);
  258 +
  259 + // 等待消息处理完成
  260 + boolean completed = latch.await(10, TimeUnit.SECONDS);
  261 +
  262 + // 验证结果
  263 + assertTrue(completed, "消息处理超时");
  264 + assertEquals(1, subscriber1Count.get(), "状态订阅者应该收到 1 条消息");
  265 + assertEquals(message1, subscriber1MessageRef.get(), "状态订阅者接收到的消息内容不匹配");
  266 + assertEquals(1, subscriber2Count.get(), "数据订阅者应该收到 1 条消息");
  267 + assertEquals(message2, subscriber2MessageRef.get(), "数据订阅者接收到的消息内容不匹配");
  268 + log.info("[测试] 多主题测试完成 - 状态订阅者收到{}条消息,数据订阅者收到{}条消息", subscriber1Count.get(), subscriber2Count.get());
  269 + }
  270 +
  271 +}
0 272 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-core/yudao-module-iot-message-bus/src/test/resources/application-test.yml deleted
1   -yudao:
2   - iot:
3   - message-bus:
4   - type: local
5 0 \ No newline at end of file