JavaWeb项目一般会把web.xml
中的<listener>
作为系统启动的入口,系统封装的基础组件也会在这里初始化,比如日志组件、缓存组件、数据库组件等等,包括SpringWeb也是在这里初始化。在搭建框架、整合环境的时候,经常会遇到因为Listener加载问题而导致的系统无法正常启动,这里我们就来探究下<listener>
的加载顺序问题。
在Listener加载之前
在Listener加载之前,web容器首先会加载<context-param>
的内容,以键值对的形式保存到ServletContext这个上下文中,供Web内部系统级访问。监听器就可以从这里获取相应的配置信息。例如:
SpringWeb的上下文监听器(同样也是SpringWeb初始化的入口)
org.springframework.web.context.ContextLoaderListener
可以从ServletContext获取Spring配置文件的路径信息,当然如果不指定的话也会有一个默认地址/WEB-INF/applicationContext.xml
。
Listener加载顺序
- web容器对同类型的Listener,按
web.xml
中配置的先后顺序依次加载。 - 特别的,
javax.servlet-api
从3.0版本开始提供@WebListener
注解,可以直接在自定义监听器类上加上该注解而不用再到web.xml
中进行配置,注解的Listener在加载时,优先级低于web.xml
中配置的同类型的Listener。
这里的同类型,我们仅讨论实现了javax.servlet.ServletContextListener
接口的Listener。其他类型的Listener,因为实现的接口不同,对应的功能和生命周期也有所不同。
下面我们用自定义的Listener验证一下以上结论。
我们定义了3个Listener,分别为TestListener1
,TestListener2
,TestListener3
,各自实现ServletContextListener
接口。
TestListener1
public class TestListener1 implements ServletContextListener {
protected Logger log = Logger.getLogger(TestListener1.class);
public void contextInitialized(ServletContextEvent sce) {
log.info("TestListener1 Initialized.");
}
public void contextDestroyed(ServletContextEvent sce) {
log.info("TestListener1 Destroyed.");
}
}
TestListener2
public class TestListener2 implements ServletContextListener {
protected Logger log = Logger.getLogger(TestListener2.class);
public void contextInitialized(ServletContextEvent sce) {
log.info("TestListener2 Initialized.");
}
public void contextDestroyed(ServletContextEvent sce) {
log.info("TestListener2 Destroyed.");
}
}
TestListener3
public class TestListener3 implements ServletContextListener {
protected Logger log = Logger.getLogger(TestListener3.class);
public void contextInitialized(ServletContextEvent sce) {
log.info("TestListener3 Initialized.");
}
public void contextDestroyed(ServletContextEvent sce) {
log.info("TestListener3 Destroyed.");
}
}
在web.xml
配置上对应的监听器,这里为了排除类加载顺序的可能性,故意把顺序打乱。
<listener>
<listener-class>com.kuku.demo.listener.TestListener1</listener-class>
</listener>
<listener>
<listener-class>com.kuku.demo.listener.TestListener3</listener-class>
</listener>
<listener>
<listener-class>com.kuku.demo.listener.TestListener2</listener-class>
</listener>
我们来看下web启动后的日志
[INFO ] 2019-03-14 20:13:06,872(0) --> [main] com.kuku.demo.listener.TestListener1.contextInitialized(TestListener1.java:18): TestListener1 Initialized.
[INFO ] 2019-03-14 20:13:06,876(4) --> [main] com.kuku.demo.listener.TestListener3.contextInitialized(TestListener3.java:18): TestListener3 Initialized.
[INFO ] 2019-03-14 20:13:06,876(4) --> [main] com.kuku.demo.listener.TestListener2.contextInitialized(TestListener2.java:18): TestListener2 Initialized.
可以看到,Listener加载的顺序是严格按照web.xml
里配置的顺序,并且都是由主线程[main]
来完成初始化。
然后我们再来看看@WebListener
注解的Listener。
TestLisener
@WebListener
public class TestListener implements ServletContextListener {
protected Logger log = Logger.getLogger(TestListener.class);
public void contextInitialized(ServletContextEvent sce) {
log.info("TestListener Initialized.");
}
public void contextDestroyed(ServletContextEvent sce) {
log.info("TestListener Destroyed.");
}
}
在这个包的外层再新建一个TestListener4
@WebListener
public class TestListener4 implements ServletContextListener {
protected Logger log = Logger.getLogger(TestListener4.class);
public void contextInitialized(ServletContextEvent sce) {
log.info("TestListener4 Initialized.");
}
public void contextDestroyed(ServletContextEvent sce) {
log.info("TestListener4 Destroyed.");
}
}
看下启动后的日志
[INFO ] 2019-03-14 22:36:26,494(0) --> [main] com.kuku.demo.listener.TestListener1.contextInitialized(TestListener1.java:19): TestListener1 Initialized.
[INFO ] 2019-03-14 22:36:26,497(3) --> [main] com.kuku.demo.listener.TestListener3.contextInitialized(TestListener3.java:18): TestListener3 Initialized.
[INFO ] 2019-03-14 22:36:26,497(3) --> [main] com.kuku.demo.listener.TestListener2.contextInitialized(TestListener2.java:19): TestListener2 Initialized.
[INFO ] 2019-03-14 22:36:26,497(3) --> [main] com.kuku.demo.listener.TestListener.contextInitialized(TestListener.java:20): TestListener Initialized.
[INFO ] 2019-03-14 22:36:26,497(3) --> [main] com.kuku.demo.TestListener4.contextInitialized(TestListener4.java:20): TestListener4 Initialized.
可以看到,TestListener1
、TestListener3
、TestListener2
的顺序没有改变,然后才是依次加载TestListener
、TestListener4
,至于同样通过@WebListener
注解的监听器,加载顺序可能和包路径的遍历顺序有关。
所以,如果你的web项目对监听器加载顺序比较重要,建议还是在 web.xml
里配置,并添加相应的注释,标明每个监听器的作用以及排序,便于后期人员维护。
自定义Listener遇到的问题
有这样一个场景,系统每次启动/终止需要往数据库增加一条记录,记录当前的时间,这时可以通过监听器来实现(当然也有其他实现方式),但是我们又希望通过Spring注解的方式来调用相应的服务,那该怎么办呢?
既然要使用Spring的注解功能,SpringWeb的初始化要先于我们的自定义Listener初始化,
即org.springframework.web.context.ContextLoaderListener
要配置在自定义Listener之前。
当然还有一种方法,就是我们的自定义Listener继承ContextLoaderListener
,然后重写contextInitialized
方法和contextDestroyed
。
CustomListener
package com.kuku.demo.listener;
import com.kuku.demo.springmvc.service.DBService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.ContextLoaderListener;
import javax.servlet.ServletContextEvent;
/**
* @author kuku713
* @description
* @date 2019-03-14
*/
public class CustomListener extends ContextLoaderListener {
@Autowired
private DBService dbService;
@Override
public void contextInitialized(ServletContextEvent event) {
super.contextInitialized(event);
dbService.write("启动");
}
@Override
public void contextDestroyed(ServletContextEvent event) {
dbService.write("终止");
super.contextDestroyed(event);
}
}
web.xml
<listener>
<listener-class>com.kuku.demo.listener.CustomListener</listener-class>
</listener>
然而一启动,就直接NPE
,原因在于dbService
没有注入。为什么?
其实很简单,要在一个Bean或者类中通过注解的方式获得注入(即依赖注入),这个Bean或者类必须是由Spring管理的,而Listener在初始化过程中是无法被Spring托管的。
那有没有其他办法在CustomListener
中获取到dbService
实例呢?
有两种方法:
- 通过
ApplicationContext
获取BeanFactory
,然后进一步获取Bean对象。
private DBService dbService;
@Override
public void contextInitialized(ServletContextEvent event) {
super.contextInitialized(event);
ApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(event.getServletContext());
dbService = applicationContext.getBean(DBService.class);
dbService.write("启动");
}
- 通过
SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
重新完成注入。
@Autowired
private DBService dbService;
@Override
public void contextInitialized(ServletContextEvent event) {
super.contextInitialized(event);
SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
dbService.write("启动");
}
第二种方法,是基于当前web应用程序上下文,处理给定目标对象的@Autowired注入。
/**
* Process {@code @Autowired} injection for the given target object,
* based on the current web application context.
* Intended for use as a delegate.
* @param target the target object to process
* @see org.springframework.web.context.ContextLoader#getCurrentWebApplicationContext()
*/
个人比较倾向使用第二种。
这样就完美解决了上述场景中遇到的问题了。
朋友们在处理监听器的时候还遇到过什么难题呢?欢迎评论,我们一起讨论。