会话

会话管理概述

为什么需要会话管理

HTTP 是无状态协议

  • 无状态就是不保存状态,即无状态协议(stateless),HTTP 协议自身不对请求和响应之间的通信状态进行保存,也就是说,在 HTTP 协议这个级别,协议对于发送过的请求或者响应都不做持久化处理
  • 简单理解:浏览器发送请求,服务器接收并响应,但是服务器不记录请求是否来自哪个浏览器,服务器没记录浏览器的特征,就是客户端的状态

举例:张三去一家饭馆点了几道菜,觉得味道不错,第二天又去了,对老板说,还点上次的那几道菜

  • 无状态:老板没有记录张三是否来过,更没有记录上次他点了那些菜,张三只能重新再点一遍
  • 有状态:老板把每次来吃饭的用户都做好记录,查阅一下之前的记录,查到了张三之前的菜单,直接下单

会话管理实现的手段

Cookie 和 Session 配合解决

  • Cookie 是在客户端保留少量数据的技术,主要通过响应头向客户端响应一些客户端要保留的信息
  • Session 是在服务端保留更多数据的技术,主要通过 HttpSession 对象保存一些和客户端相关的信息
  • Cookie 和 Session 配合记录请求状态

举例:张三去银行办业务

  • 张三第一次去某个银行办业务,银行会为张三开户(Session),并向张三发放一张银行卡(Cookie)
  • 张三后面每次去银行,就可以携带之间的银行卡(Cookie),银行根据银行卡找到之前张三的账户(Session)

Cookie 是一种客户端会话技术,Cookie 由服务端产生,它是服务器存放在浏览器的一小份数据,浏览器以后每次访问该服务器的时候都会将这小份数据携带到服务器去。

  • 服务端创建 Cookie,将 Cookie 放入响应对象中,Tomcat 容器将 Cookie 转化为 set-cookie 响应头,响应给客户端
  • 客户端在收到 Cookie 的响应头时,在下次请求该服务的资源时,会以 Cookie 请求头的形式携带之前收到的 Cookie
  • Cookie 是一种键值对格式的数据,从 tomcat8.5 开始可以保存中文,但是不推荐
  • 由于 Cookie 是存储于客户端的数据,比较容易暴露,一般不存储一些敏感或者影响安全的数据

原理图:

servletA 向响应中增加 Cookie:

@WebServlet("/servletA")
public class ServletA extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 创建 Cookie: key-value
        Cookie cookie1 = new Cookie("c1", "c1_message");
        Cookie cookie2 = new Cookie("c2", "c2_message");
        // 将 Cookie 放入响应对象
        resp.addCookie(cookie1);
        resp.addCookie(cookie2);
    }
}

servletB 从请求中读取 Cookie:

@WebServlet("/servletB")
public class ServletB extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取请求中的 Cookie
        Cookie[] cookies = req.getCookies();
        // 迭代 Cookies 数组
        if (null != cookies && cookies.length != 0) {
            for (Cookie cookie : cookies) {
                System.out.println(cookie.getName() + ":" + cookie.getValue());
            }
        }
    }
}

默认情况下 Cookie 的有效期是一次会话范围内,我们可以通过 Cookie 的 setMaxAge() 方法让 Cookie 持久化保存到浏览器上

  • 会话级 Cookie
    • 服务器端并没有明确指定 Cookie 的存在时间
    • 在浏览器端,Cookie 数据存在于内存中
    • 只要浏览器还开着,Cookie 数据就一直都在
    • 浏览器关闭,内存中的 Cookie 数据就会被释放
  • 持久化 Cookie
    • 服务器端明确设置了 Cookie 的存在时间
    • 在浏览器端,Cookie 数据会被保存到硬盘上
    • Cookie 在硬盘上存在的时间根据服务器端限定的时间来管控,不受浏览器关闭的影响
    • 持久化 Cookie 到达了预设的时间会被释放

cookie.setMaxAge(int expiry) 参数单位是秒,表示 Cookie 的持久化时间,如果设置参数为 0,表示将浏览器中保存的该 Cookie 删除

servletA 设置一个 Cookie 为持久化 Cookie:

