- 新增 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 认证信息
163 lines
5.9 KiB
Java
163 lines
5.9 KiB
Java
package com.youlai.boot.config;
|
||
|
||
import com.youlai.boot.config.property.MqttProperties;
|
||
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 jakarta.annotation.PreDestroy;
|
||
import java.nio.charset.StandardCharsets;
|
||
import java.util.concurrent.TimeUnit;
|
||
|
||
/**
|
||
* MQTT 配置
|
||
* <p>
|
||
* 基于 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 客户端实例(保存引用,供 @PreDestroy 优雅关闭)
|
||
*/
|
||
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()) {
|
||
builder.username(properties.getUsername());
|
||
}
|
||
if (properties.getPassword() != null && !properties.getPassword().isEmpty()) {
|
||
builder.password(properties.getPassword().getBytes(StandardCharsets.UTF_8));
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将 Broker 地址统一规范为 Paho 支持的 scheme。
|
||
* <p>Eclipse Paho 仅内置 {@code tcp://} 与 {@code ssl://} 两种 scheme,
|
||
* 配置中常见的 {@code mqtt://} 需转换为 {@code tcp://}(明文)或 {@code ssl://}(加密)。</p>
|
||
*/
|
||
private String normalizeServerUri(String uri) {
|
||
if (uri == null || uri.isEmpty()) {
|
||
return uri;
|
||
}
|
||
if (uri.startsWith("mqtt://")) {
|
||
return "tcp://" + uri.substring("mqtt://".length());
|
||
}
|
||
if (uri.startsWith("mqtts://")) {
|
||
return "ssl://" + uri.substring("mqtts://".length());
|
||
}
|
||
return uri;
|
||
}
|
||
}
|