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

This commit is contained in:
YunaiV
2025-06-14 20:53:29 +08:00
parent 69e25eeaac
commit 19cf311b7e
13 changed files with 160 additions and 48 deletions

View File

@@ -1,8 +1,9 @@
package cn.iocoder.yudao.module.iot.core.messagebus.config;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.local.LocalIotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.rocketmq.RocketMQIotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.local.IotLocalMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.redis.IotRedisMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.rocketmq.IotRocketMQMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.autoconfigure.RocketMQProperties;
@@ -14,6 +15,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* IoT 消息总线自动配置
@@ -34,12 +37,12 @@ public class IotMessageBusAutoConfiguration {
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.message-bus", name = "type", havingValue = "local", matchIfMissing = true)
public static class LocalIotMessageBusConfiguration {
public static class IotLocalMessageBusConfiguration {
@Bean
public IotMessageBus localIotMessageBus(ApplicationContext applicationContext) {
log.info("[localIotMessageBus][创建 Local IoT 消息总线]");
return new LocalIotMessageBus(applicationContext);
public IotMessageBus iotLocalMessageBus(ApplicationContext applicationContext) {
log.info("[iotLocalMessageBus][创建 IoT Local 消息总线]");
return new IotLocalMessageBus(applicationContext);
}
}
@@ -49,13 +52,28 @@ public class IotMessageBusAutoConfiguration {
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.message-bus", name = "type", havingValue = "rocketmq")
@ConditionalOnClass(RocketMQTemplate.class)
public static class RocketMQIotMessageBusConfiguration {
public static class IotRocketMQMessageBusConfiguration {
@Bean
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
public IotMessageBus rocketMQIotMessageBus(RocketMQProperties rocketMQProperties, RocketMQTemplate rocketMQTemplate) {
log.info("[rocketMQIotMessageBus][创建 RocketMQ IoT 消息总线]");
return new RocketMQIotMessageBus(rocketMQProperties, rocketMQTemplate);
public IotMessageBus iotRocketMQMessageBus(RocketMQProperties rocketMQProperties,
RocketMQTemplate rocketMQTemplate) {
log.info("[iotRocketMQMessageBus][创建 IoT RocketMQ 消息总线]");
return new IotRocketMQMessageBus(rocketMQProperties, rocketMQTemplate);
}
}
// ==================== Redis 实现 ====================
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.message-bus", name = "type", havingValue = "redis")
@ConditionalOnClass(RedisTemplate.class)
public static class IotRedisMessageBusConfiguration {
@Bean
public IotMessageBus iotRedisMessageBus(StringRedisTemplate redisTemplate) {
log.info("[iotRedisMessageBus][创建 IoT Redis 消息总线]");
return new IotRedisMessageBus(redisTemplate);
}
}

View File

@@ -22,7 +22,7 @@ import java.util.Map;
*/
@RequiredArgsConstructor
@Slf4j
public class LocalIotMessageBus implements IotMessageBus {
public class IotLocalMessageBus implements IotMessageBus {
private final ApplicationContext applicationContext;
@@ -34,7 +34,7 @@ public class LocalIotMessageBus implements IotMessageBus {
@Override
public void post(String topic, Object message) {
applicationContext.publishEvent(new LocalIotMessage(topic, message));
applicationContext.publishEvent(new IotLocalMessage(topic, message));
}
@Override
@@ -48,7 +48,7 @@ public class LocalIotMessageBus implements IotMessageBus {
@EventListener
@SuppressWarnings({"unchecked", "rawtypes"})
public void onMessage(LocalIotMessage message) {
public void onMessage(IotLocalMessage message) {
String topic = message.getTopic();
List<IotMessageSubscriber<?>> topicSubscribers = subscribers.get(topic);
if (CollUtil.isEmpty(topicSubscribers)) {

View File

@@ -0,0 +1,92 @@
package cn.iocoder.yudao.module.iot.core.messagebus.core.redis;
import cn.hutool.core.util.TypeUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
import java.lang.reflect.Type;
import static cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration.buildConsumerName;
import static cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration.checkRedisVersion;
/**
* Redis 的 {@link IotMessageBus} 实现类
*
* @author 芋道源码
*/
@Slf4j
public class IotRedisMessageBus implements IotMessageBus {
private final RedisTemplate<String, ?> redisTemplate;
private final StreamMessageListenerContainer<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer;
public IotRedisMessageBus(RedisTemplate<String, ?> redisTemplate) {
this.redisTemplate = redisTemplate;
checkRedisVersion(redisTemplate);
// 创建 options 配置
StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> containerOptions =
StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder()
.batchSize(10) // 一次性最多拉取多少条消息
.targetType(String.class) // 目标类型。统一使用 String通过自己封装的 AbstractStreamMessageListener 去反序列化
.build();
// 创建 container 对象
this.redisStreamMessageListenerContainer =
StreamMessageListenerContainer.create(redisTemplate.getRequiredConnectionFactory(), containerOptions);
}
@PostConstruct
public void init() {
this.redisStreamMessageListenerContainer.start();
}
@PreDestroy
public void destroy() {
this.redisStreamMessageListenerContainer.stop();
}
@Override
public void post(String topic, Object message) {
redisTemplate.opsForStream().add(StreamRecords.newRecord()
.ofObject(JsonUtils.toJsonString(message)) // 设置内容
.withStreamKey(topic)); // 设置 stream key
}
@Override
public void register(IotMessageSubscriber<?> subscriber) {
Type type = TypeUtil.getTypeArgument(subscriber.getClass(), 0);
if (type == null) {
throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
}
// 创建 listener 对应的消费者分组
try {
redisTemplate.opsForStream().createGroup(subscriber.getTopic(), subscriber.getGroup());
} catch (Exception ignore) {
}
// 创建 Consumer 对象
String consumerName = buildConsumerName();
Consumer consumer = Consumer.from(subscriber.getGroup(), consumerName);
// 设置 Consumer 消费进度,以最小消费进度为准
StreamOffset<String> streamOffset = StreamOffset.create(subscriber.getTopic(), ReadOffset.lastConsumed());
// 设置 Consumer 监听
StreamMessageListenerContainer.StreamReadRequestBuilder<String> builder = StreamMessageListenerContainer.StreamReadRequest
.builder(streamOffset).consumer(consumer)
.autoAcknowledge(false) // 不自动 ack
.cancelOnError(throwable -> false); // 默认配置,发生异常就取消消费,显然不符合预期;因此,我们设置为 false
redisStreamMessageListenerContainer.register(builder.build(), message -> {
// 消费消息
subscriber.onMessage(JsonUtils.parseObject(message.getValue(), type));
// ack 消息消费完成
redisTemplate.opsForStream().acknowledge(subscriber.getGroup(), message);
});
}
}

View File

@@ -4,6 +4,7 @@ import cn.hutool.core.util.TypeUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@@ -15,8 +16,6 @@ import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.autoconfigure.RocketMQProperties;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import jakarta.annotation.PreDestroy;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
@@ -28,7 +27,7 @@ import java.util.List;
*/
@RequiredArgsConstructor
@Slf4j
public class RocketMQIotMessageBus implements IotMessageBus {
public class IotRocketMQMessageBus implements IotMessageBus {
private final RocketMQProperties rocketMQProperties;
@@ -39,6 +38,21 @@ public class RocketMQIotMessageBus implements IotMessageBus {
*/
private final List<DefaultMQPushConsumer> topicConsumers = new ArrayList<>();
/**
* 销毁时关闭所有消费者
*/
@PreDestroy
public void destroy() {
for (DefaultMQPushConsumer consumer : topicConsumers) {
try {
consumer.shutdown();
log.info("[destroy][关闭 group({}) 的消费者成功]", consumer.getConsumerGroup());
} catch (Exception e) {
log.error("[destroy]关闭 group({}) 的消费者异常]", consumer.getConsumerGroup(), e);
}
}
}
@Override
public void post(String topic, Object message) {
// TODO @芋艿需要 orderly
@@ -81,19 +95,4 @@ public class RocketMQIotMessageBus implements IotMessageBus {
topicConsumers.add(consumer);
}
/**
* 销毁时关闭所有消费者
*/
@PreDestroy
public void destroy() {
for (DefaultMQPushConsumer consumer : topicConsumers) {
try {
consumer.shutdown();
log.info("[destroy][关闭 group({}) 的消费者成功]", consumer.getConsumerGroup());
} catch (Exception e) {
log.error("[destroy]关闭 group({}) 的消费者异常]", consumer.getConsumerGroup(), e);
}
}
}
}