@WebServlet("/servletA")
public class ServletA extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 创建 Cookie
        Cookie cookie1 = new Cookie("c1", "c1_message");
        cookie1.setMaxAge(60);
        Cookie cookie2 = new Cookie("c2", "c2_message");
        // 将 Cookie 放入响应对象
        resp.addCookie(cookie1);
        resp.addCookie(cookie2);
    }
}

servletB 接收 Cookie,浏览器中间发生一次重启再请求 servletB 测试:

@WebServlet("/servletB")
public class ServletB extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取请求中的 Cookie
        Cookie[] cookies = req.getCookies();
        // 迭代 Cookies 数组
        if (null != cookies && cookies.length != 0) {
            for (Cookie cookie : cookies) {
                System.out.println(cookie.getName() + ":" + cookie.getValue());
            }
        }
    }
}

访问互联网资源时不能每次都需要把所有 Cookie 带上。访问不同的资源时,可以携带不同的 Cookie,可以通过 Cookie 的 setPath(String path) 对 Cookie 的路径进行设置

servletA 设置 Cookie:

public class ServletA extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 创建 Cookie
        Cookie cookie1 = new Cookie("c1", "c1_message");
        // 设置 Cookie 的提交路径
        cookie1.setPath("/web03_war_exploded/servletB");
        Cookie cookie2 = new Cookie("c2", "c2_message");
        // 将 Cookie 放入响应对象
        resp.addCookie(cookie1);
        resp.addCookie(cookie2);
    }
}

向 ServletB 请求时携带携带了 c1,向其他资源请求时就不携带 c1

Session

HttpSession 概述

HttpSession 是一种保留更多信息在服务端的一种技术,服务器会为每一个客户端开辟一块内存空间,即 Session 对象。客户端在发送请求时,都可以使用自己的 Session,这样服务端就可以通过 Session 来记录某个客户端的状态了

  • 服务端在为客户端创建 Session 时,会同时将 Session 对象的 id,即 JSESSIONID 以 Cookie 的形式放入响应对象
  • 后端创建完 Session 后,客户端会收到一个特殊的 Cookie,,叫做 JSESSIONID
  • 客户端下一次请求时携带 JSESSIONID Cookie,后端收到后根据 JSESSIONID 找到对应的 Session 对象
  • 通过该机制,服务端通过 Session 就可以存储一些专门针对某个客户端的信息了
  • Session 也是域对象(后续详细讲解)

原理图如下:

HttpSession 的使用

目标:用户提交 form 表单到 ServletA,携带用户名,ServletA 将用户名存到 Session,用户再请求其他任意 Servlet,获取之前存储的用户

定义表单页,提交用户名:

<form action="servletA" method="post">
	用户名:
	<input type="text" name="username">
	<input type="submit" value="提交">
</form>

定义 ServletA,将用户名存入 Session:

@WebServlet("/servletA")
public class ServletA extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取请求中的参数
        String username = req.getParameter("username");
        // 获取 Session 对象
        HttpSession session = req.getSession();
        // 获取 Session 的 ID
        String jSessionId = session.getId();
        System.out.println(jSessionId);
        // 判断 Session 是不是新创建的
        boolean isNew = session.isNew();
        System.out.println(isNew);
        // 向 Session 对象中存入数据
        session.setAttribute("username", username);
    }
}

定义其他 Servlet,从 Session 中读取用户名:

@WebServlet("/servletB")
public class ServletB extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取 Session 对象
        HttpSession session = req.getSession();
        // 从 Session 中取出数据
        String username = (String) session.getAttribute("username");
        System.out.println(username);
    }
}

getSession 方法的处理逻辑:

HttpSession 时效性

为什么要设置 Session 的时效

  • 用户量很大之后,Session 对象相应的也要创建很多。如果一味创建不释放,那么服务器端的内存迟早要被耗尽。
  • 客户端关闭行为无法被服务端直接侦测,或者客户端较长时间不操作也经常出现,类似这些的情况,就需要对 Session 的时限进行设置了

默认的 Session 最大闲置时间(两次使用同一个 Session 中的间隔时间)在 tomcat/conf/web.xml 配置为 30 分钟:

我们可以自己在当前项目的 web.xml 对最大闲置时间进行重新设定:

