template() {
+ return redisTemplate;
+ }
+
+ /**
+ * 获取 StringRedisTemplate(仅在确定 Redis 可用时使用)
+ */
+ public static StringRedisTemplate stringTemplate() {
+ return stringRedisTemplate;
+ }
+}
diff --git a/src/main/java/com/youlai/boot/config/MqttConfig.java b/src/main/java/com/youlai/boot/config/MqttConfig.java
new file mode 100644
index 00000000..826941ee
--- /dev/null
+++ b/src/main/java/com/youlai/boot/config/MqttConfig.java
@@ -0,0 +1,117 @@
+package com.youlai.boot.config;
+
+import com.youlai.boot.config.property.MqttProperties;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+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;
+
+/**
+ * MQTT 配置
+ *
+ * 基于 spring-integration-mqtt (Eclipse Paho) 实现消息的发布与订阅。
+ * 通过 {@code mqtt.enabled=false} 可整体关闭 MQTT 功能。
+ *
+ * @author TongTongStudio
+ * @since 2026/8/8
+ */
+@Configuration
+@ConditionalOnProperty(prefix = "mqtt", name = "enabled", havingValue = "true")
+public class MqttConfig {
+
+ /**
+ * MQTT 连接工厂
+ */
+ @Bean
+ public DefaultMqttPahoClientFactory mqttClientFactory(MqttProperties properties) {
+ MqttConnectOptions options = new MqttConnectOptions();
+ options.setServerURIs(new String[]{normalizeServerUri(properties.getUrl())});
+ if (properties.getUsername() != null && !properties.getUsername().isEmpty()) {
+ options.setUserName(properties.getUsername());
+ }
+ if (properties.getPassword() != null && !properties.getPassword().isEmpty()) {
+ options.setPassword(properties.getPassword().toCharArray());
+ }
+ options.setConnectionTimeout(properties.getConnectionTimeout());
+ options.setKeepAliveInterval(properties.getKeepAliveInterval());
+ options.setAutomaticReconnect(properties.isAutomaticReconnect());
+ options.setCleanSession(true);
+
+ DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
+ factory.setConnectionOptions(options);
+ return factory;
+ }
+
+ /**
+ * 将 Broker 地址统一规范为 Paho 支持的 scheme。
+ *
Eclipse Paho 仅内置 {@code tcp://} 与 {@code ssl://} 两种 scheme,
+ * 配置中常见的 {@code mqtt://} 需转换为 {@code tcp://}(明文)或 {@code ssl://}(加密)。
+ */
+ 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;
+ }
+
+ /* ============================ 发布 (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;
+ }
+}
diff --git a/src/main/java/com/youlai/boot/config/RedisConfig.java b/src/main/java/com/youlai/boot/config/RedisConfig.java
index c821d5ef..da7eb953 100644
--- a/src/main/java/com/youlai/boot/config/RedisConfig.java
+++ b/src/main/java/com/youlai/boot/config/RedisConfig.java
@@ -1,33 +1,42 @@
package com.youlai.boot.config;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.data.redis.serializer.JacksonJsonRedisSerializer;
+import org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
-import tools.jackson.databind.cfg.DateTimeFeature;
-import tools.jackson.databind.json.JsonMapper;
+import tools.jackson.databind.ObjectMapper;
/**
* Redis 配置
+ *
+ * 使用 Lazy 初始化,避免 Redis 不可用时阻塞应用启动。
*
* @author Ray.Hao
* @since 2023/5/15
*/
+@Slf4j
@Configuration
+@ConditionalOnClass(RedisConnectionFactory.class)
public class RedisConfig {
/**
* 自定义 RedisTemplate
*
- * 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer
+ * 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer。
+ * 延迟初始化:即使 Redis 不可用,应用也能正常启动。
*
* @param redisConnectionFactory {@link RedisConnectionFactory}
* @return {@link RedisTemplate}
*/
@Bean
+ @Lazy
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
+ log.info("初始化 RedisTemplate...");
RedisTemplate redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
@@ -36,15 +45,14 @@ public class RedisConfig {
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
- JsonMapper jsonMapper = JsonMapper.builder()
- .disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS)
- .build();
- JacksonJsonRedisSerializer