上篇Blog详细介绍了Servlet的过滤器,了解到过滤器类似一个AOP的概念,这篇Blog就来学习下Servlet的监听器,了解下Java Web的第三大组件的用处是什么,又是怎么工作的。
监听器基本概念
在JSP技术中我们学习了4大作用域:pageContext、request、session、application,用于实现数据共享,其中request、session、application三个作用域分别对应于我们
- HttpServletRequest接口:请求作用域对象
- HttpSession接口:会话作用域对象
- ServletContext接口:全局作用域对象
虽然有了数据共享的作用域,但是数据的具体流转过程我们看不到,比如作用域对象什么时候创建和销毁的,什么时候存取、改变和删除的,所以无法在指定的时机对数据和对象进行操作,这个时候就需要Servlet监听器来发挥作用了:
- 事件:方法调用、属性改变、状态改变等,这里对应对象的创建与销毁事件,属性改变事件,以及额外对HttpSession附加的监听 HttpSession中的对象状态改变事件。
- 事件源:被监听的对象,这里是HttpServletRequest、HttpSession、ServletContext。
- 监听器:用于监听事件源对象 ,事件源对象状态的变化都会触发监听器,这里是我们创建的一个Servlet监听器
- 注册监听器:将监听器与事件源进行绑定
所以一句话描述就是:给事件源注册好监听器后,当某个事件发生引起事件源变化时,监听器监听到这一变化进行逻辑处理
监听器作用
Servlet监听器是Servlet规范中定义的一种特殊类,用于监听servletContext(application)、httpsession(session)、servletRequest(request)
三个作用域对象的创建与销毁事件,以及监听这些作用域对象中属性发生修改的事件,说白了就是监听三个作用域对象的生命周期。
创建监听器
用IDEA可以很轻松的创建一个监听器出来,按照如下步骤:
可以看到IDEA自动帮我们生成了监听器代码模板:
package com.example.myfirstweb.controller; /** * * @Name ${NAME} * * @Description * * @author tianmaolin * * @Data 2021/7/27 */ import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.annotation.*; @WebListener public class ServletListener implements ServletContextListener, HttpSessionListener, HttpSessionAttributeListener { public ServletListener() { } @Override public void contextInitialized(ServletContextEvent sce) { /* This method is called when the servlet context is initialized(when the Web application is deployed). */ } @Override public void contextDestroyed(ServletContextEvent sce) { /* This method is called when the servlet Context is undeployed or Application Server shuts down. */ } @Override public void sessionCreated(HttpSessionEvent se) { /* Session is created. */ } @Override public void sessionDestroyed(HttpSessionEvent se) { /* Session is destroyed. */ } @Override public void attributeAdded(HttpSessionBindingEvent sbe) { /* This method is called when an attribute is added to a session. */ } @Override public void attributeRemoved(HttpSessionBindingEvent sbe) { /* This method is called when an attribute is removed from a session. */ } @Override public void attributeReplaced(HttpSessionBindingEvent sbe) { /* This method is called when an attribute is replaced in a session. */ } }
可以看到监听器是通过注解定义的,我们来看下注解的源代码:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package javax.servlet.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface WebListener { String value() default ""; }
监听器常用方法
Servlet 规范中定义了 8 个监听器接口,可以用于监听 ServletContext、HttpSession 和 ServletRequest 对象的生命周期和属性变化事件。开发 Servlet 监听器需要实现相应的监听器接口并重写接口中的方法:
监听对象创建和销毁的监听器
Servlet 规范定义了监听 ServletContext、HttpSession、HttpServletRequest 这三个对象创建和销毁事件的监听器
监听属性变更的监听器
Servlet 规范定义了监听 ServletContext、HttpSession、HttpServletRequest 这三个对象中的属性变更事件的监听器,这三个监听器接口分别是 ServletContextAttributeListener、HttpSessionAttributeListener 和 ServletRequestAttributeListener。这三个接口中都定义了三个方法,用来处理被监听对象中属性的增加,删除和替换事件。同一种事件在这三个接口中对应的方法名称完全相同,只是参数类型不同
监听 Session中对象状态改变的监听器
Session 中的对象可以有多种状态:绑定到 Session 中、从 Session 中解除绑定、随 Session 对象持久化到存储设备中(钝化)、随 Session 对象从存储设备中恢复(活化)。Servlet 规范中定义了两个特殊的监听器接口,用来帮助对象了解自己在 Session 中的状态:HttpSessionBindingListener 接口和 HttpSessionActivationListener 接口 ,实现这两个接口的类不需要进行注册
监听器实现网站在线人数监听
监听网站的在线人数,也就是统计服务器中的session被创建多少次,创建一次在线人数+1;session销毁一次在线人数-1。
JSP页面
假设登录成功,则跳转到index.jsp页面,和上一篇的过滤器梦幻联动,所有页面请求先到login.jsp,登录后才会跳转,这里我们模拟的是已经登录成功后的跳转:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.*" errorPage="jsp/error.jsp" %> <%@ include file="jsp/top.jsp" %> <!DOCTYPE html> <html> <head> <title>JSP - Hello World</title> <%! int number = 0;// 声明全局变量 int count() { number++;//number 自增 return number; }%> </head> <h1><%= "Hello World!" %> </h1> <br/> <body> <% String print = "我是脚本语句内容"; List<String> strList = new ArrayList<>(); strList.add("脚本语句1"); strList.add("脚本语句2"); strList.add("脚本语句3"); // strList.add("脚本语句4"); //这是一个Java注释,用来注释Java代码,查看源码时不可见 %> <a href="hello-servlet">我的第一个JavaWeb项目</a> <!-- 这是一个html注释,可见<%=new Date().getTime() %> --> <br/> <%--这是一个JSP注释,查看源码时不可见--%> 声明语句示例:这是第 <%=count()%> 次访问该页面 <br/> 表达式语句示例: <%="我是一个表达式语句"%> <br/> 脚本语句示例: <%=print%>, 打印列表 <%=strList%> </body> </html>
Servlet页面
我们准备两个页面,一个页面用于跳转Servlet到index.jsp,60秒后自动退出:
package com.example.MyFirstJavaWeb; import java.io.*; import javax.servlet.http.*; import javax.servlet.annotation.*; @WebServlet(name = "helloServlet", value = "/hello-servlet") public class HelloServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { //假设登录成功 HttpSession session = request.getSession(); session.setMaxInactiveInterval(60); //60秒后自动推出 response.sendRedirect("index.jsp"); } public void destroy() { } }
还有一个用于手动退出session的页面:
package com.example.MyFirstJavaWeb; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; @WebServlet(name = "StopSession", value = "/stop-servlet") public class StopSession extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { //假设退出成功 HttpSession session = request.getSession(); session.invalidate(); } public void destroy() { } }
Servlet监听器
接下来就是我们监听器的代码了,在application初始化时我们增加一个数量统计,然后在session创建时进行累加,销毁时累减:
package com.example.MyFirstJavaWeb.listener; import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.annotation.*; import java.util.Date; @WebListener public class CountListener implements HttpSessionListener,ServletContextListener { public CountListener() { } //监听application销毁事件--即服务器关闭 @Override public void contextDestroyed(ServletContextEvent sce) { System.out.println("application销毁~~~~~~~~"); } //监听application创建事件--即服务器开启 @Override public void contextInitialized(ServletContextEvent sce) { System.out.println("application创建~~~~~~~~"); int sessionCount = 0;//服务器开启的时候,定义变量用于统计在线人数,保存起来 ServletContext application = sce.getServletContext(); application.setAttribute("sessionCount", sessionCount); } //监听session创建事件--即有人登陆成功,进入项目主页面,则在线人数+1 @Override public void sessionCreated(HttpSessionEvent hse) { System.out.println("登陆成功"+hse.getSession().getId()+"创建时间:"+new Date()); ServletContext application = hse.getSession().getServletContext(); int sessionCount = (int)application.getAttribute("sessionCount"); application.setAttribute("sessionCount", ++sessionCount); System.out.println("当前在线用户数为:"+application.getAttribute("sessionCount")); } //监听session销毁事件--销毁有很多种方式:强制销毁、自动过期等,无所谓什么方式,只要session销毁就可以监听到 @Override public void sessionDestroyed(HttpSessionEvent hse) { System.out.println("退出成功"+hse.getSession().getId()+"退出时间:"+new Date()); ServletContext application = hse.getSession().getServletContext(); int sessionCount = (int)application.getAttribute("sessionCount"); application.setAttribute("sessionCount", --sessionCount); System.out.println("当前在线用户数为:"+application.getAttribute("sessionCount")); } }
实现效果
这样当我们每次通过不同浏览器访问页面都会增加一个新的用户,整个变化过程如下:
注意要把这个勾掉,否则idea在启动时会额外给你生成一个session,那么启动时就会有两个session了。
总结一下
其实Servlet监听器最大的作用就是被当作状态机,其实我们任何流程中,抽象出来都是在通过事件对事件源的生命周期管理来实现的,对象在事件驱动下在流程中流转。系统设计时也一样,所以其实监听器能让我们感悟更多的其实是事件驱动生命周期这种设计思想,正如过滤器给我们更多的感悟是切面AOP对所有服务的控制一样,更重要的是思想,而组件只是思想的落地。有了思想我们可以应用再更多其它地方。