记MQ消息处理逻辑中使用未实例化SpringBean引发空指针问题的解决

达芬奇密码2018-08-08 10:52

在NCE工程下,存在多个模块(比如Web模块、Container模块、Res模块、OpenApi模块、Timer模块、ImageCheck模块以及RealTime模块等等),在这些模块启动过程中,都会去立即监听相应的MQ消息队列,并进行消息逻辑处理。但在这些处理逻辑中,偶尔会出现空指针异常情况。本文首先重现了该问题出现情境,接着分析了该问题的产生原因,最后记录了该问题的解决方法。

问题出现场景重现

以Web模块为例子,重现该空指针异常情景。

Web模块在模块启动时候,会监听MQ消息。如果此时在其启动之前,其监听的MQ队列中已经堆积了消息,则下次启动时,有很大概率会抛出空指针异常。在日志中的异常记录类似如下:

2016-09-18 11:22:02,516 52555 [Thread-7] (MessageDispatchHandler.java:42) INFO com.netease.cloud.nce.web.listener.MessageDispatchHandler - MessageDispatchHandler:{"uniqueId":"7ae52a21-e203-41fd-910f-4ddbfd279338","cmd":"elasticScaleMicroservice","content":{"replicas":1,"dcId":27,"tenantId":"7543eb1e10184da48e4cb0f7209df1a2","serviceId":0,"code":1,"microserviceId":594}}
2016-09-18 11:22:02,632 52671 [Thread-7] (MessageDispatchHandler.java:154) INFO com.netease.cloud.nce.web.listener.MessageDispatchHandler - message handler process...
2016-09-18 11:22:02,634 52673 [Thread-7] (MQClient.java:253) INFO com.netease.nce.client.MQClient - consume message:{"from":"CONTAINER.qa-control-pub-ci-qa-tomcat","msg":"{\"uniqueId\":\"7ae52a21-e203-41fd-910f-4ddbfd279338\",\"cmd\":\"elasticScaleMicroservice\",\"content\":{\"replicas\":1,\"dcId\":27,\"tenantId\":\"7543eb1e10184da48e4cb0f7209df1a2\",\"serviceId\":0,\"code\":1,\"microserviceId\":594}}","to":"WEB.qa-control-pub-ci-qa-tomcat"}, handle resule:true
Exception in thread "pool-3-thread-1" java.lang.NullPointerException
at com.netease.libs.spring.ApplicationContextHolder.getBean(ApplicationContextHolder.java:19)
at com.netease.cloud.nce.web.listener.ElasticScaleMicroserviceCmdProcess.run(ElasticScaleMicroserviceCmdProcess.java:43)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:745)

详细的重现操作可见Jira: 空指针异常

问题产生原因分析

仍以上面的情境进行问题原因分析。

Web模块采用MQListener监听器,监听Web模块启动事件,该监听器中的处理逻辑如下:

public class MQListener extends ContextLoaderListener {  
    private final static Logger logger = LoggerFactory.getLogger(MQListener.class);  

    public void contextInitialized(ServletContextEvent ctxEvent) {  
        super.contextInitialized(ctxEvent);  
        MQ.consumeMessage(super.getCurrentWebApplicationContext().getBean(MessageDispatchHandler.class));  
        initNos();  
    }  

    private void initNos() {  
        ConfigService config = (ConfigService) getCurrentWebApplicationContext().getBean(ConfigService.class);  
        NosUtils.init(config.getAccessKey(), config.getSecretKey());  
    }  
}

从上述代码可以看出,在Web模块启动的同时,会执行消费MQ消息逻辑操作,即上文的MQ.consumeMessage()方法。

具体看下扩缩容微服务MQ消息处理逻辑(上文中例子日志就是由该部分逻辑打印的),该逻辑中存在以下片段代码:

long microserviceId = Long.valueOf(msgFromCtrl.getContent().get("microserviceId").toString());
int code = (Integer) msgFromCtrl.getContent().get("code");
String uniqueId = msgFromCtrl.getUniqueId();

IMicroserviceService microserviceService = (IMicroserviceService) ApplicationContextHolder.getBean(IMicroserviceService.class);
IAppLogService appLogService = (IAppLogService) ApplicationContextHolder.getBean(IAppLogService.class);
IAlarmService alertService = (IAlarmService) ApplicationContextHolder.getBean(IAlarmService.class);
IEventsService eventsService = (IEventsService) ApplicationContextHolder.getBean(IEventsService.class);

MicroserviceInfo serviceInfo = microserviceService.getMicroserviceInfoById(microserviceId);
if (serviceInfo == null) {
    logger.error("微服务扩容,取数据失败, microserviceId:{}", microserviceId);
    return;
}

重点看下上述片段中的第5~8行代码。这些代码主要是为了从ApplicationContext容器中获取SpringBean实例。从上一小节中的日志记录可知,也正是这几行代码引发了空指针异常。

