Java Servlet中Listener监听器的原理以及使用方式

1,297 阅读13分钟

这是我参与8月更文挑战的第26天,活动详情查看:8月更文挑战

详细介绍了Java Web Servlet中的Listener监听器的原理以及常见用法。

此前我们学了Java Web中的Filter过滤器的原理以及常见用法:Java Web(11)—Filter过滤器的原理以及用法,现在我们来学习Java Web中的Listener监听器的原理以及用法。

Servlet、Filter、Listener被称为Java Web的三大组件!

1 监听器的概述

监听器就是一个java程序,这个程序专门用于监听另一个java对象的方法调用或属性改变,当被监听对象发生上述事件后,监听器某个方法将立即被执行。

详细的说,就是:监听器用于监听观察某个事件(程序)的发生情况,当被监听的事件真的发生了的时候,事件发生者(事件源) 就会给注册该事件的监听者(监听器)发送消息,告诉监听者某些信息,同时监听者也可以获得一份事件对象,根据这个对象可以获得相关属性和执行相关操作。

监听器可以看成是观察者模式的一种实现。监听器程序中有四种角色:

  1. 监听器(监听者):负责监听发生在事件源上的事件,它能够注册在对应的事件源上,当事件发生后会触发对应的处理方法(事件处理器)的执行。
  2. 事件源(被监听对象,可以产生某些事件的对象):提供订阅与取消监听器的方法,并负责维护监听器列表,以及发送对应的事件给对应的监听器。
  3. 事件对象:事件源发生某个动作时,比如某个增、删、改、查的动作,将该动作将封装为一个事件对象,并且事件源在通知事件监听器时会把这个事件对象传递过去。
  4. 事件处理器:可以是作为监听器的成员方法,也可以独立出来注册到监听器中,当监听器接受到对应的事件时,将会调用对应的方法,或者事件处理器来处理该事件!

在这里插入图片描述

2 Servlet监听器

Servlet中同样存在监听器程序,下面简单的介绍Servlet的的监听器体系!

2.1 事件源

在Servlet规范中定义了多种类型的监听器,它们用于监听的事件源分别是ServletContext,HttpSession和ServletRequest这三个域对象。

2.2 生命周期监听器

生命周期监听器专门用于监听域对象的创建和销毁事件,主要有三个ServletContextListener、HttpSessionListener、ServletRequestListener。

  1. ServletContextListener:Tomcat启动和关闭时调用下面两个方法
    1. default public void contextInitialized(ServletContextEvent evt):ServletContext对象被创建后调用;
    2. default public void contextDestroyed(ServletContextEvent evt):ServletContext对象被销毁前调用;
  2. HttpSessionListener:开始会话和结束会话时调用下面两个方法
    1. default public void sessionCreated(HttpSessionEvent evt):HttpSession对象被创建后调用;
    2. default public void sessionDestroyed(HttpSessionEvent evt):HttpSession对象被销毁前调用;
  3. ServletRequestListener:开始请求和结束请求时调用下面两个方法
    1. default public void requestInitiallized(ServletRequestEvent evt):ServletRequest对象被创建后调用;
    2. default public void requestDestroyed(ServletRequestEvent evt):ServletRequest对象被销毁前调用。

2.2.1 生命周期事件

当监听域对象触发了生命周期事件时,将会被封装为ServletContextEvent、HttpSeessionEvent、ServletRequestEvent事件。

  1. ServletContextEvent:具有getServletContext()方法可以获取ServletContext;
  2. HttpSeessionEvent:具有getSession()方法可以获取HttpSession;
  3. ServletRequestEvent:具有getServletRequest()方法可以获取ServletRequest和getServletContext()方法可以获取ServletContext;

2.3 属性监听器

属性监听器专门用于监听域对象的域属性的增、删、改事件,主要有三个:ServletContextAttributeListener、HttpSessionAttributeListener、ServletRequestAttributeListener。

  1. ServletContextAttributeListener:在ServletContext域进行增、删、改属性时调用下面方法。
    1. default public void attributeAdded(ServletContextAttributeEvent evt):添加域属性
    2. default public void attributeRemoved(ServletContextAttributeEvent evt) :删除域属性
    3. default public void attributeReplaced(ServletContextAttributeEvent evt) :修改域属性
  2. HttpSessionAttributeListener:在HttpSession域进行增、删、改属性时调用下面方法
    1. default public void attributeAdded(HttpSessionBindingEvent evt) :添加域属性
    2. default public void attributeRemoved (HttpSessionBindingEvent evt) :删除域属性
    3. default public void attributeReplaced (HttpSessionBindingEvent evt) :修改域属性
  3. ServletRequestAttributeListener:在ServletRequest域进行增、删、改属性时调用下面方法
    1. default public void attributeAdded(ServletRequestAttributeEvent evt) :添加域属性
    2. default public void attributeRemoved (ServletRequestAttributeEvent evt) :删除域属性
    3. default public void attributeReplaced (ServletRequestAttributeEvent evt) :修改域属性

