Commit 39fb31d4002fd19beca1d4d1341d73cc3823b0e5

Authored by YunaiV
1 parent af37176d

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

Showing 13 changed files with 500 additions and 0 deletions
yudao-module-iot/pom.xml
@@ -11,6 +11,7 @@ @@ -11,6 +11,7 @@
11 <module>yudao-module-iot-biz</module> 11 <module>yudao-module-iot-biz</module>
12 <module>yudao-module-iot-net-components</module> 12 <module>yudao-module-iot-net-components</module>
13 <module>yudao-module-iot-protocol</module> 13 <module>yudao-module-iot-protocol</module>
  14 + <module>yudao-module-iot-core</module>
14 </modules> 15 </modules>
15 <modelVersion>4.0.0</modelVersion> 16 <modelVersion>4.0.0</modelVersion>
16 17
yudao-module-iot/yudao-module-iot-core/pom.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  3 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5 + <parent>
  6 + <artifactId>yudao-module-iot</artifactId>
  7 + <groupId>cn.iocoder.boot</groupId>
  8 + <version>${revision}</version>
  9 + </parent>
  10 + <modules>
  11 + <module>yudao-module-iot-message-bus</module>
  12 + </modules>
  13 + <modelVersion>4.0.0</modelVersion>
  14 +
  15 + <artifactId>yudao-module-iot-core</artifactId>
  16 + <packaging>pom</packaging>
  17 +
  18 + <name>${project.artifactId}</name>
  19 + <description>
  20 + iot 模块下,提供 biz 和 gateway-server 模块的核心功能。
  21 + 例如说:消息总线、消息协议(编解码)等。
  22 + </description>
  23 +
  24 +</project>
0 \ No newline at end of file 25 \ No newline at end of file
yudao-module-iot/yudao-module-iot-core/yudao-module-iot-message-bus/pom.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  3 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5 + <parent>
  6 + <artifactId>yudao-module-iot-core</artifactId>
  7 + <groupId>cn.iocoder.boot</groupId>
  8 + <version>${revision}</version>
  9 + </parent>
  10 + <modelVersion>4.0.0</modelVersion>
  11 + <artifactId>yudao-module-iot-message-bus</artifactId>
  12 + <packaging>jar</packaging>
  13 +
  14 + <name>${project.artifactId}</name>
  15 + <description>
  16 + iot 模块下,提供消息总线的功能。
  17 + 可选择使用 spring event、redis stream、rocketmq、kafka、rabbitmq 等。
  18 + </description>
  19 +
  20 + <dependencies>
  21 + <dependency>
  22 + <groupId>cn.iocoder.boot</groupId>
  23 + <artifactId>yudao-common</artifactId>
  24 + </dependency>
  25 +
  26 + <!-- Spring 核心 -->
  27 + <dependency>
  28 + <groupId>org.springframework.boot</groupId>
  29 + <artifactId>spring-boot-starter</artifactId>
  30 + </dependency>
  31 +
  32 + <!-- 消息中间件相关(可选依赖) -->
  33 + <dependency>
  34 + <groupId>org.springframework.data</groupId>
  35 + <artifactId>spring-data-redis</artifactId>
  36 + <optional>true</optional>
  37 + </dependency>
  38 +
  39 + <dependency>
  40 + <groupId>org.apache.rocketmq</groupId>
  41 + <artifactId>rocketmq-spring-boot-starter</artifactId>
  42 + <optional>true</optional>
  43 + </dependency>
  44 +
  45 + <dependency>
  46 + <groupId>org.springframework.amqp</groupId>
  47 + <artifactId>spring-rabbit</artifactId>
  48 + <optional>true</optional>
  49 + </dependency>
  50 +
  51 + <dependency>
  52 + <groupId>org.springframework.kafka</groupId>
  53 + <artifactId>spring-kafka</artifactId>
  54 + <optional>true</optional>
  55 + </dependency>
  56 +
  57 + <!-- 测试依赖 -->
  58 + <dependency>
  59 + <groupId>org.springframework.boot</groupId>
  60 + <artifactId>spring-boot-starter-test</artifactId>
  61 + <scope>test</scope>
  62 + </dependency>
  63 + </dependencies>
  64 +
  65 +</project>
