Listener总结(1)---域对象创建和销毁监听器

原创文章,转载请注明出处

​ 从开年到现在两个月时间,一直想慢慢开始写点东西,将工作以来学到的知识和技能进行总结。但是偏偏年后项目比较急,一个集中刻录项目就写了一个月左右,但是得到的收获也不少。好不容易清明有了一点的空闲,就将之前的一些笔记和开发中遇到的问题好好的总结一下吧。

​ 万事开头难,能提笔开始写这篇文章,相信以后能养成定时总结的习惯,也希望能和大家进行探讨,有错误和疏漏的地方还请不吝赐教。

​ 废话也就不多说了,今天主要总结一下 JavaWeb 开发中最基本的三大类:Listener、Filter与 Servlet中的 Listener。对于 JavaWeb 程序来讲,这三个类是维持其基本运作的基石,在现代开发中,各种框架(最常见的 Spring、Struts2、Hibernate等)为程序员搭建好了舒适的开发环境,以至于让人忽略了之三大类能做到的许多事情。互联网时代每时每刻都在变化,但是这些变化也是基于一些不变的东西,学习一门技术也要从最基本的、不变的底层学起,往上才能开枝散叶,正所谓万变不离其宗。

Listener

​ Listener 是专门用来监听另一个 Java 对象方法的调用或属性改变,实现了特定接口的特殊类 。刚好最近在阅读有关设计模式的书籍,而这里 Listener 正是通过观察者模式进行实现的,而在前一个月做的项目中有多处都用到了这种设计模式。

​ 在 JavaWeb 应用中,被监听的对象主要是:ServletContextHttpSessionServletRequest

​ 针对以上三个对象的操作,Listener 又被划分成为三种类型:

  • 域对象创建和销毁的事件监听器
  • 域对象中属性改变的事件监听器
  • 绑定到 HttpSession域中的某个对象的状态的事件监听器

域对象创建和销毁的事件监听器

​ 这类Listener的作用是监听对象的创建和销毁,自定义的Listener 类需要实现特定接口中的两个方法:XXXInitialized,XXXDestroyed。下面的代码显示了针对不同的被监听对象的Listener实现:

web.xml

<listener>
    <listener-class>com.yzhang.listener.test.MyServletContextListener</listener-class>
</listener>

<listener>
    <listener-class>com.yzhang.listener.test.MyHttpSessionListener</listener-class>
</listener>

<listener>
    <listener-class>com.yzhang.listener.test.MyServletRequestListener</listener-class>
</listener>

MyServletContextListener

//***
public class MyServletContextListener implements ServletContextListener {

    private static Logger logger = Logger.getLogger(MyServletContextListener.class);

    public void contextInitialized(ServletContextEvent sce) {
        logger.debug("servletContextInitialized");
    }

    public void contextDestroyed(ServletContextEvent sce) {
        logger.debug("servletContextDestroyed");
    }
}

MyHttpSessionListener

//***
public class MyHttpSessionListener implements HttpSessionListener {

    private static Logger logger = Logger.getLogger(MyHttpSessionListener.class);

    public void sessionCreated(HttpSessionEvent se) {
        logger.debug("sessionCreated");
        logger.debug(se.getSession().getId());
    }

    public void sessionDestroyed(HttpSessionEvent se) {
        logger.debug("sessionDestroyed");
    }
}

MyServletRequestListener

public class MyServletRequestListener implements ServletRequestListener {

    private static Logger logger = Logger.getLogger(MyServletRequestListener.class);

    public void requestInitialized(ServletRequestEvent sre) {
        logger.debug("requestInitialized");
        HttpServletRequest request = (HttpServletRequest)sre.getServletRequest();
        logger.debug(request.getRequestURL());
    }

    public void requestDestroyed(ServletRequestEvent sre) {
        logger.debug("requestDestroyed");
    }
}

​ ServletContextListener 监视的是整个 Servlet 容器对象,当 Servlet 容器启动时,监听器的contextInitialized方法被调用,当这个方法被调用时,其实传达的是这么一条信息:我(Servlet 容器)已经启动完成,你(对应的程序)可以开始你自己的工作了。所以在contextInitialized中往往做一些初始化的工作,同理contextDestroyed在 Servlet 容器对象被销毁时调用,可做一些收尾工作。

​ HttpSessionListener监视的是 Session 对象,在整个 web 应用生命周期中,每当有 Session 被创建和销毁时,都会调用其中的sessionCreatedsessionDestroyed,在方法中可以通过HttpSessionEvent.getSession()来获取 Session 的详细信息。