也可以通过 HttpSession 的 API 对最大闲置时间进行设定:

// 设置最大闲置时间
session.setMaxInactiveInterval(60);

也可以直接让 Session 失效:

// 直接让 Session 失效
session.invalidate();

三大域对象

域对象概述

域对象:一些用于存储数据和传递数据的对象,传递数据不同的范围,我们称之为不同的域,不同的域对象代表不同的域,共享数据的范围也不同

  • Web 项目中,一定要熟练使用的域对象分别是:请求域、会话域、应用域
  • 请求域对象是 HttpServletRequest,传递数据的范围是一次请求之内及请求转发
  • 会话域对象是 HttpSession,传递数据的范围是一次会话之内,可以跨多个请求
  • 应用域对象是 ServletContext,传递数据的范围是本应用之内,可以跨多个会话

生活举例:热水器摆放位置不同,使用的范围就不同

  1. 摆在张三工位下,就只有张三一个人能用
  2. 摆在办公室的公共区,办公室内的所有人都可以用
  3. 摆在楼层的走廊区,该楼层的所有人都可以用

三大域对象的数据作用范围图解:

域对象的使用

域对象的 API:

API功能
void setAttribute(String name, String value)向域对象中添加/修改数据
Object getAttribute(String name);从域对象中获取数据
removeAttribute(String name);移除域对象中的数据

ServletA 向三大域中放入数据:

@WebServlet("/servletA")
public class ServletA extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 向请求域中放入数据
        req.setAttribute("request", "request-message");
        // 同一个请求,在 servletB 中可以拿到请求域中的数据
        // req.getRequestDispatcher("servletB").forward(req, resp);
 
        // 向会话域中放入数据
        HttpSession session = req.getSession();
        session.setAttribute("session", "session-message");
 
        // 向应用域中放入数据
        ServletContext application = getServletContext();
        application.setAttribute("application", "application-message");
    }
}

ServletB 从三大于中取出数据:

@WebServlet("/servletB")
public class ServletB extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 从请求域中获取数据
        String reqMessage = (String) req.getAttribute("request");
        System.out.println(reqMessage);
        
        // 从会话域中获取数据
        HttpSession session = req.getSession();
        String sessionMessage = (String) session.getAttribute("session");
        System.out.println(sessionMessage);
 
        // 从应用域中获取数据
        ServletContext application = getServletContext();
        String applicationMessage = (String) application.getAttribute("application");
        System.out.println(applicationMessage);
    }
}
  • 请求转发时,请求域可以传递数据,请求域内一般放本次请求业务有关的数据,如:查询到的所有的部门信息
  • 同一个会话内,不用请求转发,会话域可以传递数据,会话域内一般放本次会话的客户端有关的数据,如:当前客户端登录的用户
  • 同一个 APP 内,不同的客户端,应用域可以传递数据,应用域内一般放本程序应用有关的数据,如:Spring 框架的 IOC 容器

过滤器

过滤器概述

Filter,即过滤器,是 JavaEE 技术规范之一,作用目标资源的请求进行过滤的一套技术规范,是 Java Web 项目中最为实用的技术之一

  • Filter 接口定义了过滤器的开发规范,所有的过滤器都要实现该接口
  • Filter 的工作位置是项目中所有目标资源之前,容器在创建 HttpServletRequestHttpServletResponse 对象后,会先调用 FilterdoFilter 方法
  • FilterdoFilter 方法可以控制请求是否继续,如果放行,则请求继续;如果拒绝,则请求到此为止,由过滤器本身做出响应
  • Filter 不仅可以对请求做出过滤,也可以在目标资源做出响应前,对响应再次进行处理
  • Filter 是 GOF 中责任链模式的典型案例
  • Filter 的常用应用包括但不限于:登录权限检查、解决网站乱码、过滤敏感字符、日志记录、性能分析

生活举例:公司前台、停车场安保、地铁验票闸机

  • 公司前台对来访人员进行审核,如果是游客则拒绝进入公司,如果是客户则放行。客户离开时提醒客户不要遗忘物品
  • 停车场保安对来访车辆进行控制,如果没有车位拒绝进入,如果有车位,发放停车卡并放行,车辆离开时收取停车费
  • 地铁验票闸机在人员进入之前检查票,没票拒绝进入,有票验票后放行,人员离开时同样验票