3 Listener的简单使用

Servlet中监听器的使用很简单,开发人员只需要实现对应的监听器接口,并且只需在web.xml文件中使用<listener>标签或者使用@WebListener注解配置好实现的监听器,由Web容器在Web应用启动时负责统一注册,在时间触发时,将会自动回调监听器的方法。

新建一个项目listener,下面是三个自定义的监听器:

public class MyHttpSessionListener implements HttpSessionListener {
    @Override
    public void sessionCreated(HttpSessionEvent se) {
        System.out.println("Session创建: " + se.getSession());
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        System.out.println("Session销毁: " + se.getSession());
    }
}
public class MyServletContextListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("ServletContext创建: " + sce.getServletContext());
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("ServletContext销毁: " + sce.getServletContext());
    }
}
public class MyServletRequestListener implements ServletRequestListener {
    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        System.out.println("ServletRequest创建: " + sre.getServletRequest());
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        System.out.println("ServletRequest销毁: " + sre.getServletRequest());
    }
}

定义了监听器之后,虽然不需要我们手动注册到事件源中,但是仍然需要告诉Web容器,到底有哪些监听器,我们可以使用传统的web.xml配置。

基于web.xml的配置:

<!--部署监听器-->
<listener>
    <listener-class>com.example.listener.MyHttpSessionListener</listener-class>
</listener>
<listener>
    <listener-class>com.example.listener.MyServletContextListener</listener-class>
</listener>
<listener>
    <listener-class>com.example.listener.MyServletRequestListener</listener-class>
</listener>

在Servlet 3.0之后还可以直接在实现类上使用@WebListener注解,非常方便!

在webapp下准备一个index.html:

<html>
<head>
    <title>listener</title>
</head>
<body>
<h1> Hello</h1>
</body>
</html>

准备两个Servlet,用于测试Session:

@WebServlet("/hello-servlet")
public class HelloServlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        out.println("hello");
        out.close();
    }
}
@WebServlet("/session-servlet")
public class SessionServlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        //获取Session,将会创建Session
        request.getSession();
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        out.println("session");
        out.close();
    }
}

启动项目,可以看到ServletContext创建的事件被触发:

在这里插入图片描述

访问index.html,可以看到ServletRequest的创建和销毁,并且每发起一个请求都会有ServletRequest的创建和销毁事件被触发:

在这里插入图片描述

访问/hello-servlet没有获取session的Servlet,可以看到ServletRequest的创建和销毁,并且每发起一个请求都会有ServletRequest的创建和销毁事件被触发:

在这里插入图片描述

多次调用请求,我们还会发现,实际上ServletRequest的“创建”和“销毁”事件被触发时,可能并没有一个真正的ServletRequest对象被创建和销毁,这里的“创建”和“销毁”事件仅仅是为了表示一个请求已经开始处理以及处理完毕。实际上这里的request是org.apache.catalina.connector.Request对象,是由tomcat服务器创建的,可以被tomcat复用。

在这里插入图片描述

在访问/hello-servlet时,并没有session被创建,我们接着通过 调用/session-servlet访问可以获取session的Servlet,可以看到Session和ServletRequest的创建和销毁:

在这里插入图片描述

这里的Session,就是代码中的request.getSession()方法触发创建的,当我们再次访问这个链接时:

在这里插入图片描述

此时,Session并没有创建,因为处于同一个会话中,我们在另一个浏览器打开,或者将当前浏览器的cookie清理(主要是JSESSIONID的Cookie清理掉),再次访问该链接:

在这里插入图片描述

我们发现,此时又创建了Session,因为请求头中没有JSESSIONID,那么服务器就把这次请求当作是一个新的会话,从而创建新的Session。

等待大约一分钟之后,由于Session的超时时间到了,控制台将会自动输出Session销毁的信息:

在这里插入图片描述

当我们再次访问该链接时,虽然携带了JSESSIONID,但是由于对应的Session已经因为超时而被销毁了,此时又会创建新的Session对象:

在这里插入图片描述

此时我们直接关闭tomcat服务器,将会看到ServletContext销毁的信息:

在这里插入图片描述

注意,如果关闭服务器,是不会触发Session销毁事件的!

4 HttpSession的监听器

还有两个与HttpSession相关的特殊的监听器,HttpSessionBindingListener和HttpSessionActivationListener这两个监听器的特点如下:

  1. 不用在web.xml文件中或者通过注解部署;
  2. 这两个监听器不是给Session添加,而是给Bean添加。即让Bean类实现监听器接口,然后再把Bean对象添加到Session域中;

