1. 背景
因项目特殊需求,需要将之前使用的阿里云IOT消息通信中间件换掉,经过一段时间的预研,评估,和比较。综合成本、与业务的匹配度以及修改工作量这三个大方面的考虑。决定选用开源的分布式MQTT消息服务器EMQX作为替代方案。
2. 消息通信流程
简要介绍下项目通过EMQX进行消息通信的流程:
3. 服务端的核心工作
参考官方的文档,服务端的操作其实比较简单。本项目的服务端也作为EMQX消息服务器的一个连接客户端,主要操作有三个
- 建立连接
- 发布消息
- 订阅消息
Java客户端使用示例:https://www.emqx.com/zh/blog/how-to-use-mqtt-in-java
参考Java客户端订阅消息的样例代码:
- 首先通过消息服务器节点的地址,new 一个MqttClient连接对象
- 然后给MqttClient设置回调
- 这里是通过new 一个MqttCallback对象 来实现对topic中消息的监听和处理,所以服务端的处理心跳消息的主要业务逻辑均在MqttCallback实例中的messageArrived方法中
- 建立连接
- 订阅topic
package io.emqx.mqtt;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
public class SubscribeSample {
public static void main(String[] args) {
String broker = "tcp://broker.emqx.io:1883";
String topic = "mqtt/test";
String username = "emqx";
String password = "public";
String clientid = "subscribe_client";
int qos = 0;
try {
MqttClient client = new MqttClient(broker, clientid, new MemoryPersistence());
// 连接参数
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(username);
options.setPassword(password.toCharArray());
options.setConnectionTimeout(60);
options.setKeepAliveInterval(60);
// 设置回调
client.setCallback(new MqttCallback() {
public void connectionLost(Throwable cause) {
System.out.println("connectionLost: " + cause.getMessage());
}
public void messageArrived(String topic, MqttMessage message) {
System.out.println("topic: " + topic);
System.out.println("Qos: " + message.getQos());
System.out.println("message content: " + new String(message.getPayload()));
}
public void deliveryComplete(IMqttDeliveryToken token) {
System.out.println("deliveryComplete---------" + token.isComplete());
}
});
client.connect(options);
client.subscribe(topic, qos);
} catch (Exception e) {
e.printStackTrace();
}
}
}
服务端与消息服务器建立连接,并一次性订阅三类设备端(UAV、APP、WEB)的消息。这个操作只需在服务项目启动成功后执行一次。这里采用了自定义一个ApplicationListener监听类,在onApplicationEvent()方法中执行这一段逻辑。具体代码如下:
/**
* emqx消息监听器,项目启动时执行一次(与远程emqx消息服务器)建立连接的操作,并对三类产品的topic进行消息监听
* @Author kyrielx
* @Date 2023/8/24 9:48
**/
@Component
public class EmqxMsgListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// Spring容器初始化完成后,需要执行的业务逻辑(仅执行一次)
if(event.getApplicationContext().getDisplayName().equals("Root WebApplicationContext")){
// 1.创建mqttClient对象
MqttClient mqttClient = new MqttClient(broker, clientId, persistence);
// 2.设置消息回调
...
mqttClient.setCallback(new EmqxMsgCallback());
...
// 3.与emqx消息服务器建立连接
mqttClient.connect(connOpts);
// 4.订阅三类产品的topic
mqttClient.subscribe(uavPubTopic, emqxConfig.qos); // 订阅无人机消息
mqttClient.subscribe(phonePubTopic, emqxConfig.qos); // 订阅APP消息
mqttClient.subscribe(webPubTopic, emqxConfig.qos); // 订阅web消息
}
}
消息监听回调类:
@Component
public class EmqxMsgCallback implements MqttCallback {
// 连接丢失
public void connectionLost(Throwable cause) {
System.out.println("connectionLost: " + cause.getMessage());
}
// 消息到达
public void messageArrived(String topic, MqttMessage message) {
System.out.println("topic: " + topic);
System.out.println("Qos: " + message.getQos());
System.out.println("message content: " + new String(message.getPayload()));
}
// 消息传递完成
public void deliveryComplete(IMqttDeliveryToken token) {
System.out.println("deliveryComplete---------" + token.isComplete());
}
}
在messageArrived()方法中监听到消息后执行核心的一些业务逻辑。
4. 问题描述
前面铺垫了太多,问题来了。
在消息监听的回调类中,由于要进行很多业务逻辑的处理,需要引入多个相关的service层的bean。比如:收到APP发送给UAV的下发航线的指令消息,指令消息的内容可能只有一个待下发航线的id,而服务端接收到指令消息后,需要解析出消息体中的航线id,然后通过service层的方法去查库取数据文件,通过下载链接或加密数据的方式转发给UAV设备。
还有,在EmqxMsgListener监听类中,与EMQX消息服务器建立连接,得到了一个mqttClient连接对象,而在EmqxMsgCallback中进行消息转发时需要通过mqttClient.publish()执行。因此EmqxMsgCallback类中需要引入在EmqxMsgListener类中创建并初始化完成的mqttClient对象,在messageArrived()方法中使用。
这里,我想通过@AutoWired注解引入bean对象。于是在回调类EmqxMsgCallback头部也添加了@Component注解,使其也被Spring容器管理。
由于多个类需要共用一些变量或对象,而且在A类中赋值,在B类中使用。于是,我就将这些共用的变量或对象封装在一个实体类中,并注册成bean。
/**
* @Description Emqx执行时公用的参数
* @Author kyrielx
* @Date 2023/8/28 10:37
**/
@Data
@ToString
@Component
public class EmqxEntity {
private String ServerUrl; // 服务接口请求地址前缀
private MqttClient mqttClient; // mqtt连接客户端对象
}
然后在A类中通过@AutoWried引入该bean,并给其属性赋值:
在B类中,也通过@AutoWried引入该bean,并进行使用。
结果,启动项目后,在B类中使用bean的地方报了空指针异常
5. 问题排查
为什么一个已经交给Spring容器管理的bean,在A类中引入,并修改了值,再在B类中引入,会失败,而且A类和B类也都已经注册成了bean?
在排查许久后发现了问题所在。
原因在这里:
== 在A类中我们引入了EmqxEntity这个bean,并修改了它的值,但是B类的实例是通过new出来的,那么在B类的实例中去注入bean,就会为null。
这里我理解是因为new出来的实例对象不由Spring容器管理,所以在其内部注入的bean会失效。==
6. 解决思路
要想B类中的bean能够正常使用:有两种方案:
- 在B类中需要使用bean的地方,去通过getBean()方法动态获取
- 在A中设置回调实例的时候,使用注入的bean。不要使用new去创建
由于B中的业务逻辑繁杂,分散在很多方法中,所以采用第一种方案,重复代码会很多,可维护性不是很好,也不安全。
因此我这里选用第二种方案:
7. 总结
在Spring项目中使用bean的时候要注意,涉及到需要自定义bean,多处操作bean和使用bean的场景下,需要将相关联的一些类啥的都统一交给Spring容器管理,不要私自去图方便,用new创建实例对象。
EMQX文档里的示例仅供参考,方便建demo做测试。不要图方便直接复制粘贴。
8. 参考
参考链接:https://blog.csdn.net/mike__cool/article/details/84335245