过滤器开发中应用的场景:

  • 日志的记录
  • 性能的分析
  • 乱码的处理
  • 事务的控制
  • 登录的控制
  • 跨域的处理

过滤器工作位置图解:

Filter 接口源码:

package jakarta.servlet;
import java.io.IOException;
 
public interface Filter {
    default public void init(FilterConfig filterConfig) throws ServletException {
    }
 
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException;
 
    default public void destroy() {
    }
}

Filter 接口 API:

API目标
default public void init(FilterConfig filterConfig)初始化方法,由容器调用并传入初始配置信息 filterConfig 对象
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)过滤方法,核心方法,过滤请求,决定是否放行,响应之前的其他处理等都在该方法中
default public void destroy()销毁方法,容器在回收过滤器对象之前调用的方法

过滤器使用

目标:开发一个日志记录过滤器

  • 用户请求到达目标资源之前,记录用户的请求资源路径
  • 响应之前记录本次请求目标资源运算的耗时
  • 可以选择将日志记录进入文件,为了方便测试,这里将日志直接在控制台打印
package com.atguigu.filters;
 
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
 
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
 
public class LoggingFilter implements Filter {
    private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        // 参数父转子
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse  response = (HttpServletResponse) servletResponse;
        // 拼接日志文本
        String requestURI = request.getRequestURI();
        String time = dateFormat.format(new Date());
        String beforeLogging = requestURI + "在" + time + "被请求了";
        // 打印日志
        System.out.println(beforeLogging);
        // 获取系统时间
        long t1 = System.currentTimeMillis();
        // 放行请求
        filterChain.doFilter(request, response);
        // 获取系统时间
        long t2 = System.currentTimeMillis();
        // 拼接日志文本
        String afterLogging = requestURI + "在" + time + "的请求耗时:" + (t2-t1) + "毫秒";
        // 打印日志
        System.out.println(afterLogging);
    }
}

说明:

  • doFilter 方法中的请求和响应对象是以父接口的形式声明的,实际传入的实参就是 HttpServletRequestHttpServletResponse 子接口级别的,可以安全强转
  • filterChain.doFilter(request, response) 这行代码的功能是放行请求,如果没有这一行代码,则请求到此为止
  • filterChain.doFilter(request, response) 在放行时需要传入 requestresponse,意味着请求和响应对象要继续传递给后续的资源,这里没有产生新的 requestresponse 对象

配置过滤器以及过滤器的过滤范围:(web.xml 中)

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0">
 
    <!-- 配置 filter,并为 filter 起别名 -->
   <filter>
       <filter-name>loggingFilter</filter-name>
       <filter-class>com.atguigu.filters.LoggingFilter</filter-class>
   </filter>
 
    <!-- 为别名对应的 filter 配置要过滤的目标资源 -->
    <filter-mapping>
        <filter-name>loggingFilter</filter-name>
        <!-- 通过映射路径确定过滤资源 -->
        <url-pattern>/servletA</url-pattern>
        <!-- 通过后缀名确定过滤资源 -->
        <url-pattern>*.html</url-pattern>
        <!-- 通过 servlet 别名确定过滤资源 -->
        <servlet-name>servletBName</servlet-name>
    </filter-mapping>
</web-app>

