在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文件中,因此,正好适合用以解决该空指针问题。
网易云计算基础服务深度整合了 IaaS、PaaS 及容器技术,提供弹性计算、DevOps 工具链及微服务基础设施等服务,帮助企业解决 IT、架构及运维等问题,使企业更聚焦于业务,是新一代的云计算平台。点击可免费试用
网易云新用户大礼包:https://www.163yun.com/gift
本文来自网易实践者社区,经作者邓尉授权发布。