Files
youlai-boot/src/main/java/com/youlai/boot/config/MqttConfig.java
TongTongStudio 035e21a432 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 认证信息
2026-08-12 08:26:30 +08:00

163 lines
5.9 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}