Spring中ApplicationListener导致的mqtt连接问题:已断开连接32109

715 阅读4分钟

问题描述

项目引入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子容器就没了。

问题解决

  1. ApplicationListener<ContextRefreshedEvent>这个改成implements ApplicationRunner或者使用@PostConstruct
  2. 依旧使用ApplicationListener<ContextRefreshedEvent>,但是在onApplicationEvent监听到消息时判断一下,如果event.getApplicationContext().getParent() == null那说明事件是由spring父容器发布的,那么进行处理,在子容器发布该事件时不进行处理,就可以保证按需只处理一次了。
@Override  
public void onApplicationEvent(ContextRefreshedEvent event) {  
    if (event.getApplicationContext().getParent() == null) {//root application context 没有parent,他就是老大.    
        //需要执行的逻辑代码,当spring容器初始化完成后就会执行该方法。    
}  
  
}