Bootstrap

SpringBoot集成MQTT客户端代码实现

        在上篇文章中我们讲述了如何使用eqmx搭建MQTT服务端,接下来讲讲如何在SpringBoot中去使用MQTT客户端。

一、引入依赖

        mqtt依赖和excel依赖,引入Excel依赖是为了方便对mqtt订阅主题进行管理。

        <dependency>
            <groupId>org.eclipse.paho</groupId>
            <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
            <version>1.2.2</version>
        </dependency>
   <!--  解析excel工具类-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>2.2.6</version>
        </dependency>

二、客户端实现

1.客户端代码
package org.example.mqtt;

import com.alibaba.fastjson.JSONObject;

import lombok.Data;
import lombok.extern.log4j.Log4j2;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.example.config.MqttDeviceSignalConfig;
import org.example.config.MqttExcelItem;

import java.util.List;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * Mqtt客户端
 */
@Data
@Log4j2
public class BasicMqttClient {

    public static final int MQTT_CONNECT_TIMEOUT = 10;

    public static final int KEEP_ALIVE_INTERVAL = 60;

    private String ip;

    private int port;

    private String userName;

    private String password;

    private MqttClient mqttClient;

    private MqttConnectOptions mqttConnectOptions;


    private MqttCallback mqttCallback;

    /**
     * 异步发布消息线程池
     */
    private ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            4,
            10,
            30,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100),
            new ThreadPoolExecutor.DiscardPolicy());

    public volatile boolean connectMqttFlag = false;

    public BasicMqttClient(String ip, int port, String userName, String password, MqttCallback mqttCallback) {
        this.ip = ip;
        this.port = port;
        this.userName = userName;
        this.password = password;
        this.mqttCallback = mqttCallback;
    }

    public void init() throws MqttException {
        String clientId = UUID.randomUUID().toString();
        String servers = "tcp://" + this.ip + ":" + this.port;
        log.info("mqtt-url: {}", servers);
        log.info("mqtt-userName: {}", userName);
        log.info("mqtt-password: {}", password);

        int qos = 1;
        mqttClient = new MqttClient(servers, clientId, new MemoryPersistence());
        if (null == mqttConnectOptions) {
            mqttConnectOptions = new MqttConnectOptions();
        }
        //设置是否清空session,false表示服务器会保留客户端的连接记录,true表示每次都以新身份连接
        mqttConnectOptions.setCleanSession(false);
        //设置来连接使用的用户名
        mqttConnectOptions.setUserName(userName);
        //设置连接的密码
        mqttConnectOptions.setPassword(password.toCharArray());
        //设置连接的服务端
        mqttConnectOptions.setServerURIs(new String[]{servers});
        //连接超时时间,单位秒
        mqttConnectOptions.setConnectionTimeout(MQTT_CONNECT_TIMEOUT);
        //设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
        mqttConnectOptions.setKeepAliveInterval(KEEP_ALIVE_INTERVAL);
        mqttConnectOptions.setMqttVersion(MqttConnectOptions.MQTT_VERSION_3_1_1);
        //设置自动重连
//        mqttConnectOptions.setAutomaticReconnect(true);
        //设置回调,client.setCallback就可以调用PushCallback类中的messageArrived()方法
        mqttClient.setCallback(new MqttReceiveCallback());

        mqttClient.connect(mqttConnectOptions);
        //订阅消息
        //订阅消息
        List<MqttExcelItem> excelItems = MqttDeviceSignalConfig.excelItems;

        for (int i = 0; i < excelItems.size(); i++) {
            MqttExcelItem mqttExcelItem = excelItems.get(i);
            mqttClient.subscribe(mqttExcelItem.getGateway(), qos);
        }
    }

    public void connect() throws MqttException {
        mqttClient.connect(mqttConnectOptions);
        //订阅消息
        List<MqttExcelItem> excelItems = MqttDeviceSignalConfig.excelItems;

        for (int i = 0; i < excelItems.size(); i++) {
            MqttExcelItem mqttExcelItem = excelItems.get(i);
            mqttClient.subscribe(mqttExcelItem.getGateway(), 1);
        }
        connectMqttFlag = true;
        log.info("mqtt连接成功");
    }

    /**
     * 发布消息
     *
     * @param topic   主题
     * @param message 消息
     * @return
     * @throws MqttException
     */
    private static boolean publish(MqttTopic topic, MqttMessage message) throws MqttException {
        MqttDeliveryToken token = topic.publish(message);
        token.waitForCompletion(1000);
        return token.isComplete();
    }

    class MqttReceiveCallback implements MqttCallback {
        @Override
        public void connectionLost(Throwable cause) {
            System.out.println("connectionLost");
        }

        @Override
        public void messageArrived(String topic, MqttMessage message) throws Exception {

            log.info("topic:{}", topic);
            log.info("message content:{}", JSONObject.parse(new String(message.getPayload())));
        }

        @Override
        public void deliveryComplete(IMqttDeliveryToken token) {
            System.out.println("deliveryComplete---------" + token.isComplete());
        }
    }

    /**
     * 发送消息
     *
     * @param topic   主题
     * @param message 消息
     * @param qos     质量
     * @throws Exception
     */
    public void sendMqttMessage(String topic, String message, int qos) {
        threadPoolExecutor.submit(() -> {
            try {
                MqttTopic mqttTopic = mqttClient.getTopic(topic);
                MqttMessage mqttMessage = new MqttMessage();
                mqttMessage.setQos(qos);
                mqttMessage.setRetained(false);
                mqttMessage.setPayload(message.getBytes("UTF-8"));
                BasicMqttClient.publish(mqttTopic, mqttMessage);
            } catch (Exception e) {
                log.error("MQTT生产数据异常, topic: {}, message: {}, exception: {}", topic, message, e.getMessage(), e);
            }
        });
    }
}
2.设置初始化及断线重连机制
package org.example.mqtt;


