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,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 * 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下, 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,8 +2,12 @@ package cn.iocoder.yudao.module.iot.messagebus.config;
2 2
3 import cn.iocoder.yudao.module.iot.messagebus.core.IotMessageBus; 3 import cn.iocoder.yudao.module.iot.messagebus.core.IotMessageBus;
4 import cn.iocoder.yudao.module.iot.messagebus.core.local.LocalIotMessageBus; 4 import cn.iocoder.yudao.module.iot.messagebus.core.local.LocalIotMessageBus;
  5 +import cn.iocoder.yudao.module.iot.messagebus.core.rocketmq.RocketMQIotMessageBus;
5 import lombok.extern.slf4j.Slf4j; 6 import lombok.extern.slf4j.Slf4j;
  7 +import org.apache.rocketmq.spring.autoconfigure.RocketMQProperties;
  8 +import org.apache.rocketmq.spring.core.RocketMQTemplate;
6 import org.springframework.boot.autoconfigure.AutoConfiguration; 9 import org.springframework.boot.autoconfigure.AutoConfiguration;
  10 +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
7 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 11 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
8 import org.springframework.boot.context.properties.EnableConfigurationProperties; 12 import org.springframework.boot.context.properties.EnableConfigurationProperties;
9 import org.springframework.context.ApplicationContext; 13 import org.springframework.context.ApplicationContext;
@@ -34,4 +38,19 @@ public class IotMessageBusAutoConfiguration { @@ -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 \ No newline at end of file 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 \ No newline at end of file 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,9 +20,8 @@ public interface IotMessageBus {
20 /** 20 /**
21 * 注册消息订阅者 21 * 注册消息订阅者
22 * 22 *
23 - * @param topic 主题  
24 * @param subscriber 订阅者 23 * @param subscriber 订阅者
25 */ 24 */
26 - void register(String topic, IotMessageBusSubscriber<?> subscriber); 25 + void register(IotMessageBusSubscriber<?> subscriber);
27 26
28 } 27 }
29 \ No newline at end of file 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,18 +10,20 @@ package cn.iocoder.yudao.module.iot.messagebus.core;
10 public interface IotMessageBusSubscriber<T> { 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 \ No newline at end of file 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 package cn.iocoder.yudao.module.iot.messagebus.core.local; 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 import cn.iocoder.yudao.module.iot.messagebus.core.IotMessageBus; 4 import cn.iocoder.yudao.module.iot.messagebus.core.IotMessageBus;
5 import cn.iocoder.yudao.module.iot.messagebus.core.IotMessageBusSubscriber; 5 import cn.iocoder.yudao.module.iot.messagebus.core.IotMessageBusSubscriber;
6 import lombok.RequiredArgsConstructor; 6 import lombok.RequiredArgsConstructor;
@@ -8,6 +8,13 @@ import lombok.extern.slf4j.Slf4j; @@ -8,6 +8,13 @@ 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;
  14 +import java.util.Map;
  15 +import java.util.concurrent.ConcurrentHashMap;
  16 +import java.util.concurrent.CopyOnWriteArrayList;
  17 +
11 /** 18 /**
12 * 本地的 {@link IotMessageBus} 实现类 19 * 本地的 {@link IotMessageBus} 实现类
13 * 20 *
@@ -17,23 +24,46 @@ import org.springframework.context.event.EventListener; @@ -17,23 +24,46 @@ import org.springframework.context.event.EventListener;
17 */ 24 */
18 @RequiredArgsConstructor 25 @RequiredArgsConstructor
19 @Slf4j 26 @Slf4j
20 -public class LocalIotMessageBus extends AbstractIotMessageBus { 27 +public class LocalIotMessageBus implements IotMessageBus {
21 28
22 private final ApplicationContext applicationContext; 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 @Override 37 @Override
25 public void post(String topic, Object message) { 38 public void post(String topic, Object message) {
26 applicationContext.publishEvent(new LocalIotMessage(topic, message)); 39 applicationContext.publishEvent(new LocalIotMessage(topic, message));
27 } 40 }
28 41
29 @Override 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 @EventListener 51 @EventListener
  52 + @SuppressWarnings({"unchecked", "rawtypes"})
35 public void onMessage(LocalIotMessage message) { 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 \ No newline at end of file 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 \ No newline at end of file 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 \ No newline at end of file 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,7 +6,6 @@ import cn.iocoder.yudao.module.iot.messagebus.core.IotMessageBusSubscriber;
6 import jakarta.annotation.Resource; 6 import jakarta.annotation.Resource;
7 import lombok.extern.slf4j.Slf4j; 7 import lombok.extern.slf4j.Slf4j;
8 import org.junit.jupiter.api.Test; 8 import org.junit.jupiter.api.Test;
9 -import org.springframework.beans.factory.annotation.Autowired;  
10 import org.springframework.boot.test.context.SpringBootTest; 9 import org.springframework.boot.test.context.SpringBootTest;
11 import org.springframework.context.annotation.Import; 10 import org.springframework.context.annotation.Import;
12 import org.springframework.test.context.TestPropertySource; 11 import org.springframework.test.context.TestPropertySource;
@@ -51,17 +50,21 @@ public class LocalIotMessageBusIntegrationTest { @@ -51,17 +50,21 @@ public class LocalIotMessageBusIntegrationTest {
51 IotMessageBusSubscriber<String> subscriber1 = new IotMessageBusSubscriber<>() { 50 IotMessageBusSubscriber<String> subscriber1 = new IotMessageBusSubscriber<>() {
52 51
53 @Override 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 @Override 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,35 +72,39 @@ public class LocalIotMessageBusIntegrationTest {
69 IotMessageBusSubscriber<String> subscriber2 = new IotMessageBusSubscriber<>() { 72 IotMessageBusSubscriber<String> subscriber2 = new IotMessageBusSubscriber<>() {
70 73
71 @Override 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 @Override 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 log.info("[测试] 发送消息 - Topic: {}, Message: {}", topic, testMessage); 98 log.info("[测试] 发送消息 - Topic: {}, Message: {}", topic, testMessage);
92 messageBus.post(topic, testMessage); 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 assertTrue(completed, "消息处理超时"); 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,16 +123,20 @@ public class LocalIotMessageBusIntegrationTest {
116 IotMessageBusSubscriber<String> statusSubscriber = new IotMessageBusSubscriber<>() { 123 IotMessageBusSubscriber<String> statusSubscriber = new IotMessageBusSubscriber<>() {
117 124
118 @Override 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 @Override 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,28 +144,32 @@ public class LocalIotMessageBusIntegrationTest {
133 IotMessageBusSubscriber<String> dataSubscriber = new IotMessageBusSubscriber<>() { 144 IotMessageBusSubscriber<String> dataSubscriber = new IotMessageBusSubscriber<>() {
134 145
135 @Override 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 @Override 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 messageBus.post(topic1, message1); 169 messageBus.post(topic1, message1);
155 messageBus.post(topic2, message2); 170 messageBus.post(topic2, message2);
156 // 等待消息处理完成 171 // 等待消息处理完成
157 - boolean completed = latch.await(5, TimeUnit.SECONDS); 172 + boolean completed = latch.await(10, TimeUnit.SECONDS);
158 assertTrue(completed, "消息处理超时"); 173 assertTrue(completed, "消息处理超时");
159 log.info("[测试] 多主题测试完成"); 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 \ No newline at end of file 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 \ No newline at end of file 0 \ No newline at end of file