说明:

  • filter-mapping 标签中定义了过滤器对那些资源进行过滤
  • 子标签 url-pattern 通过映射路径确定过滤范围
    • /servletA:精确匹配,表示对 servletA 资源的请求进行过滤
    • *.html:表示对以 .html 结尾的路径进行过滤
    • /*:表示对所有资源进行过滤
    • 一个 filter-mapping 下可以配置多个 url-pattern
  • 子标签 servlet-name 通过 servlet 别名确定对哪些 servlet 进行过滤
    • 使用该标签确定目标资源的前提是 servlet 已经起了别名
    • 一个 filter-mapping 下可以定义多个 servlet-name
    • 一个 filter-mapping 下,servlet-nameurl-pattern 子标签可以同时存在

过滤过程图解:

过滤器生命周期

过滤器作为 Web 项目的组件之一,和 Servlet 的生命周期类似,略有不同,没有 Servlet 的 load-on-startup 的配置,默认就是系统启动立刻构造

阶段对应方法执行时机执行次数
创建对象构造器Web 应用启动时1次
初始化方法void init(FilterConfig filterConfig)构造完毕1次
过滤请求void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)每次请求多次
销毁default void destroy()Web 应用关闭时1次

测试代码:

package com.atguigu.filters;
 
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebServlet;
 
import java.io.IOException;
 
 
@WebServlet("/*")
public class LifeCycleFilter implements Filter {
    public LifeCycleFilter() {
        System.out.println("LifeCycleFilter constructor method invoked");
    }
    
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("LifeCycleFilter init method invoked");
        
    }
 
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("LifeCycleFilter doFilter method invoked");
        filterChain.doFilter(servletRequest, servletResponse);
    }
 
    @Override
    public void destroy() {
        System.out.println("LifeCycleFilter destory method invoked");
    }
}

过滤器链的使用

一个 Web 项目中,可以同时定义多个过滤器,多个过滤器对同一个资源进行过滤时,工作位置有先后,整体形成一个工作链,称之为过滤器链

  • 过滤器链中的过滤器的顺序由 filter-mapping 顺序决定
  • 每个过滤器过滤的范围不同,针对同一个资源来说,过滤器链中的过滤器个数可能是不同的
  • 如果某个 Filter 是使用 ServletName 进行匹配规则的配置,那么这个 Filter 执行的优先级要更低

图解过滤器链:

过滤器链功能测试:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0">
 
    <filter>
        <filter-name>filter1</filter-name>
        <filter-class>com.atguigu.filters.Filter1</filter-class>
    </filter>
 
    <filter>
        <filter-name>filter2</filter-name>
        <filter-class>com.atguigu.filters.Filter2</filter-class>
    </filter>
 
    <filter>
        <filter-name>filter3</filter-name>
        <filter-class>com.atguigu.filters.Filter3</filter-class>
    </filter>
 
    <!-- filter-mapping 的顺序决定了过滤器的工作顺序 -->
    <filter-mapping>
        <filter-name>filter1</filter-name>
        <url-pattern>/servletC</url-pattern>
    </filter-mapping>
 
    <filter-mapping>
        <filter-name>filter2</filter-name>
        <url-pattern>/servletC</url-pattern>
    </filter-mapping>
 
    <filter-mapping>
        <filter-name>filter3</filter-name>
        <url-pattern>/servletC</url-pattern>
    </filter-mapping>
</web-app>

注解方式配置过滤器

@WebFilter 源码:

package jakarta.servlet.annotation;
 
import jakarta.servlet.DispatcherType;
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 WebFilter {
    String description() default "";
 
    String displayName() default "";
 
    WebInitParam[] initParams() default {};
 
    String filterName() default "";
 
    String smallIcon() default "";
 
    String largeIcon() default "";
 
    String[] servletNames() default {};
 
    String[] value() default {};
 
    String[] urlPatterns() default {};
 
    DispatcherType[] dispatcherTypes() default {DispatcherType.REQUEST};
 
    boolean asyncSupported() default false;
}

一个比较完整的 Filter 的 XML 配置:

<!-- 配置 filter,并为 filter 起别名 -->
<filter>
    <filter-name>loggingFilter</filter-name>
    <filter-class>com.atguigu.filters.LoggingFilter</filter-class>
    <!-- 配置 filter 的初始参数 -->
    <init-param>
        <param-name>dateTimePattern</param-name>
        <param-value>yyyy-MM-dd HH:mm:ss</param-value>
    </init-param>
</filter>
 
<!-- 为别名对应的 filter 配置要过滤的目标资源 -->
<filter-mapping>
    <filter-name>loggingFilter</filter-name>
    <!-- 通过映射路径确定过滤资源 -->
    <url-pattern>/servletA</url-pattern>
    <!-- 通过后缀名确定过滤资源 -->
    <url-pattern>*.html</url-pattern>
    <!-- 通过 servlet 别名确定过滤资源 -->
    <servlet-name>servletBName</servlet-name>
</filter-mapping>

将 XML 配置转换成注解方式实现:

package com.atguigu.filters;
 
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.annotation.WebInitParam;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
 
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
 
@WebFilter(
	filterName = "loggingFilter",
	initParams = {@WebInitParam(name="dateTimePattern", value="yyyy-MM-dd HH:mm:ss")},
	urlPatterns = {"/servletA", "*.html"},
	servletNames = {"servletBName"}
)
public class LoggingFilter implements Filter {
    private SimpleDateFormat dateFormat;
 
    /* 
     * init 初始化方法,通过 filterConfig 获取初始化参数
     * init 方法中,可以用于定义一些其他初始化功能代码
    */
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 获取初始参数
        String dateTimePattern = filterConfig.getInitParameter("dateTimePattern");
        // 初始化成员变量
        dateFormat = new SimpleDateFormat(dateTimePattern);
    }
 
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        // ...
    }
}

