# Spring Boot 集成 EMQX 客户端重连与订阅恢复

> **场景**: 解决微服务重启后 EMQX 客户端连接断开，但服务端订阅状态不同步的问题
> 
> **技术栈**: Spring Boot + MQTT + EMQX
> 
> **作者**: yuejianbin  
> **日期**: 2025-06-26

---

## 问题描述

在微服务架构中，当云服务重启后，会出现以下问题：
- EMQX 服务端客户端连接断开
- 订阅者在服务端依然存在
- 客户端重连后订阅状态不同步

## 使用 Cursor 的解决过程

### 1. 问题诊断 Prompt

```
@/micro-cloud-adapter cloud 服务重启后 会 emqx 服务端 客户端断开了连接，但是订阅者在服务端还依然存在，该如何解决
```

### 2. 代码分析与解决方案

#### 问题根因分析

Cursor 分析后发现主要问题：

1. **clientId 使用 UUID**: 每次服务重启都会生成新的 clientId，导致 EMQX 无法识别同一客户端
2. **cleanSession 设置**: 会话持久化配置不当
3. **缺少重连机制**: 没有在重连后立即恢复订阅的机制

#### 解决方案代码

**优化后的 MqttConfig 配置类**：

```java
@Configuration
public class MqttConfig {
    
    @Value("${spring.application.name}")
    private String applicationName;
    
    @Autowired
    private MqttTopicService mqttTopicService;
    
    /**
     * 生成固定的 clientId，基于应用名和主机信息
     * 确保服务重启后 clientId 保持一致
     */
    private String getFixedClientId() {
        try {
            String hostName = InetAddress.getLocalHost().getHostName();
            return applicationName + "-" + hostName + "-service";
        } catch (Exception e) {
            return applicationName + "-service";
        }
    }
    
    @Bean
    public MqttPahoClientFactory mqttClientFactory() {
        DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
        MqttConnectOptions options = new MqttConnectOptions();
        
        // 关键配置：使用固定 clientId
        options.setClientId(getFixedClientId());
        options.setCleanSession(false); // 保持会话持久化
        options.setAutomaticReconnect(true); // 自动重连
        options.setKeepAliveInterval(60);
        options.setConnectionTimeout(30);
        
        factory.setConnectionOptions(options);
        return factory;
    }
}
```

**添加连接状态监听器**：

```java
@Component
@Slf4j
public class MqttConnectionCallback implements MqttCallback {
    
    @Autowired
    private MqttTopicService mqttTopicService;
    
    @Override
    public void connectionLost(Throwable cause) {
        log.warn("MQTT连接丢失: {}", cause.getMessage());
    }
    
    @Override
    public void connectComplete(boolean reconnect, String serverURI) {
        if (reconnect) {
            log.info("MQTT重连成功，开始恢复订阅");
            // 重连后立即恢复订阅
            restoreSubscriptions();
        }
    }
    
    private void restoreSubscriptions() {
        try {
            List<String> topics = mqttTopicService.getAllTopics();
            // 重新订阅所有主题
            mqttTopicService.subscribeTopics(topics);
            log.info("订阅恢复完成，共恢复 {} 个主题", topics.size());
        } catch (Exception e) {
            log.error("恢复订阅失败", e);
        }
    }
}
```

### 3. 效果验证

使用 Cursor 生成的测试用例：

```java
@Test
public void testReconnectAndSubscribe() {
    // 模拟服务重启
    mqttClient.disconnect();
    
    // 验证重连后订阅状态
    mqttClient.connect();
    
    // 检查订阅是否正常恢复
    List<String> activeTopics = mqttTopicService.getActiveSubscriptions();
    assertThat(activeTopics).isNotEmpty();
}
```

## 关键配置总结

| 配置项 | 推荐值 | 说明 |
|--------|--------|------|
| `clientId` | 基于应用名+主机名 | 确保重启后一致性 |
| `cleanSession` | `false` | 保持会话持久化 |
| `automaticReconnect` | `true` | 启用自动重连 |
| `keepAliveInterval` | `60` | 心跳间隔 |

## 使用体验

### Cursor 的优势
- 快速定位问题根因
- 提供完整的代码解决方案
- 自动生成相关测试用例

### 注意事项
- 需要根据实际项目结构调整包名和类名
- SSL 配置需要根据实际证书路径调整
- 监听器注册需要确保在 Spring 容器中正确装配

---

## 相关资源

- [EMQX 官方文档](https://www.emqx.io/docs)
- [Spring Integration MQTT](https://docs.spring.io/spring-integration/reference/html/mqtt.html)
- [Eclipse Paho MQTT Client](https://www.eclipse.org/paho/clients/java/)
- [完整对话记录](./original-conversations/cursor-emqx-mqtt-reconnect-conversation.md) - 查看与 Cursor AI 的完整问题分析过程

## 扩展思考

1. **集群环境下的 clientId 策略**
2. **订阅主题的动态管理**
3. **消息重复处理机制**
4. **监控与告警集成**

---

## 💡 学习建议

想要了解完整的问题分析过程？点击查看 [原始 Cursor 对话记录](./original-conversations/cursor-emqx-mqtt-reconnect-conversation.md)，包含：
- AI 的完整分析思路
- 多轮问题诊断过程
- 代码生成和优化过程
- 测试验证方案推导 