0 \ No newline at end of file 66 \ 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/config/IotMessageBusAutoConfiguration.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.messagebus.config;
  2 +
  3 +import cn.iocoder.yudao.module.iot.messagebus.core.IotMessageBus;
  4 +import cn.iocoder.yudao.module.iot.messagebus.core.local.LocalIotMessageBus;
  5 +import lombok.extern.slf4j.Slf4j;
  6 +import org.springframework.boot.autoconfigure.AutoConfiguration;
  7 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  8 +import org.springframework.boot.context.properties.EnableConfigurationProperties;
  9 +import org.springframework.context.ApplicationContext;
  10 +import org.springframework.context.annotation.Bean;
  11 +import org.springframework.context.annotation.Configuration;
  12 +
  13 +/**
  14 + * IoT 消息总线自动配置
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@AutoConfiguration
  19 +@EnableConfigurationProperties(IotMessageBusProperties.class)
  20 +@Slf4j
  21 +public class IotMessageBusAutoConfiguration {
  22 +
  23 + // ==================== Local 实现 ====================
  24 +
  25 + @Configuration
  26 + @ConditionalOnProperty(prefix = "yudao.iot.message-bus", name = "type", havingValue = "local", matchIfMissing = true)
  27 + public static class LocalIotMessageBusConfiguration {
  28 +
  29 + @Bean
  30 + public IotMessageBus localIotMessageBus(ApplicationContext applicationContext) {
  31 + log.info("[localIotMessageBus][创建 Local IoT 消息总线]");
  32 + return new LocalIotMessageBus(applicationContext);
  33 + }
  34 +
  35 + }
  36 +
  37 +}
0 \ No newline at end of file 38 \ 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/config/IotMessageBusProperties.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.messagebus.config;
  2 +
  3 +import lombok.Data;
  4 +import org.springframework.boot.context.properties.ConfigurationProperties;
  5 +import org.springframework.validation.annotation.Validated;
  6 +
  7 +import jakarta.validation.constraints.NotEmpty;
  8 +import jakarta.validation.constraints.NotNull;
  9 +
  10 +/**
  11 + * IoT 消息总线配置属性
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@ConfigurationProperties("yudao.iot.message-bus")
  16 +@Data
  17 +@Validated
  18 +public class IotMessageBusProperties {
  19 +
  20 + /**
  21 + * 消息总线类型
  22 + *
  23 + * 可选值:local、redis、rocketmq、rabbitmq
  24 + */
  25 + @NotNull(message = "IoT 消息总线类型不能为空")
  26 + private String type = "local";
  27 +
  28 +}
0 \ No newline at end of file 29 \ 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 0 → 100644
  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 +}
0 \ 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/IotMessageBus.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.messagebus.core;
  2 +
  3 +/**
  4 + * IoT 消息总线接口
  5 + *
  6 + * 用于在 IoT 系统中发布和订阅消息,支持多种消息中间件实现
  7 + *
  8 + * @author 芋道源码
  9 + */
  10 +public interface IotMessageBus {
  11 +
  12 + /**
  13 + * 发布消息到消息总线
  14 + *
  15 + * @param topic 主题
  16 + * @param message 消息内容
  17 + */
  18 + void post(String topic, Object message);
  19 +
  20 + /**
  21 + * 注册消息订阅者
  22 + *
  23 + * @param topic 主题
  24 + * @param subscriber 订阅者
  25 + */
  26 + void register(String topic, IotMessageBusSubscriber<?> subscriber);
  27 +
  28 +}