或许这两个接口可以看作早期的、原始的Spring的Aware感知接口!

4.1 HttpSessionBindingListener

当某个类实现了该接口后,可以感知本类对象(Aware,即感知自己)被添加到Session中,以及感知从session中移除(包括Session销毁时的移除)。 例如让Person类实现HttpSessionBindingListener接口,那么当把Person对象添加到Session中,或者把Person对象从Session中移除时会调用下面两个方法:

  1. default public void valueBound(HttpSessionBindingEvent event):当把监听器对象添加到Session中会调用监听器对象的本方法;
  2. default public void valueUnbound(HttpSessionBindingEvent event):当把监听器对象从Session中移除时会调用监听器对象的本方法;

这里要注意,HttpSessionBindingListener监听器的使用与前面介绍的都不相同,当该监听器的实现类对象添加到Session中,或把该监听器对象从Session移除时才会调用监听器中的方法(包括Session销毁时的移除),并且无需在web.xml文件中部署这个监听器。

下面简单测试一下!

编写Person类,让其实现HttpSessionBindingListener监听器接口:

/**
 * @author lx
 */
public class Person implements HttpSessionBindingListener {

    /*
     * HttpSessionBindingListener的方法
     */

    @Override
    public void valueBound(HttpSessionBindingEvent evt) {
        System.out.println("把Person对象存放到session中:" + evt.getValue());
    }

    @Override
    public void valueUnbound(HttpSessionBindingEvent evt) {
        System.out.println("从session中移除Person对象:" + evt.getValue());
    }

    /*
     * Person的属性
     */

    private String name;
    private int age;
    private String sex;

    public Person(String name, int age, String sex) {
        super();
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + ", sex=" + sex + "]";
    }
}

编写SessionBindingServlet类,一个方法向session中添加Person对象,另一个从session中移除Person对象,通过name参数是否等于add来区分:

/**
 1. @author lx
 */
@WebServlet("/SessionBindingServlet")
public class SessionBindingServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String name = req.getParameter("name");
        if ("add".equals(name)) {
            addPerson(req, resp);
        } else {
            removePerson(req, resp);
        }
    }

    private void addPerson(HttpServletRequest request, HttpServletResponse response) {
        //创建Person并且存入Session中
        Person p = new Person("zhangSan", 23, "male");
        request.getSession().setAttribute("person", p);
    }

    private void removePerson(HttpServletRequest request, HttpServletResponse response) {
        //从Session中移除Person对象
        request.getSession().removeAttribute("person");
    }
}

首先我们通过调用/SessionBindingServlet?name=add尝试向session中添加Person,可以发现Session属性绑定事件被触发了,也就是Person的valueBound方法被执行了!

在这里插入图片描述

接着通过调用/SessionBindingServlet?name=del尝试向session中移除Person,可以发现Session属性移除事件被触发了,也就是Person的valueUnbound方法被执行了!

在这里插入图片描述

如果我们再次尝试执行移除,那么我们会发现由于这个属性已经被移除了,那么属性解绑事件也就不会被触发了。

如果我们的属性没有被移除,并且Sesssion超时了,那么在Session移除事件触发之后,将会触发该Sesssion中的属性的解绑事件(如果可以):

在这里插入图片描述

4.2 HttpSessionActivationListener

Tomcat会在session时间不被使用时钝化session对象,所谓钝化session,就是把session通过序列化的方式保存到硬盘文件中。当用户再使用session时,Tomcat还会把钝化的对象再活化session,所谓活化就是把硬盘文件中的session在反序列化回内存。当session被Tomcat钝化时,session中存储的对象和属性也被纯化,当session被活化时,也会把session中存储的对象活化。

如果某个类实现了HttpSessionActivationListener接口后并且被加入到Session中之后,该类对象可以感知到自己随着session被钝化和活化的事件,并且分别调用该对象的以下两个方法:

  1. public void sessionWillPassivate(HttpSessionEvent se):当对象感知到Session被活化时调用本方法;
  2. public void sessionDidActivate(HttpSessionEvent se):当对象感知到Session被钝化时调用本方法;

HttpSessionActivationListener监听器与HttpSessionBindingListener监听器相似,都是感知型的监听器,例如让Person类实现了HttpSessionActivationListener监听器接口,并把Person对象添加到了session中后,当Tomcat钝化session时,同时也会钝化session中的Person对象,这时Person对象就会感知到自己被钝化了,其实就是调用Person对象的sessionWillPassivate()方法。当用户再次使用session时,Tomcat会活化session,这时Person会感知到自己被活化,其实就是调用Person对象的sessionDidActivate()方法。

