问题描述
项目引入mqtt处理物联网设备的实时数据消息,同事copy了mqtt的demo代码到项目中,在demo中运行没有问题,但是在项目中启动后一直在报已断开连接32109。同事排查了半天问题,最后发现把项目中的spring-web依赖排除以后就好了,但是又不能排除排除了之后其他接口怎么办。网上查找该问题说是因为注册到mqtt的client-id重复了才会报这个错,但是看代码client-id是一个类属性,初始化是random出来的怎么会重复呢?
源码如下:
mqtt-client包装
package com.xx.mqtt;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* Mqtt-订阅(服务启动自动加载)
* <p>
* Mqtt-订阅为服务启动自动加载,如需新增订阅主题需在配置文件中新增(新服务需重新配置)
*/
@Slf4j
@Component
public class MqttConsumerUtil {
/**
* Mqtt客户端
*/
private MqttClient mqttClient;
/**
* 客户端ID
*/
private String clientId = "SUB" + (int) (Math.random() * 100000000);
@Value("${config.mq-url}")
private String mqUrl;
@Value("${config.mq-username}")
private String mqUserName;
@Value("${config.mq-password}")
private String mqPassword;
@Value("${config.mq-topiclist}")
private List<String> mqTopicList;
/**
* 创建客户端
*
* @param mqttCallback 回调函数
*/
public void setMqttClient(MqttCallback mqttCallback) throws MqttException {
MqttConnectOptions options = mqttConnectOptions();
if (mqttCallback == null) {
mqttClient.setCallback(new MqttCallBack());
} else {
mqttClient.setCallback(mqttCallback);
}
mqttClient.connect(options);
}
/**
* 客户端连接
*/
private MqttConnectOptions mqttConnectOptions() throws MqttException {
//mqttClient = new MqttClient(mqUrl, "SUB" + (int) (Math.random() * 100000000), new MemoryPersistence());
mqttClient = new MqttClient(mqUrl, clientId, new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(mqUserName);
options.setPassword(mqPassword.toCharArray());
options.setConnectionTimeout(10);
options.setAutomaticReconnect(true);
options.setCleanSession(false);
return options;
}
/**
* 订阅某一个主题 ,此方法默认的的Qos等级为:1
*/
public void sub() throws MqttException {
for (String topic : mqTopicList) {
mqttClient.subscribe(topic);
}
}
/**
* 订阅某一个主题,可携带Qos
*
* @param qos 消息质量:0、1、2
*/
public void sub(int qos) throws MqttException {
log.info("----------开始订阅主题----------");
for (String topic : mqTopicList) {
mqttClient.subscribe(topic, qos);
}
}
/**
* 关闭MQTT连接
*/
public void close() throws MqttException {
mqttClient.close();
mqttClient.disconnect();
}
}
mqtt-回调
package com.xx.mqtt;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.springframework.stereotype.Component;
/**
* Mqtt-回调函数
*/
@Slf4j
@Component
public class MqttCallBack implements MqttCallback {
/**
* MQTT 断开连接会执行此方法
*/
@Override
public void connectionLost(Throwable throwable) {
log.info("断开了MQTT连接 :{}", throwable.getMessage());
log.error(throwable.getMessage(), throwable);
}
/**
* publish发布成功后会执行到这里
*/
@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
log.info("发布消息成功");
}
/**
* subscribe订阅后得到的消息会执行到这里
*/
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
// TODO 此处可以将订阅得到的消息进行业务处理、数据存储
log.info("收到来自 " + topic + " 的消息:{}", new String(message.getPayload()));
}
}
mqtt-client初始化连接
package com.xx.mqtt;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class MqttListenerUtil implements ApplicationListener<ContextRefreshedEvent> {
private final MqttConsumerUtil server;
@Autowired
public MqttListenerUtil(MqttConsumerUtil server) {
this.server = server;
}
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
try {
server.setMqttClient(new MqttCallBack());
server.sub();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
mqtt依赖:
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mqtt</artifactId>
</dependency>
问题溯源
最后发现问题是mqttclient的容器bean中创建client的方法被调用了两遍导致的,因为clientid是容器bean的类属性只初始化一次,然后创建client的方法被调用了两次,调用第二次创建client跟第一遍用的一样的clientid所以导致报错。
可以改成不用类属性初始化clientid,在创建client的方法中random生成clientid就好,但是这样没有从根上解决问题,这样一个mqtt的topic有了两个client去订阅。
最后跟同事说了问题出在client创建了两次上,同事将代码改了一下问题解决。
package com.xx.mqtt;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class MqttListenerUtil implements ApplicationRunner {
private final MqttConsumerUtil server;
@Autowired
public MqttListenerUtil(MqttConsumerUtil server) {
this.server = server;
}
@Override
public void run(ApplicationArguments args) throws Exception {
try {
server.setMqttClient(new MqttCallBack());
server.sub();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
将ApplicationListener<ContextRefreshedEvent>这个改成implements ApplicationRunner或者使用@PostConstruct就好了。所以问题出在ApplicationListener<ContextRefreshedEvent>上。在网上查找找到下面博文。
Spring中的ApplicationListener和ContextRefreshedEvent的理解_context refreshed_追风少年201的博客-CSDN博客
所以问题根源是:spring容器初始化完成和mvc子容器初始化完成后都会发布ContextRefreshedEvent事件,所以导致创建mqttclient的方法被调用了两次。这也是为什么排除了spring-web依赖也能解决问题的原因,因为排除了spring-web那么mvc子容器就没了。
问题解决
- 将
ApplicationListener<ContextRefreshedEvent>这个改成implements ApplicationRunner或者使用@PostConstruct - 依旧使用
ApplicationListener<ContextRefreshedEvent>,但是在onApplicationEvent监听到消息时判断一下,如果event.getApplicationContext().getParent() == null那说明事件是由spring父容器发布的,那么进行处理,在子容器发布该事件时不进行处理,就可以保证按需只处理一次了。
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (event.getApplicationContext().getParent() == null) {//root application context 没有parent,他就是老大.
//需要执行的逻辑代码,当spring容器初始化完成后就会执行该方法。
}
}