0 \ No newline at end of file 29 \ 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 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.messagebus.core;
  2 +
  3 +/**
  4 + * IoT 消息总线订阅者接口
  5 + *
  6 + * 用于处理从消息总线接收到的消息
  7 + *
  8 + * @author 芋道源码
  9 + */
  10 +public interface IotMessageBusSubscriber<T> {
  11 +
  12 + /**
  13 + * 处理接收到的消息
  14 + *
  15 + * @param topic 主题
  16 + * @param message 消息内容
  17 + */
  18 + void onMessage(String topic, T message);
  19 +
  20 + /**
  21 + * 获取订阅者的顺序
  22 + *
  23 + * @return 顺序值
  24 + */
  25 + int order();
  26 +
  27 +}
0 \ 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/local/LocalIotMessage.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.messagebus.core.local;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Data;
  5 +
  6 +@Data
  7 +@AllArgsConstructor
  8 +public class LocalIotMessage {
  9 +
  10 + private String topic;
  11 +
  12 + private Object message;
  13 +
  14 +}
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 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.messagebus.core.local;
  2 +
  3 +import cn.iocoder.yudao.module.iot.messagebus.core.AbstractIotMessageBus;
  4 +import cn.iocoder.yudao.module.iot.messagebus.core.IotMessageBus;
  5 +import cn.iocoder.yudao.module.iot.messagebus.core.IotMessageBusSubscriber;
  6 +import lombok.RequiredArgsConstructor;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +import org.springframework.context.ApplicationContext;
  9 +import org.springframework.context.event.EventListener;
  10 +
  11 +/**
  12 + * 本地的 {@link IotMessageBus} 实现类
  13 + *
  14 + * 注意:仅适用于单机场景!!!
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@RequiredArgsConstructor
  19 +@Slf4j
  20 +public class LocalIotMessageBus extends AbstractIotMessageBus {
  21 +
  22 + private final ApplicationContext applicationContext;
  23 +
  24 + @Override
  25 + public void post(String topic, Object message) {
  26 + applicationContext.publishEvent(new LocalIotMessage(topic, message));
  27 + }
  28 +
  29 + @Override
  30 + protected void doRegister(String topic, IotMessageBusSubscriber<?> subscriber) {
  31 + // 无需实现,交给 Spring @EventListener 监听
  32 + }
  33 +
  34 + @EventListener
  35 + public void onMessage(LocalIotMessage message) {
  36 + notifySubscribers(message.getTopic(), message.getMessage());
  37 + }
  38 +
  39 +}
0 \ No newline at end of file 40 \ No newline at end of file
yudao-module-iot/yudao-module-iot-core/yudao-module-iot-message-bus/src/main/resources/META-INF/spring.factories 0 → 100644
  1 +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  2 +cn.iocoder.yudao.module.iot.messagebus.config.IotMessageBusAutoConfiguration
0 \ No newline at end of file 3 \ 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 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.messagebus.core.local;
  2 +
  3 +import cn.iocoder.yudao.module.iot.messagebus.config.IotMessageBusAutoConfiguration;
  4 +import cn.iocoder.yudao.module.iot.messagebus.core.IotMessageBus;
  5 +import cn.iocoder.yudao.module.iot.messagebus.core.IotMessageBusSubscriber;
  6 +import jakarta.annotation.Resource;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +import org.junit.jupiter.api.Test;
  9 +import org.springframework.beans.factory.annotation.Autowired;
  10 +import org.springframework.boot.test.context.SpringBootTest;
  11 +import org.springframework.context.annotation.Import;
  12 +import org.springframework.test.context.TestPropertySource;
  13 +
  14 +import java.util.concurrent.CountDownLatch;
  15 +import java.util.concurrent.TimeUnit;
  16 +import java.util.concurrent.atomic.AtomicInteger;
  17 +
  18 +import static org.junit.jupiter.api.Assertions.*;
  19 +
  20 +/**
  21 + * {@link LocalIotMessageBus} 集成测试
  22 + *
  23 + * @author 芋道源码
  24 + */
  25 +@SpringBootTest(classes = LocalIotMessageBusIntegrationTest.class)
  26 +@Import(IotMessageBusAutoConfiguration.class)
  27 +@TestPropertySource(properties = {
  28 + "yudao.iot.message-bus.type=local"
  29 +})
  30 +@Slf4j
  31 +public class LocalIotMessageBusIntegrationTest {
  32 +
  33 + @Resource
  34 + private IotMessageBus messageBus;
  35 +
  36 + /**
  37 + * 1 topic 2 subscriber
  38 + */
  39 + @Test
  40 + public void testSendMessageWithTwoSubscribers() throws InterruptedException {
  41 + // 准备
  42 + String topic = "test-topic";
  43 + String testMessage = "Hello IoT Message Bus!";
  44 + // 用于等待消息处理完成
  45 + CountDownLatch latch = new CountDownLatch(2);
  46 + // 用于记录接收到的消息
  47 + AtomicInteger subscriber1Count = new AtomicInteger(0);
  48 + AtomicInteger subscriber2Count = new AtomicInteger(0);
  49 +
  50 + // 创建第一个订阅者
  51 + IotMessageBusSubscriber<String> subscriber1 = new IotMessageBusSubscriber<>() {
  52 +
  53 + @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();
  60 + }
  61 +
  62 + @Override
  63 + public int order() {
  64 + return 1;
  65 + }
  66 +
  67 + };
  68 + // 创建第二个订阅者
  69 + IotMessageBusSubscriber<String> subscriber2 = new IotMessageBusSubscriber<>() {
  70 +
  71 + @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();
  78 + }
  79 +
  80 + @Override
  81 + public int order() {
  82 + return 0;
  83 + }
  84 +
  85 + };
  86 + // 注册订阅者
  87 + messageBus.register(topic, subscriber1);
  88 + messageBus.register(topic, subscriber2);
  89 +
  90 + // 发送消息
  91 + log.info("[测试] 发送消息 - Topic: {}, Message: {}", topic, testMessage);
  92 + messageBus.post(topic, testMessage);
  93 + // 等待消息处理完成(最多等待5秒)
  94 + boolean completed = latch.await(5, TimeUnit.SECONDS);
  95 +
  96 + // 验证结果
  97 + 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());
  101 + }
  102 +
  103 + /**
  104 + * 2 topic 2 subscriber
  105 + */
  106 + @Test
  107 + public void testMultipleTopics() throws InterruptedException {
  108 + // 准备
  109 + String topic1 = "device-status";
  110 + String topic2 = "device-data";
  111 + String message1 = "设备在线";
  112 + String message2 = "温度:25°C";
  113 + CountDownLatch latch = new CountDownLatch(2);
  114 +
  115 + // 创建订阅者 1 - 只订阅设备状态
  116 + IotMessageBusSubscriber<String> statusSubscriber = new IotMessageBusSubscriber<>() {
  117 +
  118 + @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();
  124 + }
  125 +
  126 + @Override
  127 + public int order() {
  128 + return 0;
  129 + }
  130 +
  131 + };
  132 + // 创建订阅者 2 - 只订阅设备数据
  133 + IotMessageBusSubscriber<String> dataSubscriber = new IotMessageBusSubscriber<>() {
  134 +
  135 + @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();
  141 + }
  142 +
  143 + @Override
  144 + public int order() {
  145 + return 1;
  146 + }
  147 +
  148 + };
  149 + // 注册订阅者到不同主题
  150 + messageBus.register(topic1, statusSubscriber);
  151 + messageBus.register(topic2, dataSubscriber);
  152 +
  153 + // 发送消息到不同主题
  154 + messageBus.post(topic1, message1);
  155 + messageBus.post(topic2, message2);
  156 + // 等待消息处理完成
  157 + boolean completed = latch.await(5, TimeUnit.SECONDS);
  158 + assertTrue(completed, "消息处理超时");
  159 + log.info("[测试] 多主题测试完成");
  160 + }
  161 +
  162 +}
0 \ No newline at end of file 163 \ No newline at end of file
yudao-module-iot/yudao-module-iot-core/yudao-module-iot-message-bus/src/test/resources/application-test.yml 0 → 100644
  1 +yudao:
  2 + iot:
  3 + message-bus:
  4 + type: local
0 \ No newline at end of file 5 \ No newline at end of file