​ ServletRequestListener与 HttpSessionListener 大致相同,区别在于其监视的是 ServletRequest 对象,通俗来讲:每当有一次页面访问请求到达,都会创建一个ServletRequest 对象,从而导致requestInitialized方法被调用,而当这次请求返回以后,ServletRequest 对象被销毁时,requestDestroyed方法将被调用。

​ 进行一次实验,启动 Tomcat -> 访问index.jsp ,将得到下面的输出:

Tomcat 输出1.png

​ 很容易可以分析出各监听器的调用顺序,这里就不过多赘述。

ServletContextListener 的应用——— Spring 的入口

​ 大家肯定都记得在使用 Spring 的时候需要在 Web.xml 中配置一个 Spring 的监听器:

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:/applicationContext.xml</param-value>
</context-param>

​ context-param 是 Spring 的配置文件位置,在 Spring 启动后会根据applicationContext.xml中的配置对 Bean 进行实例化,这里主要观察org.springframework.web.context.ContextLoaderListener这个监听器。

​ 当我们进入到这个监听器内部会发现,这个监听器正是实现了 ServletContextListener,这也符合之前所讲:在contextInitialized中程序可以进行自己的初始化工作了。在这里Spring就会开始自己实际的工作。

//...
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
    public ContextLoaderListener() {
    }

    public ContextLoaderListener(WebApplicationContext context) {
        super(context);
    }

    public void contextInitialized(ServletContextEvent event) {
        this.initWebApplicationContext(event.getServletContext());
    }

    public void contextDestroyed(ServletContextEvent event) {
        this.closeWebApplicationContext(event.getServletContext());
        ContextCleanupListener.cleanupAttributes(event.getServletContext());
    }
}

​ 而在initWebApplicationContext方法中将会进行 Bean 的初始化等工作。


​ 下面看一个在项目中实际遇到过的问题,新手常常会犯这种错误,如果没有理解 Listener 的作用时间和范围,往往不能很快找出错误原因。

在一个刻录系统需要维护多条线程,每条线程分别与不同的刻录机服务通过 WebService 进行通信。在程序启动时,需要从数据库中获取用户配置过的刻录机信息,并启动相应的线程。这里可以很快想到的做法是实现一个 ServletContextListener,并在contextInitialized中调用PrinterService.listAllLivePrinter()方法获取活跃的刻录机列表,然后开启线程。

​ 然而事与愿违,在程序启动时报错:找不到 PrinterService。问题就出在自己实现的ServletContextListener和 Spring 的ContextLoaderListener同样都是对 Servlet 容器的监听,那么谁先执行呢?结论是在 web.xml中谁排在前面谁先执行。

​ 这里犯的第一个错误就是将自定义的 ServletContextListener放在了 Spring 的 Listener 之前。在进一步的测试中,把自定义 Listener 放到 Spring 的 Listener 之后,并且在contextInitialized方法中以如下形式调用:

private UserService userService;
private WebApplicationContext springContext;

public void contextInitialized(ServletContextEvent sce) {
        logger.debug("servletContextInitialized");
        springContext = WebApplicationContextUtils.getWebApplicationContext(sce.getServletContext());
        if (springContext!=null){
            logger.debug("get Spring ApplicationContext success");
            userService = (UserService) springContext.getBean("userService");
        }else{
            logger.error("get SpringContext failed");
            return;
        }
        userService.listAllAlive();
        logger.debug("call userService.listAllAlive() success");
    }

​ WebApplicationContext 描述的是 Web 应用中的Spring上下文,这里需要通过直接获取 bean 的方式得到 userService 实例对象。修改以后的控制台输出:

自定义 listener.png

​ 可以看到,在自定义的 Listener 中成功获取到userService 并调用了userService.listAllAlive()。稍作总结,在这个例子中,想要在程序初始化时做一些工作,有两点值得我们注意的地方:

  1. 在自定义的 ServiceContextListener 中获取 Spring 装配的实例对象,必须要将自定义 Listener 写到 Spring 的 ContextLoaderListener 之后。
  2. 新手可能直接会在自定义 Listener中new UserService(),这么做说明根本没有理解 Spring 的IoC,new出来的对象与 Spring Bean 工厂生成的根本不是同一个实例,其中的各种 dao 必然没有注入,当调用需要 dao 的方法时(往往是必然的)则会报 NPE。

​ 通过这个例子可以对 ServletContextListener 有更深的认识,而对于其他两个 Listener,除了监听的对象不同,在用法上都是大同小异,在理解了其调用关系之后应该能够很快上手使用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容