监听器

监听器概述

监听器:专门用于对域对象对象身上发生的事件或状态改变进行监听和相应处理的对象

  • 监听器是 GOF 设计模式中观察者模式的典型案例
  • 观察者模式:当被观察的对象发生某些改变时,观察者自动采取对应的行动的一种设计模式
  • 监听器使用的感受类似 JS 中的事件,被观察的对象发生某些情况时,自动触发代码的执行
  • 监听器并不监听 Web 项目中的所有组件,仅仅是对三大域对象做相关的事件监听

监听器的分类:

  • Web 中定义八个监听器接口作为监听器的规范,这八个接口按照不同的标准可以形成不同的分类
  • 按监听的对象划分
    • application 域监听器:ServletContextListenerServletContextAttributeListener
    • session 域监听器:HttpSessionListenerHttpSessionAttributeListenerHttpSessionBindingListenerHttpSessionActivationListener
    • request 域监听器:ServletRequestListenerServletRequestAttributeListener
  • 按监听的事件分
    • 域对象的创建和销毁监听器:ServletContextListenerHttpSessionListenerServletRequestListener
    • 域对象数据增删改事件监听器:ServletContextAttributeListenerHttpSessionAttributeListenerServletRequestAttributeListener
    • 其他监听器:HttpSessionBindingListenerHttpSessionActivationListener

监听器的六个主要接口

application 域监听器

ServletContextListener 监听 ServletContext 对象的创建与销毁:

方法名作用
contextInitialized(ServletContextEvent sce)ServletContext 创建时调用
contextDestroyed(ServletContextEvent sce)ServletContext 销毁时调用

ServletContextEvent 对象代表从 ServletContext 对象身上捕获到的事件,通过这个事件对象可以获取到 ServletContext 对象。

ServletContextAttributeListener 监听 ServletContext 中属性的添加、移除和修改:

方法名作用
attributeAdded(ServletContextAttributeEvent scab)ServletContext 中添加属性时调用
attributeRemoved(ServletContextAttributeEvent scab)ServletContext 中移除属性时调用
attributeReplaced(ServletContextAttributeEvent scab)ServletContext 中的属性被修改时调用

ServletContextAttributeEvent 对象代表属性变化事件,它包含的方法如下:

方法名作用
getName()获取修改或添加的属性名
getValue()获取被修改或添加的属性值
getServletContext()获取 ServletContext 对象

测试代码:

  • 定义监听器
package com.atguigu.listeners;
 
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebListener;
 
@WebListener
public class ApplicationListener implements ServletContextListener, ServletContextAttributeListener {
    // 监听初始化
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        ServletContext application = sce.getServletContext();
        System.out.println("application" + application.hashCode() + " initialized");
    }
 
    // 监听销毁
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        ServletContext application = sce.getServletContext();
        System.out.println("application" + application.hashCode() + " destroyed");
    }
 
    // 监听数据增加
    @Override
    public void attributeAdded(ServletContextAttributeEvent scae) {
        String name = scae.getName();
        Object value = scae.getValue();
        ServletContext application = scae.getServletContext();
        System.out.println("application" + application.hashCode() + " add:" + name + "=" + value);
    }
 
    // 监听数据移除
    @Override
    public void attributeRemoved(ServletContextAttributeEvent scae) {
        String name = scae.getName();
        Object value = scae.getValue();
        ServletContext application = scae.getServletContext();
        System.out.println("application" + application.hashCode() + " remove:" + name + "=" + value);
    }
 
    // 监听数据修改
    @Override
    public void attributeReplaced(ServletContextAttributeEvent scae) {
        String name = scae.getName();
        Object value = scae.getValue();
        ServletContext application = scae.getServletContext();
        Object newValue = application.getAttribute(name);
        System.out.println("application" + application.hashCode() + " change:" + name + "=" + value + " to " + newValue);
    }
}
  • 定义触发监听器的代码
