JavaWeb之Listener加载问题

1 篇文章 0 订阅
1 篇文章 0 订阅

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,分别为TestListener1TestListener2TestListener3,各自实现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. 

可以看到,TestListener1TestListener3TestListener2的顺序没有改变,然后才是依次加载TestListenerTestListener4,至于同样通过@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()
*/

个人比较倾向使用第二种。
这样就完美解决了上述场景中遇到的问题了。

朋友们在处理监听器的时候还遇到过什么难题呢?欢迎评论,我们一起讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值