进一步看下ApplicationContextHolder类的实现代码:

package com.netease.libs.spring;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

public class ApplicationContextHolder implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    public static ApplicationContext getContext() {
        return ApplicationContextHolder.applicationContext;
    }

    public static Object getBean(String beanName) {
        return ApplicationContextHolder.applicationContext.getBean(beanName);
    }

    public static Object getBean(Class requiredType) {
        return ApplicationContextHolder.applicationContext.getBean(requiredType);
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ApplicationContextHolder.applicationContext = applicationContext;
    }
}

因此,可得知调用上述applicationContext对象getBean方法导致异常抛出,即applicationContext对象为空,在没有得到实例化的情况下被使用。我们可以进一步分析出,是由于ApplicationContextHolder类没有得到实例化,从而导致applicationContext对象为空(因为ApplicationContextHolder实现了ApplicationContextAware接口,如果ApplicationContextHolder被实例化,则在其被实例化过程中,必然会被注入ApplicationContext对象,即applicationContext不可能为空)。

再来看下MQListener和ApplicationContextHolder在配置文件中的配置方式。

MQListener配置在Web.xml文件中,配置代码如下:

<listener>  
    <listener-class>com.netease.cloud.nce.web.listener.MQListener</listener-class>  
</listener>  

<servlet>
    <servlet-name>mvc-dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:servlet-context.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

而,ApplicationContextHolder配置在servlet-context.xml文件中,配置代码如下:

<bean id="applicationContextHolder" class="com.netease.libs.spring.ApplicationContextHolder"></bean>

在Tomcat容器启动过程中,会去加载web.xml文件。其按照Listener,Filter,Servlet顺序进行加载。所以Web模块启动时,MQListener监听到Servlet容器启动事件,会执行MQListener.contextInitialized()方法,即执行加载applicationContext.xml配置,初始化ApplicationContext容器操作,将applicationContext.xml文件中配置的SpringBean实例化后放入该容器中,消费MQ消息等操作。

在加载完MQListener后,会去加载DispatcherServlet(NCE中的Web模块,采用Spring中的MVC框架),执行DispatcherServlet.initServletBean()方法,即执行加载servlet-context.xml配置,初始化servlet-context.xml中配置的SpringBean并放入Servlet对应的SpringBean容器中(这其中就包括了ApplicationContextHolder这个SpringBean)等操作。

从上面的MQ消息处理逻辑可知,该逻辑中会使用实例化后的ApplicationContextHolder从ApplicationContext容器中获取SpringBean。在DispatcherServlet.initServletBean()执行过程中,MQ消息逻辑已经运行了,已经在消费处理MQ消息。因此,由于MQ消息处理逻辑与DispatchServlet初始化过程并发执行,所以不能控制ApplicationContextHolder类实例化在MQ消息处理逻辑使用ApplicationContextHodler之前。而在实际的Web启动过程中,由于Web工程庞大,需要执行很多耗时操作,比如处理整个网易云计算基础服务中的微服务、容器、集群服务等等相关的Http请求映射关系等,因此DispatchServlet的初始化过程占时蛮长的。这时,就会有很大概率出现,MQ消息处理逻辑使用未初始化的ApplicationContextHolder类,导致空指针异常被抛出,例如上面重现的情景。

问题解决方法

由上一小节中的问题原因分析可知,MQ消息处理逻辑中的ApplicationContextHolder的使用与其实例化是并发的,因此,很可能导致在使用之前,ApplicationContextHolder仍然没有被初始化,从而引发空指针异常。

如果代码中能够保证,ApplicationContextHolder实例化在MQ消息处理逻辑执行之前,则就能够避免上文中的空指针异常。

从MQListener.contextInitialized()方法中代码可以看出,applicationContext.xml配置文件的加载以及初始化在MQ消息处理逻辑之前,因此,如果ApplicationContextHolder类配置在applicationContext.xml,则可以轻松的解决该空指针问题。但是考虑到ApplicationContextHolder在工程中的许多个地方被使用,因此,贸然改动该配置,可能会影响整个工程,在工程运行出问题时,不利于问题的排查等等。这时,常健大神建议,使用SpringUtils类替代ApplicationContextHolder类从ApplicationContext容器中获取SpringBean实例。SpringUtils类提供了ApplicationContextHolder类似的功能,并且配置在applicationContext.xml文件中,因此,正好适合用以解决该空指针问题。



网易云计算基础服务深度整合了 IaaSPaaS 及容器技术,提供弹性计算、DevOps 工具链及微服务基础设施等服务,帮助企业解决 IT、架构及运维等问题,使企业更聚焦于业务,是新一代的云计算平台。点击可免费试用

网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者邓尉授权发布。