Commit 6a1798bf6acc114e2f4119d7ce6bd5531cc082a3

Authored by 芋道源码
Committed by Gitee
2 parents 53693529 cb16539b

!1255 【功能新增】IoT: 新增 Kafka、RabbitMQ、RedisStreamMQ 数据桥梁实现

Merge pull request !1255 from puhui999/iot
yudao-dependencies/pom.xml
... ... @@ -35,6 +35,8 @@
35 35 <taos.version>3.3.3</taos.version>
36 36 <!-- 消息队列 -->
37 37 <rocketmq-spring.version>2.3.2</rocketmq-spring.version>
  38 + <kafka-spring.version>3.3.3</kafka-spring.version>
  39 + <rabbitmq-spring.version>3.4.3</rabbitmq-spring.version>
38 40 <!-- 服务保障相关 -->
39 41 <lock4j.version>2.2.7</lock4j.version>
40 42 <!-- 监控相关 -->
... ... @@ -285,12 +287,21 @@
285 287 <artifactId>yudao-spring-boot-starter-mq</artifactId>
286 288 <version>${revision}</version>
287 289 </dependency>
288   -
289 290 <dependency>
290 291 <groupId>org.apache.rocketmq</groupId>
291 292 <artifactId>rocketmq-spring-boot-starter</artifactId>
292 293 <version>${rocketmq-spring.version}</version>
293 294 </dependency>
  295 + <dependency>
  296 + <groupId>org.springframework.kafka</groupId>
  297 + <artifactId>spring-kafka</artifactId>
  298 + <version>${kafka-spring.version}</version>
  299 + </dependency>
  300 + <dependency>
  301 + <groupId>org.springframework.boot</groupId>
  302 + <artifactId>spring-boot-starter-amqp</artifactId>
  303 + <version>${rabbitmq-spring.version}</version>
  304 + </dependency>
