记一次Spring项目中使用@Autowired注入bean,使用时报空指针异常的问题

1. 背景

因项目特殊需求,需要将之前使用的阿里云IOT消息通信中间件换掉,经过一段时间的预研,评估,和比较。综合成本、与业务的匹配度以及修改工作量这三个大方面的考虑。决定选用开源的分布式MQTT消息服务器EMQX作为替代方案。

2. 消息通信流程

简要介绍下项目通过EMQX进行消息通信的流程:
在这里插入图片描述

3. 服务端的核心工作

参考官方的文档,服务端的操作其实比较简单。本项目的服务端也作为EMQX消息服务器的一个连接客户端,主要操作有三个

  1. 建立连接
  2. 发布消息
  3. 订阅消息

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能够正常使用:有两种方案:

  1. 在B类中需要使用bean的地方,去通过getBean()方法动态获取
  2. 在A中设置回调实例的时候,使用注入的bean。不要使用new去创建

由于B中的业务逻辑繁杂,分散在很多方法中,所以采用第一种方案,重复代码会很多,可维护性不是很好,也不安全。
在这里插入图片描述
在这里插入图片描述

因此我这里选用第二种方案:
在这里插入图片描述
在这里插入图片描述

7. 总结

在Spring项目中使用bean的时候要注意,涉及到需要自定义bean,多处操作bean和使用bean的场景下,需要将相关联的一些类啥的都统一交给Spring容器管理,不要私自去图方便,用new创建实例对象。
EMQX文档里的示例仅供参考,方便建demo做测试。不要图方便直接复制粘贴。

8. 参考

参考链接:https://blog.csdn.net/mike__cool/article/details/84335245

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

kyrielx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值