Commit 19cf311b7e476076333ab8981afd093445e5213c

Authored by YunaiV
1 parent 69e25eea

feat:【IoT 物联网】增加 redis + event-bus 的实现

Showing 13 changed files with 160 additions and 48 deletions
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java
... ... @@ -69,9 +69,8 @@ public class YudaoRedisMQConsumerAutoConfiguration {
69 69 @ConditionalOnBean(AbstractRedisStreamMessageListener.class) // 只有 AbstractStreamMessageListener 存在的时候,才需要注册 Redis pubsub 监听
70 70 public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractRedisStreamMessageListener<?>> listeners,
71 71 RedisMQTemplate redisTemplate,
72   - @Value("${spring.application.name}") String groupName,
73 72 RedissonClient redissonClient) {
74   - return new RedisPendingMessageResendJob(listeners, redisTemplate, groupName, redissonClient);
  73 + return new RedisPendingMessageResendJob(listeners, redisTemplate, redissonClient);
75 74 }
76 75  
77 76 /**
... ... @@ -141,14 +140,14 @@ public class YudaoRedisMQConsumerAutoConfiguration {
141 140 *
142 141 * @return 消费者名字
143 142 */
144   - private static String buildConsumerName() {
  143 + public static String buildConsumerName() {
145 144 return String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID());
146 145 }
147 146  
148 147 /**
149 148 * 校验 Redis 版本号,是否满足最低的版本号要求!
150 149 */
151   - private static void checkRedisVersion(RedisTemplate<String, ?> redisTemplate) {
  150 + public static void checkRedisVersion(RedisTemplate<String, ?> redisTemplate) {
152 151 // 获得 Redis 版本
153 152 Properties info = redisTemplate.execute((RedisCallback<Properties>) RedisServerCommands::info);
154 153 String version = MapUtil.getStr(info, "redis_version");
... ...
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisPendingMessageResendJob.java
... ... @@ -35,7 +35,6 @@ public class RedisPendingMessageResendJob {
35 35  
36 36 private final List<AbstractRedisStreamMessageListener<?>> listeners;
37 37 private final RedisMQTemplate redisTemplate;
38   - private final String groupName;
39 38 private final RedissonClient redissonClient;
40 39  
41 40 /**
... ... @@ -64,13 +63,13 @@ public class RedisPendingMessageResendJob {
64 63 private void execute() {
65 64 StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream();
66 65 listeners.forEach(listener -> {
67   - PendingMessagesSummary pendingMessagesSummary = Objects.requireNonNull(ops.pending(listener.getStreamKey(), groupName));
  66 + PendingMessagesSummary pendingMessagesSummary = Objects.requireNonNull(ops.pending(listener.getStreamKey(), listener.getGroup()));
68 67 // 每个消费者的 pending 队列消息数量
69 68 Map<String, Long> pendingMessagesPerConsumer = pendingMessagesSummary.getPendingMessagesPerConsumer();
70 69 pendingMessagesPerConsumer.forEach((consumerName, pendingMessageCount) -> {
71 70 log.info("[processPendingMessage][消费者({}) 消息数量({})]", consumerName, pendingMessageCount);
72 71 // 每个消费者的 pending消息的详情信息
73   - PendingMessages pendingMessages = ops.pending(listener.getStreamKey(), Consumer.from(groupName, consumerName), Range.unbounded(), pendingMessageCount);
  72 + PendingMessages pendingMessages = ops.pending(listener.getStreamKey(), Consumer.from(listener.getGroup(), consumerName), Range.unbounded(), pendingMessageCount);
74 73 if (pendingMessages.isEmpty()) {
75 74 return;
76 75 }
... ... @@ -91,7 +90,7 @@ public class RedisPendingMessageResendJob {
91 90 .ofObject(records.get(0).getValue()) // 设置内容
92 91 .withStreamKey(listener.getStreamKey()));
93 92 // ack 消息消费完成
94   - redisTemplate.getRedisTemplate().opsForStream().acknowledge(groupName, records.get(0));
  93 + redisTemplate.getRedisTemplate().opsForStream().acknowledge(listener.getGroup(), records.get(0));
95 94 log.info("[processPendingMessage][消息({})重新投递成功]", records.get(0).getId());
96 95 });
97 96 });
... ...
yudao-module-iot/yudao-module-iot-core/pom.xml
... ... @@ -33,6 +33,11 @@
33 33  
34 34 <!-- 消息中间件相关(可选依赖) -->
35 35 <dependency>
  36 + <groupId>cn.iocoder.boot</groupId>
  37 + <artifactId>yudao-spring-boot-starter-mq</artifactId>
  38 + </dependency>
  39 +
  40 + <dependency>
36 41 <groupId>org.springframework.data</groupId>
37 42 <artifactId>spring-data-redis</artifactId>
38 43 <optional>true</optional>
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/config/IotMessageBusAutoConfiguration.java
1 1 package cn.iocoder.yudao.module.iot.core.messagebus.config;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4   -import cn.iocoder.yudao.module.iot.core.messagebus.core.local.LocalIotMessageBus;
5   -import cn.iocoder.yudao.module.iot.core.messagebus.core.rocketmq.RocketMQIotMessageBus;
  4 +import cn.iocoder.yudao.module.iot.core.messagebus.core.local.IotLocalMessageBus;
  5 +import cn.iocoder.yudao.module.iot.core.messagebus.core.redis.IotRedisMessageBus;
  6 +import cn.iocoder.yudao.module.iot.core.messagebus.core.rocketmq.IotRocketMQMessageBus;
6 7 import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer;
7 8 import lombok.extern.slf4j.Slf4j;
8 9 import org.apache.rocketmq.spring.autoconfigure.RocketMQProperties;
... ... @@ -14,6 +15,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
14 15 import org.springframework.context.ApplicationContext;
15 16 import org.springframework.context.annotation.Bean;
16 17 import org.springframework.context.annotation.Configuration;
  18 +import org.springframework.data.redis.core.RedisTemplate;
  19 +import org.springframework.data.redis.core.StringRedisTemplate;
17 20  
18 21 /**
19 22 * IoT 消息总线自动配置
... ... @@ -34,12 +37,12 @@ public class IotMessageBusAutoConfiguration {
34 37  
35 38 @Configuration
36 39 @ConditionalOnProperty(prefix = "yudao.iot.message-bus", name = "type", havingValue = "local", matchIfMissing = true)
37   - public static class LocalIotMessageBusConfiguration {
  40 + public static class IotLocalMessageBusConfiguration {
38 41  
39 42 @Bean
40   - public IotMessageBus localIotMessageBus(ApplicationContext applicationContext) {
41   - log.info("[localIotMessageBus][创建 Local IoT 消息总线]");
42   - return new LocalIotMessageBus(applicationContext);
  43 + public IotMessageBus iotLocalMessageBus(ApplicationContext applicationContext) {
  44 + log.info("[iotLocalMessageBus][创建 IoT Local 消息总线]");
  45 + return new IotLocalMessageBus(applicationContext);
43 46 }
44 47  
45 48 }
... ... @@ -49,13 +52,28 @@ public class IotMessageBusAutoConfiguration {
49 52 @Configuration
50 53 @ConditionalOnProperty(prefix = "yudao.iot.message-bus", name = "type", havingValue = "rocketmq")
51 54 @ConditionalOnClass(RocketMQTemplate.class)
52   - public static class RocketMQIotMessageBusConfiguration {
  55 + public static class IotRocketMQMessageBusConfiguration {
53 56  
54 57 @Bean
55   - @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
56   - public IotMessageBus rocketMQIotMessageBus(RocketMQProperties rocketMQProperties, RocketMQTemplate rocketMQTemplate) {
57   - log.info("[rocketMQIotMessageBus][创建 RocketMQ IoT 消息总线]");
58   - return new RocketMQIotMessageBus(rocketMQProperties, rocketMQTemplate);
  58 + public IotMessageBus iotRocketMQMessageBus(RocketMQProperties rocketMQProperties,
  59 + RocketMQTemplate rocketMQTemplate) {
  60 + log.info("[iotRocketMQMessageBus][创建 IoT RocketMQ 消息总线]");
  61 + return new IotRocketMQMessageBus(rocketMQProperties, rocketMQTemplate);
  62 + }
  63 +
  64 + }
  65 +
  66 + // ==================== Redis 实现 ====================
  67 +
  68 + @Configuration
  69 + @ConditionalOnProperty(prefix = "yudao.iot.message-bus", name = "type", havingValue = "redis")
  70 + @ConditionalOnClass(RedisTemplate.class)
  71 + public static class IotRedisMessageBusConfiguration {
  72 +
  73 + @Bean
  74 + public IotMessageBus iotRedisMessageBus(StringRedisTemplate redisTemplate) {
  75 + log.info("[iotRedisMessageBus][创建 IoT Redis 消息总线]");
  76 + return new IotRedisMessageBus(redisTemplate);
59 77 }
60 78  
61 79 }
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/local/LocalIotMessage.java renamed to yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/local/IotLocalMessage.java
... ... @@ -5,7 +5,7 @@ import lombok.Data;
5 5  
6 6 @Data
7 7 @AllArgsConstructor
8   -public class LocalIotMessage {
  8 +public class IotLocalMessage {
9 9  
10 10 private String topic;
11 11  
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/local/LocalIotMessageBus.java renamed to yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/local/IotLocalMessageBus.java
... ... @@ -22,7 +22,7 @@ import java.util.Map;
22 22 */
23 23 @RequiredArgsConstructor
24 24 @Slf4j
25   -public class LocalIotMessageBus implements IotMessageBus {
  25 +public class IotLocalMessageBus implements IotMessageBus {
26 26  
27 27 private final ApplicationContext applicationContext;
28 28  
... ... @@ -34,7 +34,7 @@ public class LocalIotMessageBus implements IotMessageBus {
34 34  
35 35 @Override
36 36 public void post(String topic, Object message) {
37   - applicationContext.publishEvent(new LocalIotMessage(topic, message));
  37 + applicationContext.publishEvent(new IotLocalMessage(topic, message));
38 38 }
39 39  
40 40 @Override
... ... @@ -48,7 +48,7 @@ public class LocalIotMessageBus implements IotMessageBus {
48 48  
49 49 @EventListener
50 50 @SuppressWarnings({"unchecked", "rawtypes"})
51   - public void onMessage(LocalIotMessage message) {
  51 + public void onMessage(IotLocalMessage message) {
52 52 String topic = message.getTopic();
53 53 List<IotMessageSubscriber<?>> topicSubscribers = subscribers.get(topic);
54 54 if (CollUtil.isEmpty(topicSubscribers)) {
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/redis/IotRedisMessageBus.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.messagebus.core.redis;
  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.core.messagebus.core.IotMessageBus;
  6 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
  7 +import jakarta.annotation.PostConstruct;
  8 +import jakarta.annotation.PreDestroy;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +import org.springframework.data.redis.connection.stream.*;
  11 +import org.springframework.data.redis.core.RedisTemplate;
  12 +import org.springframework.data.redis.stream.StreamMessageListenerContainer;
  13 +
  14 +import java.lang.reflect.Type;
  15 +
  16 +import static cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration.buildConsumerName;
  17 +import static cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration.checkRedisVersion;
  18 +
  19 +/**
  20 + * Redis 的 {@link IotMessageBus} 实现类
  21 + *
  22 + * @author 芋道源码
  23 + */
  24 +@Slf4j
  25 +public class IotRedisMessageBus implements IotMessageBus {
  26 +
  27 + private final RedisTemplate<String, ?> redisTemplate;
  28 +
  29 + private final StreamMessageListenerContainer<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer;
  30 +
  31 + public IotRedisMessageBus(RedisTemplate<String, ?> redisTemplate) {
  32 + this.redisTemplate = redisTemplate;
  33 + checkRedisVersion(redisTemplate);
  34 + // 创建 options 配置
  35 + StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> containerOptions =
  36 + StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder()
  37 + .batchSize(10) // 一次性最多拉取多少条消息
  38 + .targetType(String.class) // 目标类型。统一使用 String,通过自己封装的 AbstractStreamMessageListener 去反序列化
  39 + .build();
  40 + // 创建 container 对象
  41 + this.redisStreamMessageListenerContainer =
  42 + StreamMessageListenerContainer.create(redisTemplate.getRequiredConnectionFactory(), containerOptions);
  43 + }
  44 +
  45 + @PostConstruct
  46 + public void init() {
  47 + this.redisStreamMessageListenerContainer.start();
  48 + }
  49 +
  50 + @PreDestroy
  51 + public void destroy() {
  52 + this.redisStreamMessageListenerContainer.stop();
  53 + }
  54 +
  55 + @Override
  56 + public void post(String topic, Object message) {
  57 + redisTemplate.opsForStream().add(StreamRecords.newRecord()
  58 + .ofObject(JsonUtils.toJsonString(message)) // 设置内容
  59 + .withStreamKey(topic)); // 设置 stream key
  60 + }
  61 +
  62 + @Override
  63 + public void register(IotMessageSubscriber<?> subscriber) {
  64 + Type type = TypeUtil.getTypeArgument(subscriber.getClass(), 0);
  65 + if (type == null) {
  66 + throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
  67 + }
  68 +
  69 + // 创建 listener 对应的消费者分组
  70 + try {
  71 + redisTemplate.opsForStream().createGroup(subscriber.getTopic(), subscriber.getGroup());
  72 + } catch (Exception ignore) {
  73 + }
  74 + // 创建 Consumer 对象
  75 + String consumerName = buildConsumerName();
  76 + Consumer consumer = Consumer.from(subscriber.getGroup(), consumerName);
  77 + // 设置 Consumer 消费进度,以最小消费进度为准
  78 + StreamOffset<String> streamOffset = StreamOffset.create(subscriber.getTopic(), ReadOffset.lastConsumed());
  79 + // 设置 Consumer 监听
  80 + StreamMessageListenerContainer.StreamReadRequestBuilder<String> builder = StreamMessageListenerContainer.StreamReadRequest
  81 + .builder(streamOffset).consumer(consumer)
  82 + .autoAcknowledge(false) // 不自动 ack
  83 + .cancelOnError(throwable -> false); // 默认配置,发生异常就取消消费,显然不符合预期;因此,我们设置为 false
  84 + redisStreamMessageListenerContainer.register(builder.build(), message -> {
  85 + // 消费消息
  86 + subscriber.onMessage(JsonUtils.parseObject(message.getValue(), type));
  87 + // ack 消息消费完成
  88 + redisTemplate.opsForStream().acknowledge(subscriber.getGroup(), message);
  89 + });
  90 + }
  91 +
  92 +}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/rocketmq/RocketMQIotMessageBus.java renamed to yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/rocketmq/IotRocketMQMessageBus.java
... ... @@ -4,6 +4,7 @@ import cn.hutool.core.util.TypeUtil;
4 4 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
5 5 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
6 6 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
  7 +import jakarta.annotation.PreDestroy;
7 8 import lombok.RequiredArgsConstructor;
8 9 import lombok.SneakyThrows;
9 10 import lombok.extern.slf4j.Slf4j;
... ... @@ -15,8 +16,6 @@ import org.apache.rocketmq.common.message.MessageExt;
15 16 import org.apache.rocketmq.spring.autoconfigure.RocketMQProperties;
16 17 import org.apache.rocketmq.spring.core.RocketMQTemplate;
17 18  
18   -import jakarta.annotation.PreDestroy;
19   -
20 19 import java.lang.reflect.Type;
21 20 import java.util.ArrayList;
22 21 import java.util.List;
... ... @@ -28,7 +27,7 @@ import java.util.List;
28 27 */
29 28 @RequiredArgsConstructor
30 29 @Slf4j
31   -public class RocketMQIotMessageBus implements IotMessageBus {
  30 +public class IotRocketMQMessageBus implements IotMessageBus {
32 31  
33 32 private final RocketMQProperties rocketMQProperties;
34 33  
... ... @@ -39,6 +38,21 @@ public class RocketMQIotMessageBus implements IotMessageBus {
39 38 */
40 39 private final List<DefaultMQPushConsumer> topicConsumers = new ArrayList<>();
41 40  
  41 + /**
  42 + * 销毁时关闭所有消费者
  43 + */
  44 + @PreDestroy
  45 + public void destroy() {
  46 + for (DefaultMQPushConsumer consumer : topicConsumers) {
  47 + try {
  48 + consumer.shutdown();
  49 + log.info("[destroy][关闭 group({}) 的消费者成功]", consumer.getConsumerGroup());
  50 + } catch (Exception e) {
  51 + log.error("[destroy]关闭 group({}) 的消费者异常]", consumer.getConsumerGroup(), e);
  52 + }
  53 + }
  54 + }
  55 +
42 56 @Override
43 57 public void post(String topic, Object message) {
44 58 // TODO @芋艿:需要 orderly!
... ... @@ -81,19 +95,4 @@ public class RocketMQIotMessageBus implements IotMessageBus {
81 95 topicConsumers.add(consumer);
82 96 }
83 97  
84   - /**
85   - * 销毁时关闭所有消费者
86   - */
87   - @PreDestroy
88   - public void destroy() {
89   - for (DefaultMQPushConsumer consumer : topicConsumers) {
90   - try {
91   - consumer.shutdown();
92   - log.info("[destroy][关闭 group({}) 的消费者成功]", consumer.getConsumerGroup());
93   - } catch (Exception e) {
94   - log.error("[destroy]关闭 group({}) 的消费者异常]", consumer.getConsumerGroup(), e);
95   - }
96   - }
97   - }
98   -
99 98 }
100 99 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/messagebus/core/local/LocalIotMessageBusIntegrationTest.java
... ... @@ -17,7 +17,7 @@ import java.util.concurrent.atomic.AtomicInteger;
17 17 import static org.junit.jupiter.api.Assertions.*;
18 18  
19 19 /**
20   - * {@link LocalIotMessageBus} 集成测试
  20 + * {@link IotLocalMessageBus} 集成测试
21 21 *
22 22 * @author 芋道源码
23 23 */
... ...
yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/messagebus/core/rocketmq/RocketMQIotMessageBusTest.java
... ... @@ -21,7 +21,7 @@ import java.util.concurrent.atomic.AtomicReference;
21 21 import static org.junit.jupiter.api.Assertions.*;
22 22  
23 23 /**
24   - * {@link RocketMQIotMessageBus} 集成测试
  24 + * {@link IotRocketMQMessageBus} 集成测试
25 25 *
26 26 * @author 芋道源码
27 27 */
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application-local.yaml
... ... @@ -31,7 +31,7 @@ yudao:
31 31 # 针对引入的 EMQX 组件的配置
32 32 # ====================================
33 33 emqx:
34   - enabled: true
  34 + enabled: false
35 35 http-port: 8090 # MQTT HTTP 服务端口
36 36 mqtt-host: 127.0.0.1 # MQTT Broker 地址
37 37 mqtt-port: 1883 # MQTT Broker 端口
... ... @@ -44,7 +44,7 @@ yudao:
44 44  
45 45 # 消息总线配置
46 46 message-bus:
47   - type: rocketmq # 本地开发使用 RocketMQ
  47 + type: redis # 本地开发使用 RocketMQ
48 48  
49 49 --- #################### 日志相关配置 ####################
50 50  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
... ... @@ -37,14 +37,14 @@ yudao:
37 37 # 针对引入的 EMQX 组件的配置
38 38 # ====================================
39 39 emqx:
40   - enabled: true
  40 + enabled: false
41 41 mqtt-ssl: false
42 42 mqtt-topics:
43 43 - "/sys/#" # 系统主题
44 44  
45 45 # 消息总线配置
46 46 message-bus:
47   - type: rocketmq # 消息总线的类型
  47 + type: redis # 消息总线的类型
48 48  
49 49 --- #################### 日志相关配置 ####################
50 50  
... ...
yudao-server/src/main/resources/application.yaml
... ... @@ -313,6 +313,6 @@ yudao:
313 313 customer: E77DF18BE109F454A5CD319E44BF5177
314 314 iot:
315 315 message-bus:
316   - type: rocketmq # 消息总线的类型
  316 + type: redis # 消息总线的类型
317 317  
318 318 debug: false
319 319 \ No newline at end of file
... ...