// ServletA 用于向 application 域中放入数据
@WebServlet(urlPatterns = "/servletA", name = "servletAName")
public class ServletA extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 向 application 域中放入数据
        ServletContext application = this.getServletContext();
        application.setAttribute("k1", "v1");
        application.setAttribute("k2", "v2");
    }
}
 
// ServletB 用于向 application 域中修改和移除数据
@WebServlet(urlPatterns = "/servletB", name = "servletBName")
public class ServletB extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ServletContext appliation  = getServletContext();
        // 修改 application 域中的数据
        appliation.setAttribute("k1", "value1");
        // 删除 application 域中的数据
        appliation.removeAttribute("k2");
    }
}

session 域监听器

HttpSessionListener 监听 HttpSession 对象的创建与销毁:

方法名作用
sessionCreated(HttpSessionEvent hse)HttpSession 对象创建时调用
sessionDestroyed(HttpSessionEvent hse)HttpSession 对象销毁时调用

HttpSessionEvent 对象代表从 HttpSession 对象身上捕获到的事件,通过这个事件对象我们可以获取到触发事件的 HttpSession 对象。

HttpSessionAttributeListener 监听 HttpSession 中属性的添加、移除和修改:

方法名作用
attributeAdded(HttpSessionBindingEvent se)HttpSession 中添加属性时调用
attributeRemoved(HttpSessionBindingEvent se)HttpSession 中移除属性时调用
attributeReplaced(HttpSessionBindingEvent se)HttpSession 中的属性被修改时调用

HttpSessionBindingEvent 对象代表属性变化事件,它包含的方法如下:

方法名作用
getName()获取修改或添加的属性名
getValue()获取被修改或添加的属性值
getSession()获取触发事件的 HttpSession 对象

request 域监听器

ServletRequestListener 监听 ServletRequest 对象的创建与销毁:

方法名作用
requestInitialized(ServletRequestEvent sre)ServletRequest 对象创建时调用
requestDestroyed(ServletRequestEvent sre)ServletRequest 对象销毁时调用

ServletRequestEvent 对象代表从 HttpServletRequest 对象身上捕获到的事件,通过这个事件对象我们可以获取到触发事件的 HttpServletRequest 对象。另外还有一个方法可以获取到当前 Web 应用的 ServletContext 对象。

ServletRequestAttributeListener 监听 ServletRequest 中属性的添加、移除和修改:

方法名作用
attributeAdded(ServletRequestAttributeEvent srae)ServletRequest 中添加属性时调用
attributeRemoved(ServletRequestAttributeEvent srae)ServletRequest 中移除属性时调用
attributeReplaced(ServletRequestAttributeEvent srae)ServletRequest 中的属性被修改时调用

ServletRequestAttributeEvent 对象代表属性变化事件,它包含的方法如下:

方法名作用
getName()获取修改或添加的属性名
getValue()获取被修改或添加的属性值
getServletRequest()获取触发事件的 ServletRequest 对象

session 域的两个特殊监听器

session 绑定监听器

HttpSessionBindingListener 监听当前监听器对象在 Session 域中的增加与移除:

方法名作用
valueBound(HttpSessionBindingEvent event)该类的实例被放到 Session 域中时调用
valueUnbound(HttpSessionBindingEvent event)该类的实例从 Session 中移除时调用

HttpSessionBindingEvent 对象代表属性变化事件,它包含的方法如下:

方法名作用
getName()获取当前事件涉及的属性名
getValue()获取当前事件涉及的属性值
getSession()获取触发事件的 HttpSession 对象

测试代码:

  • 定义监听器
package com.atguigu.listeners;
 
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.HttpSessionBindingEvent;
import jakarta.servlet.http.HttpSessionBindingListener;
 
