feat: 新增 TDengine 多数据源与 MQTT v5 客户端支持
- 新增 DataSourceConfig 配置类,支持 MySQL(主)与 TDengine(时序)多数据源,使用 Druid 手动配置以兼容 Spring Boot 4.x
- 重构 MQTT 配置,从 spring-integration-mqtt 迁移至 Paho MQTT v5 原生客户端,实现消息发布/订阅与优雅关闭
- 重构 MqttMessageHandler 与 MqttProducer,适配 v5 客户端回调,优化设备状态消息处理逻辑(online/offline 字段调整)
- 新增设备修改昵称接口(PUT /api/v1/devices/{id})与 TDengine 心跳最后在线时间查询接口
- 设备列表查询填充在线状态(Redis),增强设备管理功能
- 配置缓存预热失败时仅记录日志,避免数据库不可用导致应用启动失败
- 更新开发与生产环境配置,添加 TDengine 数据源及 MQTT 认证信息
This commit is contained in:
@@ -1,53 +1,145 @@
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import com.youlai.boot.config.property.MqttProperties;
|
||||
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
|
||||
import com.youlai.boot.support.mqtt.MqttMessageHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.paho.mqttv5.client.*;
|
||||
import org.eclipse.paho.mqttv5.common.MqttException;
|
||||
import org.eclipse.paho.mqttv5.common.MqttMessage;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.integration.annotation.ServiceActivator;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.core.MessageProducer;
|
||||
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
|
||||
import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
|
||||
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* MQTT 配置
|
||||
* <p>
|
||||
* 基于 spring-integration-mqtt (Eclipse Paho) 实现消息的发布与订阅。
|
||||
* 基于 Eclipse Paho MQTT v5 原生客户端 (org.eclipse.paho.mqttv5.client) 实现消息的发布与订阅。
|
||||
* 通过 {@code mqtt.enabled=false} 可整体关闭 MQTT 功能。
|
||||
*
|
||||
* @author TongTongStudio
|
||||
* @since 2026/8/8
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "mqtt", name = "enabled", havingValue = "true")
|
||||
public class MqttConfig {
|
||||
|
||||
/**
|
||||
* MQTT 连接工厂
|
||||
* MQTT 客户端实例(保存引用,供 @PreDestroy 优雅关闭)
|
||||
*/
|
||||
@Bean
|
||||
public DefaultMqttPahoClientFactory mqttClientFactory(MqttProperties properties) {
|
||||
MqttConnectOptions options = new MqttConnectOptions();
|
||||
options.setServerURIs(new String[]{normalizeServerUri(properties.getUrl())});
|
||||
private IMqttClient client;
|
||||
|
||||
/**
|
||||
* MQTT 客户端
|
||||
* <p>同一客户端同时用于发布与订阅。连接成功后立即订阅配置的主题。</p>
|
||||
*/
|
||||
@Bean(destroyMethod = "disconnect")
|
||||
public IMqttClient mqttClient(MqttProperties properties, MqttMessageHandler mqttMessageHandler) throws MqttException {
|
||||
String serverUri = normalizeServerUri(properties.getUrl());
|
||||
String clientId = properties.getClientId();
|
||||
|
||||
client = new MqttClient(serverUri, clientId);
|
||||
|
||||
MqttConnectionOptionsBuilder builder = new MqttConnectionOptionsBuilder()
|
||||
.serverURI(serverUri)
|
||||
.connectionTimeout(properties.getConnectionTimeout())
|
||||
.keepAliveInterval(properties.getKeepAliveInterval())
|
||||
.automaticReconnect(properties.isAutomaticReconnect())
|
||||
.sessionExpiryInterval(TimeUnit.DAYS.toSeconds(1))
|
||||
.cleanStart(false);
|
||||
|
||||
if (properties.getUsername() != null && !properties.getUsername().isEmpty()) {
|
||||
options.setUserName(properties.getUsername());
|
||||
builder.username(properties.getUsername());
|
||||
}
|
||||
if (properties.getPassword() != null && !properties.getPassword().isEmpty()) {
|
||||
options.setPassword(properties.getPassword().toCharArray());
|
||||
builder.password(properties.getPassword().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
options.setConnectionTimeout(properties.getConnectionTimeout());
|
||||
options.setKeepAliveInterval(properties.getKeepAliveInterval());
|
||||
options.setAutomaticReconnect(properties.isAutomaticReconnect());
|
||||
options.setCleanSession(true);
|
||||
|
||||
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
|
||||
factory.setConnectionOptions(options);
|
||||
return factory;
|
||||
MqttConnectionOptions options = builder.build();
|
||||
|
||||
// 消息到达回调:分发到业务处理器
|
||||
client.setCallback(new org.eclipse.paho.mqttv5.client.MqttCallback() {
|
||||
@Override
|
||||
public void messageArrived(String topic, MqttMessage message) {
|
||||
String payload = new String(message.getPayload(), StandardCharsets.UTF_8);
|
||||
mqttMessageHandler.handleMessage(topic, payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deliveryComplete(IMqttToken iMqttToken) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnected(org.eclipse.paho.mqttv5.client.MqttDisconnectResponse disconnectResponse) {
|
||||
log.warn("MQTT 连接断开: reasonCode={}, reasonString={}",
|
||||
disconnectResponse.getReturnCode(), disconnectResponse.getReasonString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mqttErrorOccurred(MqttException exception) {
|
||||
log.error("MQTT 发生异常", exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectComplete(boolean reconnect, String serverURI) {
|
||||
log.info("MQTT 连接成功, reconnect={}, serverURI={}", reconnect, serverURI);
|
||||
// 连接/重连成功后订阅主题
|
||||
subscribeTopics(client, properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authPacketArrived(int i, org.eclipse.paho.mqttv5.common.packet.MqttProperties mqttProperties) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
client.connect(options);
|
||||
log.info("MQTT 客户端已启动, serverURI={}, clientId={}", serverUri, clientId);
|
||||
|
||||
// 首次连接成功后订阅主题(重连场景由 connectComplete 处理)
|
||||
if (client.isConnected()) {
|
||||
subscribeTopics(client, properties);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用关闭时优雅停止 MQTT 客户端。
|
||||
* <p>先 {@code disconnect} 断开连接,再 {@code close} 释放 Paho 后台线程池,
|
||||
* 避免容器已销毁(Redisson 已 shutdown)后回调线程仍在处理消息而报错。</p>
|
||||
*/
|
||||
@PreDestroy
|
||||
public void destroy() {
|
||||
try {
|
||||
if (client != null) {
|
||||
if (client.isConnected()) {
|
||||
client.disconnect();
|
||||
}
|
||||
client.close();
|
||||
}
|
||||
} catch (MqttException e) {
|
||||
log.warn("关闭 MQTT 客户端失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅配置中的所有主题
|
||||
*/
|
||||
private void subscribeTopics(IMqttClient client, MqttProperties properties) {
|
||||
for (String topic : properties.getTopics()) {
|
||||
try {
|
||||
client.subscribe(topic, properties.getQos());
|
||||
log.info("已订阅 MQTT 主题: {}", topic);
|
||||
} catch (MqttException e) {
|
||||
log.error("订阅 MQTT 主题失败: {}", topic, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,51 +159,4 @@ public class MqttConfig {
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
/* ============================ 发布 (Publish) ============================ */
|
||||
|
||||
/**
|
||||
* 发布消息的出站通道
|
||||
*/
|
||||
@Bean
|
||||
public MessageChannel mqttOutboundChannel() {
|
||||
return new DirectChannel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布消息处理器,通过 {@link org.springframework.messaging.support.MessageBuilder} 发送到出站通道即可发布
|
||||
*/
|
||||
@Bean
|
||||
@ServiceActivator(inputChannel = "mqttOutboundChannel")
|
||||
public MessageHandler mqttOutbound(DefaultMqttPahoClientFactory factory, MqttProperties properties) {
|
||||
MqttPahoMessageHandler handler = new MqttPahoMessageHandler(properties.getClientId() + "-pub", factory);
|
||||
handler.setAsync(false);
|
||||
handler.setDefaultQos(properties.getQos());
|
||||
return handler;
|
||||
}
|
||||
|
||||
/* ============================ 订阅 (Subscribe) ============================ */
|
||||
|
||||
/**
|
||||
* 订阅消息的入站通道
|
||||
*/
|
||||
@Bean
|
||||
public MessageChannel mqttInboundChannel() {
|
||||
return new DirectChannel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 入站订阅适配器:订阅 {@link MqttProperties#getTopics()} 中配置的主题
|
||||
*/
|
||||
@Bean
|
||||
public MessageProducer mqttInbound(DefaultMqttPahoClientFactory factory, MqttProperties properties) {
|
||||
String[] topics = properties.getTopics().toArray(new String[0]);
|
||||
MqttPahoMessageDrivenChannelAdapter adapter =
|
||||
new MqttPahoMessageDrivenChannelAdapter(
|
||||
properties.getClientId() + "-sub", factory, topics);
|
||||
adapter.setCompletionTimeout(5000L);
|
||||
adapter.setQos(properties.getQos());
|
||||
adapter.setOutputChannel(mqttInboundChannel());
|
||||
return adapter;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user