import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.example.config.MqttDeviceSignalConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;


/**
 * @Description: 平台北向Mqtt客户端
 * @Author: kele
 * @CreateTime: 2023-11-27  13:48
 */
@Log4j2
@Component
public class PlatformNorthBoundMqttClient {

    @Autowired
    private MqttDeviceSignalConfig mqttDeviceSignalConfig;



    @Getter
    private BasicMqttClient basicMqttClient = null;

    @Autowired
    private MqttReConnectThread mqttReConnectThread;

    public void init() {
        basicMqttClient = new BasicMqttClient(mqttDeviceSignalConfig.getIp(),
                mqttDeviceSignalConfig.getPort(),
                mqttDeviceSignalConfig.getUsername(),
                mqttDeviceSignalConfig.getPassword(),
                new MqttCallback() {
                    @Override
                    public void connectionLost(Throwable throwable) {
                        log.info(throwable.getMessage());
                        mqttReConnectThread.asyncReConnect(basicMqttClient);
                    }

                    @Override
                    public void messageArrived(String topic, MqttMessage message) throws Exception {
                        System.out.println("topic:"+topic);
                        System.out.println("Qos:"+message.getQos());
                        System.out.println("message content:"+new String(message.getPayload()));
                    }

                    @Override
                    public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {

                    }
                });

        try {
            basicMqttClient.init();
        } catch (MqttException e) {
            log.error("连接第三方mqtt失败, 失败原因:{}", e.getMessage(), e);
            mqttReConnectThread.asyncReConnect(basicMqttClient);
        }
    }


}

断线重连线程开启

package org.example.mqtt;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

/**
 * Copyright (c) 2007, Hangzhou uniview Technologies Co., Ltd. All rights reserved.
 * <http://www.uniview.com/>
 * ------------------------------------------------------------------------------
 * Product     :
 * Module Name :
 * Date Created: 
 * Creator     : 
 * Description :
 */
@Component
@Slf4j
public class MqttReConnectThread {

    @Autowired
    private PlatformNorthBoundMqttClient mqttClient;

    @Async
    public void asyncReConnect(BasicMqttClient basicMqttClient) {
        while (true) {
            try {
                log.info("MQTT连接失败,进行重连");
                Thread.sleep(5000);
                basicMqttClient.connect();

                if (basicMqttClient.connectMqttFlag){
                    log.info("MQTT重连成功");
//                    mqttClient.subscribeDeviceSignal();
                    break;
                }
            } catch (Exception e) {
                log.info("MQTT重连失败,继续重试: {}", e.getMessage());
            }
        }
    }
}

三、Excel工具类的使用

1.application.yml中配置
platform-northbound-mqtt:
  ip: 127.0.0.1
  port: 1883
  username: admin
  password: "*Ab123456"
  #第三发提供的点表
  mqtt-excel-config: "mqtt订阅表.xlsx"
  #需要解析的sheet页的下标(从0开始)
  mqtt-excel-sheet-numbers: 0
2.加载excel工具类
package org.example.util;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.enums.CellExtraTypeEnum;
import com.alibaba.excel.metadata.CellExtra;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.alibaba.excel.support.ExcelTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.List;

/**
 * TODO
 *
 * @author zV15174
 * @date 2023/5/8 14:18
 */
@Slf4j
public class ExcelAnalysisUtil<T> {

    public static <T> List<T> getList(String filePath, Class<T> clazz) throws IllegalAccessException {
        return getList(filePath, clazz, 0, 1);
    }