注意,因为钝化和活化session,其实就是使用序列化和反序列化技术把session从内存保存到硬盘,和把session从硬盘加载到内存。这说明如果Person类没有实现Serializable接口,那么当session钝化时就不会钝化Person,而是把Person从session中移除再钝化!这也说明session活化后,session中就不在有Person对象了。

由于不能钝化而被移除的时候,如果属性对象实现了HttpSessionBindingListener接口,那么该对象同样会触发valueUnbound事件。另外,Session活化的时候,将会创建新的Session对象,因此会触发Session创建事件。另外,Session钝化并不会影响Session的超时时间,时间到了,Session还是会被销毁。

Session的钝化,需要修改的配置文件,我们在项目的webapp下新增的META-INF目录,并且新增的context.xml文件: 在这里插入图片描述 在context.xml文件中添加下面的配置:

<Context>
    <Manager className="org.apache.catalina.session.PersistentManager" maxIdleSwap="1">
        <Store className="org.apache.catalina.session.FileStore" directory="listener"/>
    </Manager>
</Context>

创建Animal类,让Animal类实现HttpSessionActivationListener和Serializable接口:

/**
 * @author lx
 */
public class Animal implements HttpSessionActivationListener, Serializable {

    /*
     * HttpSessionActivationListener的方法
     */

    @Override
    public void sessionDidActivate(HttpSessionEvent evt) {
        System.out.println("session已经活化: " + evt.getSession());
    }

    @Override
    public void sessionWillPassivate(HttpSessionEvent evt) {
        System.out.println("session被钝化: " + evt.getSession());
    }


    /*
     * Animal的属性
     */

    private String name;
    private int age;
    private String sex;

    public Animal(String name, int age, String sex) {
        super();
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "Animal [name=" + name + ", age=" + age + ", sex=" + sex + "]";
    }
}

创建SessionActivationServlet用于向Session中添加Animal并观察钝化、活化现象:

@WebServlet("/SessionActivationServlet")
public class SessionActivationServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String name = req.getParameter("name");
        if ("add".equals(name)) {
            addAnimal(req, resp);
        } else if ("del".equals(name)) {
            removeAnimal(req, resp);
        } else if ("get".equals(name)) {
            getSession(req, resp);
        }
    }

    private void addAnimal(HttpServletRequest request, HttpServletResponse response) {
        //创建Animal并且存入Session中
        Animal animal = new Animal("Animal", 12, "male");
        request.getSession().setAttribute("animal", animal);
    }

    private void getSession(HttpServletRequest request, HttpServletResponse response) {
        //获取Session
        request.getSession();
    }


    private void removeAnimal(HttpServletRequest request, HttpServletResponse response) {
        //从Session中移除removeAnimal对象
        request.getSession().removeAttribute("animal");
    }
}

添加Animal到Session中之后,等待一分钟,这时session会被钝化(Session超时时间可以设置为2分钟),也就会调用Anmal的sessionWillPassivate()方法,访问这个Session这会使session活化,会调用Anmal的sessionDidActivate()方法。

在这里插入图片描述

5 监听器的应用

监听器机制通常可以实现网站在线人数统计、监听用户的行为(管理员踢人)等功能。当然监听器实际上能做很多事情,比如用于实现业务逻辑解耦,这在Spring中由被称为“事件发布机制”,并且Spring框架提供了更加完善的监听器机制供我们直接使用。

5.1 在线人数统计

在线人数使用HttpSessionListener监听器统计,每当一个session会话建立 在线用户人数+1,每当一个session会话销毁 在线用户人数-1,在线人数的数据采用Context域对象保存。

监听器:

/**
 * @author lx
 */
@WebListener
public class CountListener implements HttpSessionListener {

    @Override
    public void sessionCreated(HttpSessionEvent se) {

        //获取得到Context对象,使用Context域对象保存用户在线的个数
        ServletContext context = se.getSession().getServletContext();

        //直接判断Context对象是否存在这个域,如果存在就人数+1,如果不存在,那么就将属性设置到Context域中
        Integer num = (Integer) context.getAttribute("num");

        if (num == null) {
            context.setAttribute("num", 1);
        } else {
            num++;
            context.setAttribute("num", num);
        }
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        ServletContext context = se.getSession().getServletContext();
        Integer num = (Integer) se.getSession().getAttribute("num");
        context.setAttribute("num", --num);
    }
}

人数统计Servlet:

@WebServlet("/CountServlet")
public class CountServlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        request.getSession();
        ServletContext servletContext = getServletContext();
        Object num = servletContext.getAttribute("num");
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        out.println(num);
    }
}

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!