在上篇文章中我们讲述了如何使用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;
}