public class MySessionBindingListener implements HttpSessionBindingListener {
    // 监听绑定
    @Override
    public void valueBound(HttpSessionBindingEvent event) {
        HttpSession session = event.getSession();
        String name = event.getName();
        System.out.println("MySessionBindingListener" + this.hashCode() + " binding into session" + session.hashCode() + " with name " + name);
    }
 
    // 监听解除绑定
    @Override
    public void valueUnbound(HttpSessionBindingEvent event) {
        HttpSession session = event.getSession();
        String name = event.getName();
        System.out.println("MySessionBindingListener" + this.hashCode() + " unbond outof session" + session.hashCode() + " with name " + name);
    }
}
  • 定义触发监听器的代码
@WebServlet(urlPatterns = "/servletA", name = "servletAName")
public class ServletA extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        HttpSession session = req.getSession();
        // 绑定监听器
        session.setAttribute("bindingListener", new MySessionBindingListener());
        // 解除绑定监听器
        session.removeAttribute("bindingListener");
    }
}

钝化活化监听器

HttpSessionActivationListener 监听某个对象在 Session 中的序列化与反序列化。

方法名作用
sessionWillPassivate(HttpSessionEvent se)该类实例和Session一起钝化到硬盘时调用
sessionDidActivate(HttpSessionEvent se)该类实例和Session一起活化到内存时调用

HttpSessionEvent 对象代表事件对象,通过 getSession() 方法获取事件涉及的 HttpSession 对象。

什么是钝化活化:

  • Session 对象在服务端是以对象的形式存储于内存的,Session 过多,服务器的内存也是吃不消的
  • 而且一旦服务器发生重启,所有的 Session 对象都将被清除,也就意味着 Session 中存储的不同客户端的登录状态丢失
  • 为了分摊内存压力并且为了保证 Session 重启不丢失,我们可以设置将 Session 进行钝化处理
  • 在关闭服务器前或者到达了设定时间时,对 Session 进行序列化到磁盘,这种情况叫做 Session 的钝化
  • 在服务器启动后或者再次获取某个 Session 时,将磁盘上的 Session 进行反序列化到内存,这种情况叫做 Session 的活化

如何配置钝化活化:

  • 在 web 目录下,添加 META-INF 下创建 Context.xml

  • 文件中配置钝化
<?xml version="1.0" encoding="UTF-8"?>
<Context>
    <Manager className="org.apache.catalina.session.PersistentManager" maxIdleSwap="1">
        <Store className="org.apache.catalina.session.FileStore" directory="d:\mysession"></Store>
    </Manager>
</Context>
  • 请求 servletA,获得 Session,并存入数据,然后重启服务器
@WebServlet(urlPatterns = "/servletA", name = "servletAName")
public class ServletA extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        HttpSession session = req.getSession();
        // 添加数据
        session.setAttribute("k1","v1");
    }
}
  • 请求 servletB 获取 Session,获取重启前存入的数据
@WebServlet(urlPatterns = "/servletB", name = "servletBName")
public class ServletB extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        HttpSession session = req.getSession();
        Object v1 = session.getAttribute("k1");
        System.out.println(v1);
    }
}

如何监听钝化活化:

  • 定义监听器
package com.atguigu.listeners;
 
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.HttpSessionActivationListener;
import jakarta.servlet.http.HttpSessionEvent;
 
import java.io.Serializable;
 
public class ActivationListener  implements HttpSessionActivationListener, Serializable {
    // 监听钝化
    @Override
    public void sessionWillPassivate(HttpSessionEvent se) {
        HttpSession session = se.getSession();
        System.out.println("session with JSESSIONID " + session.getId() + " will passivate");
    }
 
    // 监听活化
    @Override
    public void sessionDidActivate(HttpSessionEvent se) {
        HttpSession session = se.getSession();
        System.out.println("session with JSESSIONID " + session.getId() + " did activate");
    }
}
  • 定义触发监听器的代码
@WebServlet(urlPatterns = "/servletA", name = "servletAName")
public class ServletA extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        HttpSession session = req.getSession();
        // 添加数据
        session.setAttribute("k1","v1");
        // 添加钝化活化监听器
        session.setAttribute("activationListener", new ActivationListener());
    }
}