    public static <T> List<T> getList(String filePath, Class<T> clazz, Integer sheetNo, Integer headRowNumber) throws IllegalAccessException {
        MyAnalysisEventListener<T> listener = new MyAnalysisEventListener<>(headRowNumber);

        ExcelReader excelReader = EasyExcel.read(filePath)
                .excelType(ExcelTypeEnum.XLSX)
                .extraRead(CellExtraTypeEnum.MERGE)
                .ignoreEmptyRow(false)
                .autoCloseStream(true)
                .build();


        ReadSheet readSheet = EasyExcel.readSheet(sheetNo)
                .headRowNumber(headRowNumber)
                .head(clazz)
                .registerReadListener(listener)
                .build();

        excelReader.read(readSheet);
        excelReader.finish();

        List<CellExtra> extraMergeInfoList = listener.getExtraMergeInfoList();

        return listener.getDataList();
//        if (extraMergeInfoList.isEmpty()) {
//            return listener.getDataList();
//        }
//        List<T> data = explainMergeData(listener.getDataList(), extraMergeInfoList, headRowNumber);
//        return data;
    }

    public List<T> getList(MultipartFile file, Class<T> clazz) throws IllegalAccessException {
        return getList(file, clazz, 0, 1);
    }

    public List<T> getList(MultipartFile file, Class<T> clazz, Integer sheetNo, Integer headRowNumber) throws IllegalAccessException {
        MyAnalysisEventListener<T> listener = new MyAnalysisEventListener<>(headRowNumber);
        try {
            EasyExcel.read(file.getInputStream(), clazz, listener).extraRead(CellExtraTypeEnum.MERGE).sheet(sheetNo).headRowNumber(headRowNumber).doRead();
        } catch (IOException e) {
            log.error(e.getMessage());
        }
        List<CellExtra> extraMergeInfoList = listener.getExtraMergeInfoList();
        if (extraMergeInfoList.isEmpty()) {
            return listener.getDataList();
        }
        List<T> data = explainMergeData(listener.getDataList(), extraMergeInfoList, headRowNumber);
        return data;
    }

    /**
     * 处理合并单元格
     *
     * @param data               解析数据
     * @param extraMergeInfoList 合并单元格信息
     * @param headRowNumber      起始行
     * @return 填充好的解析数据
     */
    private static <T> List<T> explainMergeData(List<T> data, List<CellExtra> extraMergeInfoList, Integer headRowNumber) throws IllegalAccessException {
        //循环所有合并单元格信息
        for (CellExtra cellExtra : extraMergeInfoList) {
            int firstRowIndex = cellExtra.getFirstRowIndex() - headRowNumber;
            int lastRowIndex = cellExtra.getLastRowIndex() - headRowNumber;
            int firstColumnIndex = cellExtra.getFirstColumnIndex();
            int lastColumnIndex = cellExtra.getLastColumnIndex();
            //获取初始值
            Object initValue = null;
            try {
                initValue = getInitValueFromList(firstRowIndex, firstColumnIndex, data);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
            //设置值
            for (int i = firstRowIndex; i <= lastRowIndex; i++) {
                for (int j = firstColumnIndex; j <= lastColumnIndex; j++) {
                    setInitValueToList(initValue, i, j, data);
                }
            }
        }
        return data;
    }

    /**
     * 设置合并单元格的值
     *
     * @param filedValue  值
     * @param rowIndex    行
     * @param columnIndex 列
     * @param data        解析数据
     */
    public static <T> void setInitValueToList(Object filedValue, Integer rowIndex, Integer columnIndex, List<T> data) throws IllegalAccessException {
        T object = data.get(rowIndex);

        for (Field field : object.getClass().getDeclaredFields()) {
            //提升反射性能,关闭安全检查
            field.setAccessible(true);
            ExcelProperty annotation = field.getAnnotation(ExcelProperty.class);
            if (annotation != null) {
                if (annotation.index() == columnIndex) {
                    field.set(object, filedValue);
                    break;
                }
            }
        }
    }


    /**
     * 获取合并单元格的初始值
     * rowIndex对应list的索引
     * columnIndex对应实体内的字段
     *
     * @param firstRowIndex    起始行
     * @param firstColumnIndex 起始列
     * @param data             列数据
     * @return 初始值
     */
    private static <T> Object getInitValueFromList(Integer firstRowIndex, Integer firstColumnIndex, List<T> data) throws IllegalAccessException {
        Object filedValue = null;
        T object = data.get(firstRowIndex);
        for (Field field : object.getClass().getDeclaredFields()) {
            //提升反射性能,关闭安全检查
            field.setAccessible(true);
            ExcelProperty annotation = field.getAnnotation(ExcelProperty.class);
            if (annotation != null) {
                if (annotation.index() == firstColumnIndex) {
                    filedValue = field.get(object);
                    break;
                }
            }
        }
        return filedValue;
    }
}
package org.example.util;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.metadata.CellExtra;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

/**
 * TODO
 *
 * @author zV15174
 * @date 2023/5/8 14:19
 */
@Slf4j
public class MyAnalysisEventListener<T> extends AnalysisEventListener<T> {
    /**
     * 解析的数据
     */
    @Getter
    private List<T> dataList = new ArrayList<>();