294 305  
295 306 <!-- 服务保障相关 -->
296 307 <dependency>
... ...
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataBridgTypeEnum.java
... ... @@ -22,7 +22,7 @@ public enum IotDataBridgTypeEnum implements ArrayValuable&lt;Integer&gt; {
22 22 MQTT(10),
23 23  
24 24 DATABASE(20),
25   - REDIS(21),
  25 + REDIS_STREAM(21),
26 26  
27 27 ROCKETMQ(30),
28 28 RABBITMQ(31),
... ...
yudao-module-iot/yudao-module-iot-biz/pom.xml
... ... @@ -81,6 +81,16 @@
81 81 <artifactId>rocketmq-spring-boot-starter</artifactId>
82 82 <optional>true</optional>
83 83 </dependency>
  84 + <dependency>
  85 + <groupId>org.springframework.kafka</groupId>
  86 + <artifactId>spring-kafka</artifactId>
  87 + <optional>true</optional>
  88 + </dependency>
  89 + <dependency>
  90 + <groupId>org.springframework.boot</groupId>
  91 + <artifactId>spring-boot-starter-amqp</artifactId>
  92 + <optional>true</optional>
  93 + </dependency>
84 94  
85 95 <dependency>
86 96 <groupId>org.pf4j</groupId> <!-- PF4J:内置插件机制 -->
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotDataBridgeDO.java
... ... @@ -170,4 +170,104 @@ public class IotDataBridgeDO extends BaseDO {
170 170  
171 171 }
172 172  
  173 + /**
  174 + * Kafka 配置
  175 + */
  176 + @Data
  177 + public static class KafkaMQConfig implements Config {
  178 +
  179 + /**
  180 + * Kafka 服务器地址
  181 + */
  182 + private String bootstrapServers;
  183 + /**
  184 + * 用户名
  185 + */
  186 + private String username;
  187 + /**
  188 + * 密码
  189 + */
  190 + private String password;
  191 + /**
  192 + * 是否启用 SSL
  193 + */
  194 + private Boolean ssl;
  195 +
  196 + /**
  197 + * 主题
  198 + */
  199 + private String topic;
  200 +
  201 + }
  202 +
  203 + /**
  204 + * RabbitMQ 配置
  205 + */
  206 + @Data
  207 + public static class RabbitMQConfig implements Config {
  208 +
  209 + /**
  210 + * RabbitMQ 服务器地址
  211 + */
  212 + private String host;
  213 + /**
  214 + * 端口
  215 + */
  216 + private Integer port;
  217 + /**
  218 + * 虚拟主机
  219 + */
  220 + private String virtualHost;
  221 + /**
  222 + * 用户名
  223 + */
  224 + private String username;
  225 + /**
  226 + * 密码
  227 + */
  228 + private String password;
  229 +
  230 + /**
  231 + * 交换机名称
  232 + */
  233 + private String exchange;
  234 + /**
  235 + * 路由键
  236 + */
  237 + private String routingKey;
  238 + /**
  239 + * 队列名称
  240 + */
  241 + private String queue;
  242 + }
  243 +
  244 + /**
  245 + * Redis Stream MQ 配置
  246 + */
  247 + @Data
  248 + public static class RedisStreamMQConfig implements Config {
  249 +
  250 + /**
  251 + * Redis 服务器地址
  252 + */
  253 + private String host;
  254 + /**
  255 + * 端口
  256 + */
  257 + private Integer port;
  258 + /**
  259 + * 密码
  260 + */
  261 + private String password;
  262 + /**
  263 + * 数据库索引
  264 + */
  265 + private Integer database;
  266 +
  267 + /**
  268 + * 主题
  269 + */
  270 + private String topic;
  271 + }
  272 +
173 273 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/AbstractCacheableDataBridgeExecute.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.action.databridge;
  2 +
  3 +import com.google.common.cache.CacheBuilder;
  4 +import com.google.common.cache.CacheLoader;
  5 +import com.google.common.cache.LoadingCache;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +
  8 +import java.time.Duration;
  9 +
  10 +// TODO @芋艿:因为下面的,都是有状态的,所以通过 guava 缓存连接,然后通过 RemovalNotification 实现关闭。例如说,一次新建有效期是 10 分钟;
  11 +// TODO @芋艿:mq-redis
  12 +// TODO @芋艿:mq-数据库
  13 +// TODO @芋艿:kafka
  14 +// TODO @芋艿:rocketmq
  15 +// TODO @芋艿:rabbitmq
  16 +// TODO @芋艿:mqtt
  17 +// TODO @芋艿:tcp
  18 +// TODO @芋艿:websocket
  19 +
  20 +/**
  21 + * 带缓存功能的数据桥梁执行器抽象类
  22 + *
  23 + * @author HUIHUI
  24 + */
  25 +@Slf4j
  26 +public abstract class AbstractCacheableDataBridgeExecute implements IotDataBridgeExecute {
  27 +
  28 + /**
  29 + * Producer 缓存
  30 + */
  31 + private final LoadingCache<Object, Object> PRODUCER_CACHE = CacheBuilder.newBuilder()
  32 + .expireAfterAccess(Duration.ofMinutes(30))
  33 + .removalListener(notification -> {
  34 + Object producer = notification.getValue();
  35 + if (producer == null) {
  36 + return;
  37 + }
  38 + try {
  39 + closeProducer(producer);
  40 + log.info("[PRODUCER_CACHE][配置({}) 对应的 producer 已关闭]", notification.getKey());
  41 + } catch (Exception e) {
  42 + log.error("[PRODUCER_CACHE][配置({}) 对应的 producer 关闭失败]", notification.getKey(), e);
  43 + }
  44 + })
  45 + .build(new CacheLoader<Object, Object>() {
  46 + @Override
  47 + public Object load(Object config) throws Exception {
  48 + Object producer = initProducer(config);
  49 + log.info("[PRODUCER_CACHE][配置({}) 对应的 producer 已创建并启动]", config);
  50 + return producer;
  51 + }
  52 + });
  53 +
  54 + /**
  55 + * 获取生产者
  56 + *
  57 + * @param config 配置信息
  58 + * @return 生产者对象
  59 + */
  60 + protected Object getProducer(Object config) throws Exception {
  61 + return PRODUCER_CACHE.get(config);
  62 + }
  63 +
  64 + /**
  65 + * 初始化生产者
  66 + *
  67 + * @param config 配置信息
  68 + * @return 生产者对象
  69 + * @throws Exception 如果初始化失败
  70 + */
  71 + protected abstract Object initProducer(Object config) throws Exception;
  72 +
  73 + /**
  74 + * 关闭生产者
  75 + *
  76 + * @param producer 生产者对象
  77 + */
  78 + protected abstract void closeProducer(Object producer);
  79 +
  80 +}
0 81 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecute.java
... ... @@ -12,21 +12,11 @@ import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage;
12 12 public interface IotDataBridgeExecute {
13 13  
14 14 /**
15   - * 执行数据桥接操作
  15 + * 执行数据桥梁操作
16 16 *
17 17 * @param message 设备消息
18 18 * @param dataBridge 数据桥梁
19 19 */
20 20 void execute(IotDeviceMessage message, IotDataBridgeDO dataBridge);
21 21  
22   - // TODO @芋艿:因为下面的,都是有状态的,所以通过 guava 缓存连接,然后通过 RemovalNotification 实现关闭。例如说,一次新建有效期是 10 分钟;
23   - // TODO @芋艿:mq-redis
24   - // TODO @芋艿:mq-数据库
25   - // TODO @芋艿:kafka
26   - // TODO @芋艿:rocketmq
27   - // TODO @芋艿:rabbitmq
28   - // TODO @芋艿:mqtt
29   - // TODO @芋艿:tcp
30   - // TODO @芋艿:websocket
31   -
32 22 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotHttpDataBridgeExecute.java
... ... @@ -32,7 +32,7 @@ public class IotHttpDataBridgeExecute implements IotDataBridgeExecute {
32 32  
33 33 @Override
34 34 public void execute(IotDeviceMessage message, IotDataBridgeDO dataBridge) {
35   - // 1.1 校验数据桥接的类型 == HTTP
  35 + // 1.1 校验数据桥梁的类型 == HTTP
36 36 if (!IotDataBridgTypeEnum.HTTP.getType().equals(dataBridge.getType())) {
37 37 return;
38 38 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotKafkaMQDataBridgeExecute.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.action.databridge;
  2 +
  3 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO;
  4 +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgTypeEnum;
  5 +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +import org.apache.kafka.clients.producer.ProducerConfig;
  8 +import org.apache.kafka.common.serialization.StringSerializer;
  9 +import org.springframework.kafka.core.DefaultKafkaProducerFactory;
  10 +import org.springframework.kafka.core.KafkaTemplate;
  11 +import org.springframework.stereotype.Component;
  12 +
  13 +import java.time.LocalDateTime;
  14 +import java.util.HashMap;
  15 +import java.util.Map;
  16 +import java.util.concurrent.TimeUnit;
  17 +import java.util.concurrent.TimeoutException;
  18 +
  19 +/**
  20 + * Kafka 的 {@link IotDataBridgeExecute} 实现类
  21 + *
  22 + * @author HUIHUI
  23 + */
  24 +@Component
  25 +@Slf4j
  26 +public class IotKafkaMQDataBridgeExecute extends AbstractCacheableDataBridgeExecute {
  27 +
  28 + @Override
  29 + public void execute(IotDeviceMessage message, IotDataBridgeDO dataBridge) {
  30 + // 1.1 校验数据桥梁的类型 == KAFKA
  31 + if (!IotDataBridgTypeEnum.KAFKA.getType().equals(dataBridge.getType())) {
  32 + return;
  33 + }
  34 + // 1.2 执行 Kafka 发送消息
  35 + executeKafka(message, (IotDataBridgeDO.KafkaMQConfig) dataBridge.getConfig());
  36 + }
  37 +
  38 + @SuppressWarnings("unchecked")
  39 + private void executeKafka(IotDeviceMessage message, IotDataBridgeDO.KafkaMQConfig config) {
  40 + try {
  41 + // 1. 获取或创建 KafkaTemplate
  42 + KafkaTemplate<String, String> kafkaTemplate = (KafkaTemplate<String, String>) getProducer(config);
  43 +
  44 + // 2. 发送消息并等待结果
  45 + kafkaTemplate.send(config.getTopic(), message.toString())
  46 + .get(10, TimeUnit.SECONDS); // 添加超时等待
  47 + log.info("[executeKafka][message({}) 发送成功]", message);
  48 + } catch (TimeoutException e) {
  49 + log.error("[executeKafka][message({}) config({}) 发送超时]", message, config, e);
  50 + } catch (Exception e) {
  51 + log.error("[executeKafka][message({}) config({}) 发送异常]", message, config, e);
  52 + }
  53 + }
  54 +
  55 + @Override
  56 + protected Object initProducer(Object config) {
  57 + IotDataBridgeDO.KafkaMQConfig kafkaConfig = (IotDataBridgeDO.KafkaMQConfig) config;
  58 +
  59 + // 1.1 构建生产者配置
  60 + Map<String, Object> props = new HashMap<>();
  61 + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaConfig.getBootstrapServers());
  62 + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
  63 + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
  64 +
  65 + // 1.2 如果配置了认证信息
  66 + if (kafkaConfig.getUsername() != null && kafkaConfig.getPassword() != null) {
  67 + props.put("security.protocol", "SASL_PLAINTEXT");
  68 + props.put("sasl.mechanism", "PLAIN");
  69 + props.put("sasl.jaas.config",
  70 + "org.apache.kafka.common.security.plain.PlainLoginModule required username=\""
  71 + + kafkaConfig.getUsername() + "\" password=\"" + kafkaConfig.getPassword() + "\";");
  72 + }
  73 +
  74 + // 1.3 如果启用 SSL
  75 + if (Boolean.TRUE.equals(kafkaConfig.getSsl())) {
  76 + props.put("security.protocol", "SSL");
  77 + }
  78 +
  79 + // 2. 创建 KafkaTemplate
  80 + DefaultKafkaProducerFactory<String, String> producerFactory = new DefaultKafkaProducerFactory<>(props);
  81 + return new KafkaTemplate<>(producerFactory);
  82 + }
  83 +
  84 + @Override
  85 + protected void closeProducer(Object producer) {
  86 + if (producer instanceof KafkaTemplate) {
  87 + ((KafkaTemplate<?, ?>) producer).destroy();
  88 + }
  89 + }
  90 +
  91 + // TODO @芋艿:测试代码,后续清理
  92 + public static void main(String[] args) {
  93 + // 1. 创建一个共享的实例
  94 + IotKafkaMQDataBridgeExecute action = new IotKafkaMQDataBridgeExecute();
  95 +
  96 + // 2. 创建共享的配置
  97 + IotDataBridgeDO.KafkaMQConfig config = new IotDataBridgeDO.KafkaMQConfig();
  98 + config.setBootstrapServers("127.0.0.1:9092");
  99 + config.setTopic("test-topic");
  100 + config.setSsl(false);
  101 + config.setUsername(null);
  102 + config.setPassword(null);
  103 +
  104 + // 3. 创建共享的消息
  105 + IotDeviceMessage message = IotDeviceMessage.builder()
  106 + .requestId("TEST-001")
  107 + .productKey("testProduct")
  108 + .deviceName("testDevice")
  109 + .deviceKey("testDeviceKey")
  110 + .type("property")
  111 + .identifier("temperature")
  112 + .data("{\"value\": 60}")
  113 + .reportTime(LocalDateTime.now())
  114 + .tenantId(1L)
  115 + .build();
  116 +
  117 + // 4. 执行两次测试,验证缓存
  118 + log.info("[main][第一次执行,应该会创建新的 producer]");
  119 + action.executeKafka(message, config);
  120 +
  121 + log.info("[main][第二次执行,应该会复用缓存的 producer]");
  122 + action.executeKafka(message, config);
  123 + }
  124 +
  125 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRabbitMQDataBridgeExecute.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.action.databridge;
  2 +
  3 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO;
  4 +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgTypeEnum;
  5 +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage;
  6 +import com.rabbitmq.client.Channel;
  7 +import com.rabbitmq.client.Connection;
  8 +import com.rabbitmq.client.ConnectionFactory;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +import org.springframework.stereotype.Component;
  11 +
  12 +import java.nio.charset.StandardCharsets;
  13 +import java.time.LocalDateTime;
  14 +
  15 +/**
  16 + * RabbitMQ 的 {@link IotDataBridgeExecute} 实现类
  17 + *
  18 + * @author HUIHUI
  19 + */
  20 +@Component
  21 +@Slf4j
  22 +public class IotRabbitMQDataBridgeExecute extends AbstractCacheableDataBridgeExecute {
  23 +
  24 + @Override
  25 + public void execute(IotDeviceMessage message, IotDataBridgeDO dataBridge) {
  26 + // 1.1 校验数据桥梁的类型 == RABBITMQ
  27 + if (!IotDataBridgTypeEnum.RABBITMQ.getType().equals(dataBridge.getType())) {
  28 + return;
  29 + }
  30 + // 1.2 执行 RabbitMQ 发送消息
  31 + executeRabbitMQ(message, (IotDataBridgeDO.RabbitMQConfig) dataBridge.getConfig());
  32 + }
  33 +
  34 + private void executeRabbitMQ(IotDeviceMessage message, IotDataBridgeDO.RabbitMQConfig config) {
  35 + try {
  36 + // 1. 获取或创建 Channel
  37 + Channel channel = (Channel) getProducer(config);
  38 +
  39 + // 2.1 声明交换机、队列和绑定关系
  40 + channel.exchangeDeclare(config.getExchange(), "direct", true);
  41 + channel.queueDeclare(config.getQueue(), true, false, false, null);
  42 + channel.queueBind(config.getQueue(), config.getExchange(), config.getRoutingKey());
  43 +
  44 + // 2.2 发送消息
  45 + channel.basicPublish(config.getExchange(), config.getRoutingKey(), null,
  46 + message.toString().getBytes(StandardCharsets.UTF_8));
  47 + log.info("[executeRabbitMQ][message({}) config({}) 发送成功]", message, config);
  48 + } catch (Exception e) {
  49 + log.error("[executeRabbitMQ][message({}) config({}) 发送异常]", message, config, e);
  50 + }
  51 + }
  52 +
  53 + @Override
  54 + protected Object initProducer(Object config) throws Exception {
  55 + IotDataBridgeDO.RabbitMQConfig rabbitConfig = (IotDataBridgeDO.RabbitMQConfig) config;
  56 +
  57 + // 1. 创建连接工厂
  58 + ConnectionFactory factory = new ConnectionFactory();
  59 + factory.setHost(rabbitConfig.getHost());
  60 + factory.setPort(rabbitConfig.getPort());
  61 + factory.setVirtualHost(rabbitConfig.getVirtualHost());
  62 + factory.setUsername(rabbitConfig.getUsername());
  63 + factory.setPassword(rabbitConfig.getPassword());
  64 +
  65 + // 2. 创建连接
  66 + Connection connection = factory.newConnection();
  67 +
  68 + // 3. 创建信道
  69 + return connection.createChannel();
  70 + }
  71 +
  72 + @Override
  73 + protected void closeProducer(Object producer) {
  74 + if (producer instanceof Channel) {
  75 + try {
  76 + Channel channel = (Channel) producer;
  77 + if (channel.isOpen()) {
  78 + channel.close();
  79 + }
  80 + Connection connection = channel.getConnection();
  81 + if (connection.isOpen()) {
  82 + connection.close();
  83 + }
  84 + } catch (Exception e) {
  85 + log.error("[closeProducer][关闭 RabbitMQ 连接异常]", e);
  86 + }
  87 + }
  88 + }
  89 +
  90 + // TODO @芋艿:测试代码,后续清理
  91 + public static void main(String[] args) {
  92 + // 1. 创建一个共享的实例
  93 + IotRabbitMQDataBridgeExecute action = new IotRabbitMQDataBridgeExecute();
  94 +
  95 + // 2. 创建共享的配置
  96 + IotDataBridgeDO.RabbitMQConfig config = new IotDataBridgeDO.RabbitMQConfig();
  97 + config.setHost("localhost");
  98 + config.setPort(5672);
  99 + config.setVirtualHost("/");
  100 + config.setUsername("admin");
  101 + config.setPassword("123456");
  102 + config.setExchange("test-exchange");
  103 + config.setRoutingKey("test-key");
  104 + config.setQueue("test-queue");
  105 +
  106 + // 3. 创建共享的消息
  107 + IotDeviceMessage message = IotDeviceMessage.builder()
  108 + .requestId("TEST-001")
  109 + .productKey("testProduct")
  110 + .deviceName("testDevice")
  111 + .deviceKey("testDeviceKey")
  112 + .type("property")
  113 + .identifier("temperature")
  114 + .data("{\"value\": 60}")
  115 + .reportTime(LocalDateTime.now())
  116 + .tenantId(1L)
  117 + .build();
  118 +
  119 + // 4. 执行两次测试,验证缓存
  120 + log.info("[main][第一次执行,应该会创建新的 channel]");
  121 + action.executeRabbitMQ(message, config);
  122 +
  123 + log.info("[main][第二次执行,应该会复用缓存的 channel]");
  124 + action.executeRabbitMQ(message, config);
  125 + }
  126 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRedisStreamMQDataBridgeExecute.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.action.databridge;
  2 +
  3 +import cn.hutool.core.util.ReflectUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO;
  6 +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgTypeEnum;
  7 +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage;
  8 +import com.fasterxml.jackson.databind.ObjectMapper;
  9 +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
  10 +import lombok.extern.slf4j.Slf4j;
  11 +import org.redisson.Redisson;
  12 +import org.redisson.api.RedissonClient;
  13 +import org.redisson.config.Config;
  14 +import org.redisson.config.SingleServerConfig;
  15 +import org.redisson.spring.data.connection.RedissonConnectionFactory;
  16 +import org.springframework.data.redis.connection.RedisConnectionFactory;
  17 +import org.springframework.data.redis.connection.stream.ObjectRecord;
  18 +import org.springframework.data.redis.connection.stream.StreamRecords;
  19 +import org.springframework.data.redis.core.RedisTemplate;
  20 +import org.springframework.data.redis.serializer.RedisSerializer;
  21 +import org.springframework.stereotype.Component;
  22 +
  23 +import java.time.LocalDateTime;
  24 +
  25 +/**
  26 + * Redis Stream MQ 的 {@link IotDataBridgeExecute} 实现类
  27 + *
  28 + * @author HUIHUI
  29 + */
  30 +@Component
  31 +@Slf4j
  32 +public class IotRedisStreamMQDataBridgeExecute extends AbstractCacheableDataBridgeExecute {
  33 +
  34 + @Override
  35 + public void execute(IotDeviceMessage message, IotDataBridgeDO dataBridge) {
  36 + // 1.1 校验数据桥梁类型
  37 + if (!IotDataBridgTypeEnum.REDIS_STREAM.getType().equals(dataBridge.getType())) {
  38 + return;
  39 + }
  40 + // 1.2 执行消息发送
  41 + executeRedisStream(message, (IotDataBridgeDO.RedisStreamMQConfig) dataBridge.getConfig());
  42 + }
  43 +
  44 + @SuppressWarnings("unchecked")
  45 + private void executeRedisStream(IotDeviceMessage message, IotDataBridgeDO.RedisStreamMQConfig config) {
  46 + try {
  47 + // 1. 获取 RedisTemplate
  48 + RedisTemplate<String, Object> redisTemplate = (RedisTemplate<String, Object>) getProducer(config);
  49 +
  50 + // 2. 创建并发送 Stream 记录
  51 + ObjectRecord<String, IotDeviceMessage> record = StreamRecords.newRecord()
  52 + .ofObject(message).withStreamKey(config.getTopic());
  53 + String recordId = String.valueOf(redisTemplate.opsForStream().add(record));
  54 + log.info("[executeRedisStream][消息发送成功] messageId: {}, config: {}", recordId, config);
  55 + } catch (Exception e) {
  56 + log.error("[executeRedisStream][消息发送失败] message: {}, config: {}", message, config, e);
  57 + }
  58 + }
  59 +
  60 + @Override
  61 + protected Object initProducer(Object config) {
  62 + IotDataBridgeDO.RedisStreamMQConfig redisConfig = (IotDataBridgeDO.RedisStreamMQConfig) config;
  63 +
  64 + // 1.1 创建 Redisson 配置
  65 + Config redissonConfig = new Config();
  66 + SingleServerConfig serverConfig = redissonConfig.useSingleServer()
  67 + .setAddress("redis://" + redisConfig.getHost() + ":" + redisConfig.getPort())
  68 + .setDatabase(redisConfig.getDatabase());
  69 + // 1.2 设置密码(如果有)
  70 + if (StrUtil.isNotBlank(redisConfig.getPassword())) {
  71 + serverConfig.setPassword(redisConfig.getPassword());
  72 + }
  73 +
  74 + // 2.1 创建 RedissonClient
  75 + RedissonClient redisson = Redisson.create(redissonConfig);
  76 + // 2.2 创建并配置 RedisTemplate
  77 + RedisTemplate<String, Object> template = new RedisTemplate<>();
  78 + // 设置 RedisConnection 工厂。😈 它就是实现多种 Java Redis 客户端接入的秘密工厂。感兴趣的胖友,可以自己去撸下。
  79 + template.setConnectionFactory(new RedissonConnectionFactory(redisson));
  80 + // 使用 String 序列化方式,序列化 KEY 。
  81 + template.setKeySerializer(RedisSerializer.string());
  82 + template.setHashKeySerializer(RedisSerializer.string());
  83 + // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
  84 + template.setValueSerializer(buildRedisSerializer());
  85 + template.setHashValueSerializer(buildRedisSerializer());
  86 + template.afterPropertiesSet();// 初始化
  87 + return template;
  88 + }
  89 +
  90 + @Override
  91 + protected void closeProducer(Object producer) {
  92 + if (producer instanceof RedisTemplate) {
  93 + RedisConnectionFactory factory = ((RedisTemplate<?, ?>) producer).getConnectionFactory();
  94 + try {
  95 + if (factory != null) {
  96 + ((RedissonConnectionFactory) factory).destroy();
  97 + }
  98 + } catch (Exception e) {
  99 + log.error("[closeProducer][关闭 redisson 连接异常]", e);
  100 + }
  101 + }
  102 + }
  103 +
  104 +
  105 + public static RedisSerializer<?> buildRedisSerializer() {
  106 + RedisSerializer<Object> json = RedisSerializer.json();
  107 + // 解决 LocalDateTime 的序列化
  108 + ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(json, "mapper");
  109 + objectMapper.registerModules(new JavaTimeModule());
  110 + return json;
  111 + }
  112 +
  113 + // TODO @芋艿:测试代码,后续清理
  114 + public static void main(String[] args) {
  115 + // 1. 创建一个共享的实例
  116 + IotRedisStreamMQDataBridgeExecute action = new IotRedisStreamMQDataBridgeExecute();
  117 +
  118 + // 2. 创建共享的配置
  119 + IotDataBridgeDO.RedisStreamMQConfig config = new IotDataBridgeDO.RedisStreamMQConfig();
  120 + config.setHost("127.0.0.1");
  121 + config.setPort(6379);
  122 + config.setDatabase(0);
  123 + config.setPassword("123456");
  124 + config.setTopic("test-stream");
  125 +
  126 + // 3. 创建共享的消息
  127 + IotDeviceMessage message = IotDeviceMessage.builder()
  128 + .requestId("TEST-001")
  129 + .productKey("testProduct")
  130 + .deviceName("testDevice")
  131 + .deviceKey("testDeviceKey")
  132 + .type("property")
  133 + .identifier("temperature")
  134 + .data("{\"value\": 60}")
  135 + .reportTime(LocalDateTime.now())
  136 + .tenantId(1L)
  137 + .build();
  138 +
  139 + // 4. 执行两次测试,验证缓存
  140 + log.info("[main][第一次执行,应该会创建新的 RedisTemplate]");
  141 + action.executeRedisStream(message, config);
  142 +
  143 + log.info("[main][第二次执行,应该会复用缓存的 RedisTemplate]");
  144 + action.executeRedisStream(message, config);
  145 + }
  146 +
  147 +}
0 148 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRocketMQDataBridgeExecute.java
... ... @@ -3,9 +3,6 @@ package cn.iocoder.yudao.module.iot.service.rule.action.databridge;
3 3 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO;
4 4 import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgTypeEnum;
5 5 import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage;
6   -import com.google.common.cache.CacheBuilder;
7   -import com.google.common.cache.CacheLoader;
8   -import com.google.common.cache.LoadingCache;
9 6 import lombok.extern.slf4j.Slf4j;
10 7 import org.apache.rocketmq.client.producer.DefaultMQProducer;
11 8 import org.apache.rocketmq.client.producer.SendResult;
... ... @@ -14,9 +11,7 @@ import org.apache.rocketmq.common.message.Message;
14 11 import org.apache.rocketmq.remoting.common.RemotingHelper;
15 12 import org.springframework.stereotype.Component;
16 13  
17   -import java.time.Duration;
18 14 import java.time.LocalDateTime;
19   -import java.util.concurrent.Executors;
20 15  
21 16 /**
22 17 * RocketMQ 的 {@link IotDataBridgeExecute} 实现类
... ... @@ -25,45 +20,11 @@ import java.util.concurrent.Executors;
25 20 */
26 21 @Component
27 22 @Slf4j
28   -public class IotRocketMQDataBridgeExecute implements IotDataBridgeExecute {
29   -
30   - /**
31   - * 针对 {@link IotDataBridgeDO.RocketMQConfig} 的 DefaultMQProducer 缓存
32   - */
33   - // TODO @puhui999:因为 kafka 之类也存在这个情况,是不是得搞个抽象类。提供一个 initProducer,和 closeProducer 方法
34   - private final LoadingCache<IotDataBridgeDO.RocketMQConfig, DefaultMQProducer> PRODUCER_CACHE = CacheBuilder.newBuilder()
35   - .refreshAfterWrite(Duration.ofMinutes(10)) // TODO puhui999:应该是 read 30 分钟哈
36   - // 增加移除监听器,自动关闭 producer
37   - .removalListener(notification -> {
38   - DefaultMQProducer producer = (DefaultMQProducer) notification.getValue();
39   - // TODO puhui999:if return,更简短哈
40   - if (producer != null) {
41   - try {
42   - producer.shutdown();
43   - log.info("[PRODUCER_CACHE][配置({}) 对应的 producer 已关闭]", notification.getKey());
44   - } catch (Exception e) {
45   - log.error("[PRODUCER_CACHE][配置({}) 对应的 producer 关闭失败]", notification.getKey(), e);
46   - }
47   - }
48   - })
49   - // TODO @puhui999:就同步哈,不用异步处理。
50   - // 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程
51   - .build(CacheLoader.asyncReloading(new CacheLoader<IotDataBridgeDO.RocketMQConfig, DefaultMQProducer>() {
52   -
53   - @Override
54   - public DefaultMQProducer load(IotDataBridgeDO.RocketMQConfig config) throws Exception {
55   - DefaultMQProducer producer = new DefaultMQProducer(config.getGroup());
56   - producer.setNamesrvAddr(config.getNameServer());
57   - producer.start();
58   - log.info("[PRODUCER_CACHE][配置({}) 对应的 producer 已创建并启动]", config);
59   - return producer;
60   - }
61   -
62   - }, Executors.newCachedThreadPool()));
  23 +public class IotRocketMQDataBridgeExecute extends AbstractCacheableDataBridgeExecute {
63 24  
64 25 @Override
65 26 public void execute(IotDeviceMessage message, IotDataBridgeDO dataBridge) {
66   - // 1.1 校验数据桥接的类型 == ROCKETMQ
  27 + // 1.1 校验数据桥梁的类型 == ROCKETMQ
67 28 if (!IotDataBridgTypeEnum.ROCKETMQ.getType().equals(dataBridge.getType())) {
68 29 return;
69 30 }
... ... @@ -74,7 +35,7 @@ public class IotRocketMQDataBridgeExecute implements IotDataBridgeExecute {
74 35 private void executeRocketMQ(IotDeviceMessage message, IotDataBridgeDO.RocketMQConfig config) {
75 36 try {
76 37 // 1. 获取或创建 Producer
77   - DefaultMQProducer producer = PRODUCER_CACHE.get(config);
  38 + DefaultMQProducer producer = (DefaultMQProducer) getProducer(config);
78 39  
79 40 // 2.1 创建消息对象,指定Topic、Tag和消息体
80 41 Message msg = new Message(
... ... @@ -95,6 +56,22 @@ public class IotRocketMQDataBridgeExecute implements IotDataBridgeExecute {
95 56 }
96 57 }
97 58  
  59 + @Override
  60 + protected Object initProducer(Object config) throws Exception {
  61 + IotDataBridgeDO.RocketMQConfig rocketMQConfig = (IotDataBridgeDO.RocketMQConfig) config;
  62 + DefaultMQProducer producer = new DefaultMQProducer(rocketMQConfig.getGroup());
  63 + producer.setNamesrvAddr(rocketMQConfig.getNameServer());
  64 + producer.start();
  65 + return producer;
  66 + }
  67 +
  68 + @Override
  69 + protected void closeProducer(Object producer) {
  70 + if (producer instanceof DefaultMQProducer) {
  71 + ((DefaultMQProducer) producer).shutdown();
  72 + }
  73 + }
  74 +
98 75 // TODO @芋艿:测试代码,后续清理
99 76 public static void main(String[] args) {
100 77 // 1. 创建一个共享的实例
... ...