    /**
     * 正文起始行
     */
    private Integer headRowNumber;

    /**
     * 合并单元格
     */
    @Getter
    private List<CellExtra> extraMergeInfoList = new ArrayList<>();

    public MyAnalysisEventListener(Integer headRowNumber) {
        this.headRowNumber = headRowNumber;
    }

    /**
     * 这个每一条数据解析都会来调用
     *
     * @param data    one row value. Is is same as {@link AnalysisContext#readRowHolder()}
     * @param context context
     */
    @Override
    public void invoke(T data, AnalysisContext context) {
        log.info("解析到一条数据:{}", JSON.toJSONString(data));
        dataList.add(data);
    }

    /**
     * 所有数据解析完成了 都会来调用
     *
     * @param context context
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        log.info("所有数据解析完成!");
    }

    @Override
    public void extra(CellExtra extra, AnalysisContext context) {
        log.info("读取到了一条额外信息:{}", JSON.toJSONString(extra));
        switch (extra.getType()) {
            case COMMENT: {
                log.info("额外信息是批注,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(), extra.getColumnIndex(),
                        extra.getText());
                break;
            }
            case HYPERLINK: {
                if ("Sheet1!A1".equals(extra.getText())) {
                    log.info("额外信息是超链接,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(),
                            extra.getColumnIndex(), extra.getText());
                } else if ("Sheet2!A1".equals(extra.getText())) {
                    log.info(
                            "额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{},lastColumnIndex:{},"
                                    + "内容是:{}",
                            extra.getFirstRowIndex(), extra.getFirstColumnIndex(), extra.getLastRowIndex(),
                            extra.getLastColumnIndex(), extra.getText());
                } else {
                    log.warn("Unknown hyperlink!");
                }
                break;
            }
            case MERGE: {
                log.info(
                        "额外信息是合并单元格,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{},lastColumnIndex:{}",
                        extra.getFirstRowIndex(), extra.getFirstColumnIndex(), extra.getLastRowIndex(),
                        extra.getLastColumnIndex());
                if (extra.getRowIndex() >= headRowNumber) {
                    extraMergeInfoList.add(extra);
                }
                break;
            }
            default: {
            }
        }
    }

    public List<CellExtra> getExtraMergeInfoList() {
        return extraMergeInfoList;
    }
}
3.读取excel
package org.example.config;

import lombok.Data;
import lombok.extern.log4j.Log4j2;

import org.example.util.ExcelAnalysisUtil;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.io.File;
import java.util.List;
import java.util.Map;


/**
 * 平台北向MQTT设备信号配置
 *
 * @author zV15174
 * @date 2023/7/22 10:46
 */
@Data
@Log4j2
@Configuration
@ConfigurationProperties(prefix = "platform-northbound-mqtt")
public class MqttDeviceSignalConfig {

    private String ip;

    private int port;

    private String username;

    private String password;

    private String mqttExcelConfig;

    private int[] mqttExcelSheetNumbers;

    private Map<String, String> mqttAlarmSignals;

    public static List<MqttExcelItem> excelItems;
    /**
     * 从配置文件加载配置项
     */
    @PostConstruct
    public void loadDeviceSignalConfigFromExcel() {
        String filePath = System.getProperty("user.dir") + File.separator + "config" + File.separator + mqttExcelConfig;

        for (int mqttExcelSheetNumber : mqttExcelSheetNumbers) {

            try {
                excelItems = ExcelAnalysisUtil.getList(filePath, MqttExcelItem.class, mqttExcelSheetNumber, 1);
                for (int i = 0; i < excelItems.size(); i++) {
                    log.info("读取到excel设备信息:{}",excelItems.get(i));
                }
                System.out.println("excelItems = " + excelItems);

            } catch (IllegalAccessException e) {
                log.error("解析配置文件 {} 失败, 失败信息: {}", mqttExcelConfig, e.getMessage(), e);
                throw new RuntimeException(e);
            }

        }
    }
}
package org.example.config;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;


/**
 * @Author: V18302
 * @Date: 2024/05/08 14:28
 * @Description: 对应Excel表格第三方设备数据
 */
@Data
@EqualsAndHashCode
public class MqttExcelItem {

    @ExcelProperty(value = "订阅topic", index = 0)
    private String gateway;
    @ExcelProperty(value = "订阅时间", index = 1)
    private String time;
}

四、效果展示(关于MQTT客户端工具的使用可以看我的上篇文章:使用eqmx搭建MQTT服务端-CSDN博客

;