06.Session_filter_listener

第六章 Session_filter_listener

1. Session

1.1 为什么要Session

  • HTTP是无状态协议(Stateless),即HTTP协议本身不会保存请求和响应的通信状态
    • 例如,在客户端请求——服务端回应 的C/S响应模式下,服务器并不会记录该请求来自于哪个浏览器,即客户端的状态
    • 生活例子:张三下馆子吃了一次,第二次再来吃,老板在有状态的情况下知道张三上次吃的啥,在无状态的情况下则不知道,需要张三重新点
  • 引出使用Cookie配合Session解决HTTP协议中请求和响应的持久化问题
    • Cookie记录少量数据,通过响应头set-cookie来向客户端响应用户要保留的信息
    • Session记录更多数据,通过HttpSession对象保存和客户端相关的信息
    • Session的传递配合Cookie一起使用
    • 生活例子:张三去银行办业务,银行给张三开户(Session),并发了银行卡(Cookie),后续张三通过Cookie给银行,银行就会通过Cookie查询到张三的Session

1.2 Cookie

1.2.1 Cookie原理

Cookie是客户端的会话技术,由服务端产生,客户端访问该服务器后,服务器则携带该Cookie给服务端,过程如下

  • 用户一开始不携带cookie请求服务端,服务端先创建cookie,将cookie放入相应对象中,Tomcat容器自动将cookie转化为set-cookie响应头,相应给客户端
  • 客户端收到set-cookie响应头后,下次请求会携带cookie放入请求体中,发送给服务端
    • cookie是键值对数据,tomcat8.5版本后可存放中文,但不推荐
    • cookie存储于客户端,容易暴露不安全,一般存储不敏感的数据
      • 记录用户名(帮助下次登录快速填充用户名)
      • 保存电影播放进度(帮助关闭页面后自动跳转到上次播放进度)

上述过程图解

image-20240915143213849

1.2.2 Cookie使用

ServletA:生成Cookie,将Cookie放入响应体中

  • 创建Cookie对象
  • 可以设置Cookie对象的持久化时间maxAge和持久化路径Path
    • 设置持久化时间后,Cookie将由会话级到持久级,数据会从内存存储到硬盘中,不会随浏览器关闭而消失
    • 设置持久化路径可以实现访问不同资源路径,可以携带不同的Cookie
  • 在resp对象中直接调用addCookie方法
package com.atguigu.servlet;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

/**
 * @author yuezi2048
 * @version 1.0
 */
@WebServlet("/servletA")
public class ServletA extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 创建cookie
        Cookie cookie1 = new Cookie("keyA", "valueA");
        // 设置cookie持久化时间
        cookie1.setMaxAge(60 * 5); // 单位是秒
        // 设置cookie持久化路径
        cookie1.setPath("/demo06/servletB");
        Cookie cookie2 = new Cookie("keyB", "valueB");

        // 将cookie放入resp对象响应给客户,后续客户每次请求都会携带Cookie请求(关闭浏览器后自动清除)
        resp.addCookie(cookie1);
        resp.addCookie(cookie2);
    }
}

访问ServletA后的响应报文,此时在ServeltA界面,只存储了keyB-valueB报文

image-20240915144808856

ServletB:检验用户请求体中的Cookie信息

  • 通过req对象的getCookies方法获取(可能有多个Cookie,如果没有返回的是null)
  • foreach遍历读取Cookie[]数组信息
package com.atguigu.servlet;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

/**
 * @author yuezi2048
 * @version 1.0
 */
@WebServlet("/servletB")
public class ServletB extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取请求中携带的cookie(多个cookie的数组)
        Cookie[] cookies = req.getCookies();
        // 如果没有cookie,Cookie为null还是长度为0?(为NULL,因此需要先判断在进行迭代,否则出现NULL Pointer Exception)
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                System.out.println(cookie.getName() + " " + cookie.getValue());
            }
        }

    }
}

请求ServeltB会携带keyA-valueA和keyB-valueB的Cookie,那么也就顺利实现了Cookie的提交路径

image-20240915145311092

服务端控制台也能打印出来相关Cookie信息

image-20240915145330996

1.3 Session

1.3.1 HttpSession原理

HttpSession用于保留用户更多信息,服务器会为用户开辟一块内存空间(存储Session对象),后续客户端发来请求后,服务端可以通过Session来得到和返回指定客户端的状态了,具体的流程如下。

  • 客户第一次请求后,服务端首先为客户创建Session,同时会将session对象的id(JSESSIONID)通过cookie的方式放入响应对象,进而生成响应体响应给客户
  • 客户则会接收到JSESSIONID的特殊Cookie,后续请求的时候就会携带上该Cookie,后端再接收到的时候就会根据id寻找对应的Session对象
    • 后面会提到,Session对象是三大域对象之一
    • Session可以在服务端存储用户的一些重要信息
      • 用户的登录状态(用户登陆后的敏感信息)
      • 用户操作历史(访问痕迹,购物车等临时信息)

上述流程的图解

image-20240915150156034

1.3.2 HttpSession使用

Servlet1:创建Sessision对象,并构建Cookie响应给客户端

  • 通过请求对象获取Session对象,包括了以下操作
    • 如果req包含了JSESSIONID 的Cookie,则会找对应的Session,如果找到返回Session,没找到则创建Session并返回cookie
    • 如果req没有包含JESSIONID,则创建session并返回cookie

image-20240915150950851

  • 可设置session的存活时间,也可以立即使session失效

    • tomcat/conf/web.xml的xml配置文件为默认30(min),建议不要修改
    • 我们可以根据业务需求,手动在本地项目web.xml文件对其进行更新
    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
             version="4.0">
        <session-config>
            <session-timeout>1</session-timeout>
        </session-config>
    </web-app>
    
    • 也可以通过API设定,见下面代码
  • 另外,session对象通过get方法获取到id,更新情况等属性信息,通过set方法设置属性(键值对方式存储)

package com.atguigu.servlet;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import java.io.IOException;

/**
 * @author yuezi2048
 * @version 1.0
 */
@WebServlet("/servlet1")
public class Servlet1 extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 接收请求中的Username参数
        String username = req.getParameter("username");

        // 获得Session对象
        HttpSession session = req.getSession();
//        session.setMaxInactiveInterval(30 * 60); // 秒数,在某些情况下可修改
//        session.invalidate();
        // 判断是否存在关于Session的特殊Cookie,k:JSESSIONID v:xxxxxxx
        //      有:根据JSESSIONID找对应的SESSION对象
        //          找到了:返回之前的SESSION
        //          没找到:创建SESSION 并返回特殊Cookie
        //      没有:创建一个session,并返回特殊Cookie(已封装)
        System.out.println(session.getId());
        System.out.println(session.isNew());

        // 将Username存入Session,向客户响应信息
        session.setAttribute("username", username); // v:Object类型
        resp.setContentType("text/html;charset=utf-8");
        resp.getWriter().write("创建SESSION成功");
    }
}

测试,成功获取到Session,并得到Cookie,并得到最新的SessionId,一分钟内多次请求,可以看到第一次之后就输出false了

image-20240915151333661

image-20240915151647972

image-20240915151955469

servlet2:负责接收Session对象,把Session对象中的数据拿出来

package com.atguigu.servlet;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import java.io.IOException;

/**
 * @author yuezi2048
 * @version 1.0
 */
@WebServlet("/servlet2")
public class Servlet2 extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获得Session对象
        HttpSession session = req.getSession();
        System.out.println(session.getId());
        System.out.println(session.isNew());

        // 读取Session中存储的用户名
        String username = (String) session.getAttribute("username");
        System.out.println("servlet2 got username: " + username);
    }
}

测试:请求的时候携带了Cookie,servlet1传参后也是能获取到用户信息的

image-20240915151759324

image-20240915151911044

1.4 三大域对象

1.4.1 域对象说明
  • 存储数据和传递数据的对象,数据共享的范围是不一样的,因此我们划分了三大域对象,不同的对象代表了不同的域
  • 请求域对象HttpServletRequest:传递数据的范围是一次请求内的请求转发
  • 会话域对象HttpSession:传递数据的范围是一次会话内,可跨请求
  • 应用域对象ServletContext:传递数据的范围是本应用内,可跨会话
  • 生活举例:热水器的使用范围,张三工位 只能张三用,实验室内 整个实验室用,外面走廊 该层所有人都可以用

上述对象图解

image-20240915152817138

  • 请求转发时,请求域可以传递数据请求域内一般放本次请求业务有关的数据,如:查询到的所有的部门信息
  • 同一个会话内,不用请求转发,会话域可以传递数据会话域内一般放本次会话的客户端有关的数据,如:当前客户端登录的用户
  • 同一个APP内,不同的客户端,应用域可以传递数据应用域内一般放本程序应用有关的数据 如:Spring框架的IOC容器
1.4.2 域对象通用API

域对象通用API

域对象的API

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

servletA:向三大域放入数据

import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import java.io.IOException;

/**
 * @author yuezi2048
 * @version 1.0
 */
@WebServlet("/servletA")
public class ServletA extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 向请求域存放数据
        req.setAttribute("request", "requestMessage");

        // 向会话域存放数据
        HttpSession session = req.getSession();
        session.setAttribute("session", "sessionMessage");

        // 向应用域存放数据
        ServletContext application = req.getServletContext(); // getServletContext()也可以
        application.setAttribute("application", "applicationMessage");

        // 获取请求域的数据
        String reqMessage = (String) req.getAttribute("request");
        System.out.println("request: " + reqMessage);

        // 请求转发 给 ServletB
        // req.getRequestDispatcher("servletB").forward(req, resp);

        // 重定向
        resp.sendRedirect("servletB");
    }
}

servletB:向三大域拿出数据

import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import java.io.IOException;

/**
 * @author yuezi2048
 * @version 1.0
 */
@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("request: " + reqMessage);

        // 获取会话域的数据
        HttpSession session = req.getSession();
        String session1 = (String) session.getAttribute("session");
        System.out.println("session1: " + session1);

        // 获取应用域的数据
        ServletContext application = getServletContext();
        String  appMessage = (String) application.getAttribute("application");
        System.out.println("appMessage: " + appMessage);
    }
}

请求转发测试:此时servlet1和servlet2在同一个请求域中,都能收到请求域对象的相关信息,一般后面对应的业务应该就是查询数据库等一次性请求

image-20240915154534109

重定向测试:在servletB重定向后,响应请求域的信息没了

image-20240915154421693

session域在有效期内都是一直存在的,可以控制一段时间内用户的登录状态

application域的生命周期就随着Tomcat应用的搭建和停止而同时变动了,是最大的一块,只要应用不停就一直在

2. filter

2.1 filter概述

Filter是Java EE的技术规范之一,对目标资源请求进行过滤的规范,是最为实用的技术之一

  • Filter接口定义了过滤器的开发规范,所有接口均遵循该接口
    • 这里init和destroy用到了jdk1.8特性的default,使得我们实现该接口时无须实现这两个方法
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的工作位置是项目中所有目标资源之前,容器在创建HttpServletRequestHttpServletResponse之后,会先调用doFilter方法
    • doFilter方法可以控制请求是否继续,如果放行,则继续,否则请求被拦住,过滤器本身做出响应
    • Filter不仅仅是可以对请求做响应,也可以在目标资源响应前,对响应再次处理
  • Filter是GOF中责任链的典型案例
  • 常用应用包括但不限于:登录权限、乱码处理、过滤敏感字符、日志记录、性能分析、事务控制、跨域处理…
    • 生活案例:地铁站安保,会检查行李,再根据情况放行还是拦下

图解(filter也可以在转换成响应报文前,抓响应对象resp)

image-20240915171630278

flter接口下规定的API和说明如下

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

2.2 filter使用——记录日志

我们这里希望实现一个日志记录,则可以考虑通过filter帮助我们记录req和resp的内容,因为filter是这两个报文的必经之路

这里我们简单的实现的日志内容如下:

  • 用户请求到达目标资源前,记录用户的请求路径
  • 响应服务器处理用户请求的时间
  • 日志记录写入文件(待实现),可以先简单的在控制台输出

过滤器filter:LoggingFilter

  • 实现过滤:1. 实现Filter接口;2. 重写doFilter方法;3. 配置映射路径(annotation / xml)
  • 编写技巧:记录日期的格式可以作为一个私有变量以便复用
package com.atguigu.filters;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 日志过滤器,用于记录请求的历史,将日志暂时打印到控制台,后续有框架后可以打到文件
 * 1. 实现Filter 接口
 * 2. 重写过滤方法
 * 3. 配置过滤器的映射路径
 *      web.xml
 *      注解
 *
 * @author yuezi2048
 * @version 1.0
 */
public class LoggingFilter implements Filter {

    private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    /*
        过滤请求和响应的方法
            1. 请求到达目标资源前,先经过该方法
            2. 该方法有能力控制请求是否可以往后到达目标资源,也可以直接向客户端做响应
            3. 请求到目标资源后,在响应之前也同样会经过该方法
     */
    /**
     * 1. 请求到达目标资源的方法:
     *      判断是否能登录
     *      校验权限是否满足
     * 2. 放行代码
     * 3. 响应前(HttpServletResponse对象转换成响应报文之前)的功能代码
     */
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        // 由于我们明确是通过HTTP协议通信,参数父转子,获得更丰富的req resp功能方法
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        // 请求到达目标资源URI前 yyyy-MM-dd HH:mm:ss ****被访问了
        String requestURI = request.getRequestURI();
        String dateTime = dateFormat.format(new Date());
        String beforeLogging = requestURI + "在" + dateTime + "时间被访问了";
        System.out.println(beforeLogging);

        Long t1 = System.currentTimeMillis();

        // 1. 通过过滤器链表 放行,如果这行没有执行到,将直接打回请求
        // 2. req和resp对象会继续传递给下一个filter / 服务器,即不会产生新的req resp对象
        filterChain.doFilter(servletRequest, servletResponse);

        long t2 = System.currentTimeMillis();

        // 响应前的功能代码 ***资源 在yyyy-MM-dd HH:mm:ss 的请求耗时 x 毫秒
        String afterLogging = requestURI + "资源在" + dateTime + "的请求耗时" + (t2 - t1) +  "毫秒";
        System.out.println(afterLogging);

    }
}

XML配置映射路径(这里详细说一下XML,后面同理就copy一下了,和Servlet差不多,只是多了一个通过servlet-name的访问路径,注解是简化写法,最后说)

  • 配置过滤器<filter>
    • <filter-name> :filter的别名
    • <filter-class> : filter的类路径
  • 配置过滤器的映射路径<filter-mapping>
    • <filter-name> :filter的别名
    • <url-pattern> :对指定路径过滤,这里都是用斜杠开头的,相对路径就比较难定位?
    • <servlet-name> :servlet的别名,对指定servlet过滤
  • 注:一个filter-mapping可以有多个url-pattern 和 servlet-name组合
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!--        配置过滤器(和Servlet类似)-->
    <filter>
        <filter-name>loggingFilter</filter-name>
        <filter-class>com.atguigu.filters.LoggingFilter</filter-class>
    </filter>

    <!--        配置过滤器的路径(url-name或者servlet-name两种方式定位)-->
    <filter-mapping>
        <filter-name>loggingFilter</filter-name>
        <!--    url-pattern 根据请求路径,对指定请求过滤-->
        <!--        /*          过滤全部资源-->
        <!--        /a/*        过滤a开头的全部资源-->
        <!--        *.html      过滤后缀-->
        <!--        /servlet1   对servlet1请求过滤-->
        <!--    servlet-name 根据请求的servlet别名,对指定servlet资源过滤-->
        <!--    一个filter-mapping可存在多个url-pattern和servlet-name-->
        <url-pattern>/*</url-pattern>
        <url-pattern>/servlet1</url-pattern>
        <url-pattern>/static</url-pattern>
        <url-pattern>*.html</url-pattern>
        <servlet-name>servlet1</servlet-name>
    </filter-mapping>
    
</web-app>

接下来配置servlet负责主要是响应数据

package com.atguigu.servlet;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

/**
 * @author yuezi2048
 * @version 1.0
 */
@WebServlet(value = "/servlet1", name = "servlet1")
public class Servlet1 extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("Servlet1 service invoked...");
        resp.getWriter().write("Servlet1 Message...");
    }
}

控制台输出

image-20240915175310193

上述过程图解

image-20240915174757993

2.3 filter生命周期

  • filter的声明周期和Servlet类似,少了一个load-on-startup的配置,他默认是系统启动立即构造
  • 主要是init方法,参数里面包含了filterConfig参数,这个是我们需要手动加入的配置,类似于Servlet,getNames方法 + 类似于HashMap的while循环遍历
阶段对应方法执行时机执行次数
创建对象构造器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 java.io.IOException;
import java.util.Enumeration;

/**
 * @author yuezi2048
 * @version 1.0
 */
public class LifeCycleFilter implements Filter {

    /**
     *      1. 构造       构造器         项目启动      1
     *      2. 初始化      init         构造完毕      1
     *      3. 过滤       doFilter      每次请求      多次
     *      4. 销毁       destroy       服务关闭      1
     *      init和destroy使用default修饰,不重写也不会报错
     *      启动后filter默认立即构造启动,不会像Servlet一样通过loadOnStartUp控制
     */

    public LifeCycleFilter() {
        System.out.println("Filter constructor...");
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter init...");
        System.out.println("filter config: ");
        Enumeration<String> initParameterNames = filterConfig.getInitParameterNames();
        while (initParameterNames.hasMoreElements()) {
            String initParameterName = initParameterNames.nextElement();
            String intParameterValue = filterConfig.getInitParameter(initParameterName);
            System.out.println("\t" + initParameterName + "=>" + intParameterValue);
        }
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("Filter doFilter...");

        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
        System.out.println("Filter destroy...");
    }
}

XML配置

    <filter>
        <filter-name>lifeCycleFilter</filter-name>
        <filter-class>com.atguigu.filters.LifeCycleFilter</filter-class>
        <init-param>
            <param-name>dateTimePattern</param-name>
            <param-value>yyyy-MM-dd HH:mm:ss</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>lifeCycleFilter</filter-name>
        <servlet-name>servlet1</servlet-name>
    </filter-mapping>

输出信息

Filter constructor...
Filter init...
filter config: 
	dateTimePattern=>yyyy-MM-dd HH:mm:ss
Filter doFilter...
/demo08/servlet1在2024-09-15 19:26:18时间被访问了
Servlet1 service invoked...
/demo08/servlet1资源在2024-09-15 19:26:18的请求耗时97毫秒
Filter destroy...

2.4 FilterChain

我们之前在调用dofilter方法时,参数发现是FIlterChain的对象,那么类比单链表,我们当前是可以推断出是有多个Filter的,然后有一定的先后顺序

而FilterChain的先后规则是这样的(分成两种情况):

  • 第一排序规则:按照<filter-mapping>的顺序
  • 第二排序规则:按照<servlet-name>的顺序(优先级要低)
  • 注:如果是注解的方式,是根据类名来排序的

注:每个过滤器过滤的范围是不同的,即客户访问不同的目标资源,过滤器的个数是可以不一样的

上述过程的图解:

image-20240915193259955

新建3个功能相同的filter,以filter1为例,其他两个filter同理

package com.atguigu.filters;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;

import java.io.IOException;

/**
 * 注:如果是用注解的方式来配置Filter,是通过类名的顺序来绝当过滤器实际的执行顺序(如果有顺序要求,需要注意类名 f1_test)
 * filter技术主要关注的就是顺序问题
 * @author yuezi2048
 * @version 1.0
 */
@WebFilter("/servlet1")
public class Filter1 implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("filter1 before doFilter invoked");

        filterChain.doFilter(servletRequest, servletResponse);

        System.out.println("filter1 after doFilter invoked");
    }
}

进行XML配置(注意我们将filter-mapping的顺序调整为2-1-3)

    <!-- 配置多个过滤器 -->
    <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>filter2</filter-name>
        <servlet-name>servlet1</servlet-name>
    </filter-mapping>

    <filter-mapping>
        <filter-name>filter1</filter-name>
        <servlet-name>servlet1</servlet-name>
    </filter-mapping>

    <filter-mapping>
        <filter-name>filter3</filter-name>
        <servlet-name>servlet1</servlet-name>
    </filter-mapping>

运行结果

filter2 before doFilter invoked
filter1 before doFilter invoked
filter3 before doFilter invoked
Filter doFilter...
/demo08/servlet1在2024-09-15 19:26:18时间被访问了
Filter doFilter...
Servlet1 service invoked...
/demo08/servlet1资源在2024-09-15 19:26:18的请求耗时97毫秒
filter3 after doFilter invoked
filter1 after doFilter invoked
filter2 after doFilter invoked

上述代码运行图解

image-20240915194253429

2.4 注解方式配置filter

使用@WebFilter来替代上述的XML配置,通过阅读源码,可以得到几个重要结论

  • 修饰的是类,运行时生效,因为涉及到通过class-path来反射对应的类,后面javadoc的时候这个也会记录到

  • 初始参数和Servlet一样,通过@WebInitParam传入若干个k-v即可

  • value是默认的映射路径,作用就是当只有一个参数时,可以省略不写,如@WebFilter("/servlet1")

  • 因为可能有多个映射路径,所以可以看到urlPatterns和servletNames都是String[]类型,到时候传入的也是一个数组

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebFilter {
    WebInitParam[] initParams() default {};

    String filterName() default "";

    String[] servletNames() default {};
    
    String[] value() default {};

    String[] urlPatterns() default {};
}

我们之前的日志记录为例,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>
@WebFilter(
        filterName = "loggingFilter",
        initParams = {@WebInitParam(name="dateTimePattern",value="yyyy-MM-dd HH:mm:ss")},
        urlPatterns = {"/servletA","*.html"},
        servletNames = {"servletBName"}
)
public class LoggingFilter  implements Filter {
}

3. listener

3.1 listener概述

监听器:这里监听的是域对象发生事件或状态发生改变,并进行响应的处理

  • 监听器是GOF设计模式,观察者模式的典型案例
    • 观察者模式: 当被观察的对象发生某些改变时, 观察者自动采取对应的行动的一种设计模式
  • 监听器类似于JS的事件,当被观察对象发生情况时,自动触发代码执行
  • 注意:web项目的监听器仅监听三大域对象相关的事件,其他组件没有

监听器的分类

  • web一共有8种监听器接口,可按如下规则分类:
  • 按监听的对象划分
    • application域监听器 ServletContextListener ServletContextAttributeListener
    • session域监听器 HttpSessionListener HttpSessionAttributeListener HttpSessionBindingListener HttpSessionActivationListener
    • request域监听器 ServletRequestListener ServletRequestAttributeListener
  • 按监听的事件分
    • 域对象的创建和销毁监听器 ServletContextListener HttpSessionListener ServletRequestListener
    • 域对象数据增删改事件监听器 ServletContextAttributeListener HttpSessionAttributeListener ServletRequestAttributeListener
    • 其他监听器 HttpSessionBindingListener HttpSessionActivationListener

3.2 listener六个主要接口

3.2.1 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对象

application域监听器代码

  • 注意这里如果要捕捉修改的话,参数里的getValue获取的是旧的,获取新的需要通过获取上下文再获取一次
package com.atguigu.listener;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebListener;

/**
 * @author yuezi2048
 * @version 1.0
 */
@WebListener
public class MyApplicationListener implements ServletContextListener, ServletContextAttributeListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        ServletContext application = sce.getServletContext();
        System.out.println(application.hashCode() +  "应用域初始化了");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        ServletContext application = sce.getServletContext();
        System.out.println(application.hashCode() +  "应用域销毁了");
    }

    @Override
    public void attributeAdded(ServletContextAttributeEvent scae) {
        ServletContext application = scae.getServletContext();
        String key = scae.getName();
        Object value = scae.getValue();
        System.out.println(application.hashCode() + "应用域增加了一个键值对 key=" + key + ", value=" + value);
    }

    @Override
    public void attributeRemoved(ServletContextAttributeEvent scae) {
        ServletContext application = scae.getServletContext();
        String key = scae.getName();
        Object value = scae.getValue();
        System.out.println(application.hashCode() + "应用域移除了一个键值对 key=" + key + ", value=" + value);
    }

    @Override
    public void attributeReplaced(ServletContextAttributeEvent scae) {
        ServletContext application = scae.getServletContext();
        String key = scae.getName();
        Object value = scae.getValue(); // 这里获取的是旧值
        Object newValue = application.getAttribute(key); // 再访问一次属性就是新值
        System.out.println(application.hashCode() + "应用域修改了一个键值对 key=" + key + ", value from " + value + " to " + newValue);
    }
}

触发application域监听器代码

package com.atguigu.servlet;

import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

/**
 * @author yuezi2048
 * @version 1.0
 */
@WebServlet("/servlet1")
public class Servlet1 extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        // 向应用域中放入数据
        ServletContext application = this.getServletContext();
        application.setAttribute("keyA", "valueA");
        
        // 修改应用域的数据
        // ServletContext application = this.getServletContext();
        // application.setAttribute("keyA", "valueX");
        
        // 删除应用域中的数据
        // ServletContext application = this.getServletContext();
        // application.removeAttribute("keyA");
    }
}

控制台输出

Connected to server
210262392应用域初始化了
210262392应用域增加了一个键值对 key=keyA, value=valueA
210262392应用域销毁了
3.2.2 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对象

和application同理

@WebListener
public class SessionListener implements HttpSessionListener, HttpSessionAttributeListener {
    // 监听session创建
    @Override
    public void sessionCreated(HttpSessionEvent se) {
        HttpSession session = se.getSession();
        System.out.println("session"+session.hashCode()+" created");
    }

    // 监听session销毁
    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        HttpSession session = se.getSession();
        System.out.println("session"+session.hashCode()+" destroyed");
    }
    // 监听数据增加
    @Override
    public void attributeAdded(HttpSessionBindingEvent se) {
        String name = se.getName();
        Object value = se.getValue();
        HttpSession session = se.getSession();
        System.out.println("session"+session.hashCode()+" add:"+name+"="+value);
    }
    // 监听数据移除
    @Override
    public void attributeRemoved(HttpSessionBindingEvent se) {
        String name = se.getName();
        Object value = se.getValue();
        HttpSession session = se.getSession();
        System.out.println("session"+session.hashCode()+" remove:"+name+"="+value);
    }
    // 监听数据修改
    @Override
    public void attributeReplaced(HttpSessionBindingEvent se) {
        String name = se.getName();
        Object value = se.getValue();
        HttpSession session = se.getSession();
        Object newValue = session.getAttribute(name);
        System.out.println("session"+session.hashCode()+" change:"+name+"="+value+" to "+newValue);
    }

}

触发

// servletA用于创建session并向session中放数据
@WebServlet(urlPatterns = "/servletA",name = "servletAName")
public class ServletA extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 创建session,并向session中放入数据
        HttpSession session = req.getSession();

        session.setAttribute("k1","v1");
        session.setAttribute("k2","v2");
    }
}


// servletB用于修改删除session中的数据并手动让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();
        //  修改session域中的数据
        session.setAttribute("k1","value1");
        //  删除session域中的数据
        session.removeAttribute("k2");
        // 手动让session不可用
        session.invalidate();
    }
}
3.2.3 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对象
package com.atguigu.listeners;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebListener;


@WebListener
public class RequestListener implements ServletRequestListener , ServletRequestAttributeListener {
    // 监听初始化
    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        ServletRequest request = sre.getServletRequest();
        System.out.println("request"+request.hashCode()+" initialized");
    }

    // 监听销毁
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        ServletRequest request = sre.getServletRequest();
        System.out.println("request"+request.hashCode()+" destoryed");
    }


    // 监听数据增加
    @Override
    public void attributeAdded(ServletRequestAttributeEvent srae) {
        String name = srae.getName();
        Object value = srae.getValue();
        ServletRequest request = srae.getServletRequest();
        System.out.println("request"+request.hashCode()+" add:"+name+"="+value);
    }

    //  监听数据移除
    @Override
    public void attributeRemoved(ServletRequestAttributeEvent srae) {
        String name = srae.getName();
        Object value = srae.getValue();
        ServletRequest request = srae.getServletRequest();
        System.out.println("request"+request.hashCode()+" remove:"+name+"="+value);
    }
    // 监听数据修改
    @Override
    public void attributeReplaced(ServletRequestAttributeEvent srae) {
        String name = srae.getName();
        Object value = srae.getValue();
        ServletRequest request = srae.getServletRequest();
        Object newValue = request.getAttribute(name);
        System.out.println("request"+request.hashCode()+" change:"+name+"="+value+" to "+newValue);
    }
}

触发

//  servletA向请求域中放数据
@WebServlet(urlPatterns = "/servletA",name = "servletAName")
public class ServletA extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 向request中增加数据
        req.setAttribute("k1","v1");
        req.setAttribute("k2","v2");
        // 请求转发
        req.getRequestDispatcher("servletB").forward(req,resp);
    }
}

// servletB修改删除域中的数据
@WebServlet(urlPatterns = "/servletB", name = "servletBName")
public class ServletB extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //  修改request域中的数据
        req.setAttribute("k1","value1");
        //  删除session域中的数据
        req.removeAttribute("k2");

    }
}

3.3 session域两个特殊listener(了解)

3.3.1 session绑定listener

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);
    }
}

触发监听器

  • session实际上绑定是是listener对象,因此要在session的setAttribute方法中传入listener对象来绑定
@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");
    }
}
3.3.2 钝化活化listener

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());
    }
}

4. 案例——日程管理第三期

4.1 过滤器控制登录校验

需求说明:未登录状态下不允许访问showShedule.html和SysScheduleController相关增删改处理,重定向到login.html,登录成功后可以自由访问

  • 我们可以通过filter来判断当前用户是否登录成功获得Session,如果没有Session会话就打回,否则执行doFilter放行。
    • 映射路径就是对应我们不允许访问的两个地址,只有满足登录成功才可以访问
package com.atguigu.schedule.filter;

import com.atguigu.schedule.pojo.SysUser;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.annotation.WebListener;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import java.io.IOException;

/**
 * @author yuezi2048
 * @version 1.0
 */
// servletNames也可以,是servlet注解中定义name属性即可
@WebFilter(urlPatterns = {"/showSchedule.html", "/schedule/*"})
public class LoginFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        // 获得Session域对象(通过Http子接口获取)
        HttpSession session = request.getSession();
        // 从Session域中获得用户对象
        SysUser sysUser = (SysUser) session.getAttribute("sysUser");

        // 判断是否为空,如果为空,回到login.html,否则正常放行
        if (null == sysUser) {
            response.sendRedirect("/login.html");
        } else {
            filterChain.doFilter(request, response);
        }

    }
}

SysUserController的login方法代码优化,登录成功后,将信息载入Session域以便filter检查

    /**
     * 接收用户登录请求的业务方法(完成登录的业务接口)
     * @param request
     * @param response
     * @throws ServletException
     * @throws IOException
     */
    protected void login(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 接收用户名密码
        String username = request.getParameter("username");
        String userPwd = request.getParameter("userPwd");

        // 2. 调用服务层方法,根据用户名查询用户信息
        SysUser loginUser = userService.findUserName(username);
        if (loginUser == null) {
            // 用户名有误,跳转到用户名有误提示页
            response.sendRedirect("/loginUsernameError.html");
        } else if (!MD5Util.encrypt(userPwd).equals(loginUser.getUserPwd())) {
            // 密码有误,跳转到密码有误提示也
            response.sendRedirect("/loginUserPwdError.html");
        } else {
            // 登录成功后,需要将登录成功的信息放入Session域
            HttpSession session = request.getSession();
            session.setAttribute("sysUser", loginUser);

            // 跳转成功,跳转到首页
            response.sendRedirect("/showSchedule.html");
        }
    }

5. Ajax

5.1 Ajax是什么

  • AJAX = Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)
  • AJAX的优势
    • 允许不重新加载整个页面的情况下,可以请求服务器后只更新网页的部分数据
    • AJAX仅需支持JS即可执行,无需其他插件
  • XMLHttpRequest 只是实现 Ajax 的一种方式。

image-20240915203647873

  • 我们之前学的是通过form表单标签、a标签来发起请求,而现在只需要通过触发js代码就可以动态请求
    • 通过JS运行代码,无需跳转界面(当然也可以自行设定是否跳转界面)
    • 当接收到返回结果后,通过DOM编程渲染到相关元素上,实现局部更新

5.2 Ajax的使用

原生JS写法(了解),后面优化的话,jquery可以封装这个过程但不是最优解,后续我们直接用VUE框架的axios promise帮助我们完成即可

  • 创建XMLHttpRequest对象
  • 执行回调函数onreadystatechange,解析回送报文,动态更新组件
    • xmlhttp.readyState=4表示请求完成,xmlhttp.status表示响应状态码(请区别后续JSON的业务状态码
  • open方法设置请求方式、请求路径,再通过send方法发送请求
function loadXMLDoc(){
    var xmlhttp = new XMLHttpRequest();
    // 设置回调函数处理响应结果
    xmlhttp.onreadystatechange=function(){
        if (xmlhttp.readyState==4 && xmlhttp.status==200)
        {
            document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
        }
    }
    // 设置请求方式和请求的资源路径
    xmlhttp.open("GET","/try/ajax/ajax_info.txt",true);
    // 发送请求
    xmlhttp.send();
}

6. 案例——日程管理第四期

6.1 提交请求前检验用户名占用

6.1.1 前端AJAX请求

前端在鼠标移开后,直接执行js代码,异步判断用户名是否被占用(checkUsername)

HTML部分

<tr class="ltr">
    <td>请输入账号</td>
    <td>
        <input class="ipt" id="usernameInput" type="text" name="username" onblur="checkUsername()">
        <span id="usernameMsg" class="msg"></span>
    </td>
</tr>

JS部分(script标签)

  • 规范响应:使用JSON串来表示,其中包括业务状态码code、补充描述message、数据data
  • 回调函数解析JSON串后,读取里面的信息进行后续的业务判断即可,如果返回业务码200就说明没有被占用
  • 注意一下请求路径问题吧,我这里是把上下文路径设置为/了,否则如果是绝对路径要加上下文路径,否则就使用相对路径了(我个人不太推荐 害怕后面路径变化)
function checkUsername() {
    var usernameInput = document.getElementById("usernameInput")
    var username = usernameInput.value;
    var usernameRegex = /^[a-zA-Z0-9]{5,10}$/;

    var usernameMsg = document.getElementById("usernameMsg");
    if (! usernameRegex.test(username)) {
        usernameMsg.style.color = "red";
        usernameMsg.innerText = "须为5-10位字母或数字"
        return false;
    }

    // 格式正确通过后,继续校验用户名是否被占用
    var request = new XMLHttpRequest();
    // 回调函数,设置响应回来的信息如何处理
    /**
             * 存在的问题:
             *      1. 响应乱码问题
             *      2. 响应的信息格式不规范,处理方式不规范
             *          后端响应回来的信息应当有一个统一的格式,前后端共同遵守
             *          使用JSON字符串(为了不用自己拼接字符串,可以通过JackSon工具帮助我们通过对象来转换成JSON串)
             *          {
             *              // 举例:code:1成功,code:2失败...
             *              "code":"",      业务状态码,表示该请求是否成功,如果失败了,为什么失败(与报文的响应码区别)
             *              "message":"",   业务状态码的补充说明(描述)
             *              “data”: {}      本次响应的数据     成功/不成功,List<Schedule>
             *              ...,...
             *          }
             *      3. 如果校验不通过,无法阻止表单的提交
             *          未来使用VUE axios 和 promise解决
             */
    // 这里无法阻止表单提交,回调函数的return true和return false和原方法无关,因为已经执行完了,这里没必要深究解决
    request.onreadystatechange = function () {
        if (request.readyState === 4 && request.status === 200) {
            // console.log(request.responseText); // 此时是一个JSON串,需要将串转成对象
            var result = JSON.parse(request.responseText);
            if (result.code !== 200) {
                usernameMsg.style.color = "red";
                usernameMsg.innerText = "账号被占用"
            } else {
                usernameMsg.style.color = "green";
                usernameMsg.innerText = "账号可用"
            }
        }
    };
    // 设置请求方式和资源路径,发送资源请求
    request.open("GET", "/user/checkUsernameUsed?username=" + username);
    request.send(); // 后面的代码执行无意义,因为可能会被后端代码覆盖掉,这是回调函数的缺陷,未来使用VUE的axios promise解决

    // usernameMsg.style.color = "green";
    // usernameMsg.innerText = "账号可用"
    // return true;
}
6.1.2 后端处理业务并构建JSON响应

接下来就是服务端业务,判断用户名有无被占用,然后构造JSON串发给客户端

而为了构建JSON,我们需要根据业务定义若干业务状态码(通过Enum实现)

package com.atguigu.schedule.common;

/**
 * @author yuezi2048
 * @version 1.0
 */
public enum ResultCodeEnum {
    SUCCESS(200, "成功"),
    USERNAME_ERROR(501, "usernameError"),
    PASSWORD_ERROR(503, "passwordError"),
    NOT_LOGIN(504, "notLogin"),
    USERNAME_USED(505, "usernameUsed");

    private Integer code;
    private String message;

    private ResultCodeEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public Integer getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

接下来定义JSON串中的属性,以便转换,注意这里的data因为不确定,可以使用泛型,关键的几个方法是

  • 根据data code message 的构建对象方法 build
    • 通用构建方法:通过enum 和 data构建(message通过enum来set)
    • 没有数据,data可以为null
  • 常用构建操作
    • SUCCESS --> ok 方法
package com.atguigu.schedule.common;
/**
 * code:1成功,code:2失败...
 *         "code":"",      业务状态码,表示该请求是否成功,如果失败了,为什么失败(与报文的响应码区别) 通过枚举实现
 *         "message":"",   业务状态码的补充说明(描述)
 *         “data”: {}      本次响应的数据     成功/不成功,List<Schedule>
 *              ...,...
 * @author yuezi2048
 * @version 1.0
 */
public class Result<T> {
    // 返回码
    private Integer code;
    // 返回消息
    private String message;
    // 返回数据
    private T data;

    public Result(){}

    // 将Enum的内容填入Result方法

    // 返回数据,快速构建Result对象
    protected static <T> Result<T> build(T data) {
        Result<T> result = new Result<T>();
        if (data != null)
            result.setData(data);
        return result;
    }
    public static <T> Result<T> build(T body, Integer code, String message) {
        Result<T> result = build(body);
        result.setCode(code);
        result.setMessage(message);
        return result;
    }
    public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) {
        Result<T> result = build(body);
        result.setCode(resultCodeEnum.getCode());
        result.setMessage(resultCodeEnum.getMessage());
        return result;
    }
    /**
     * 操作成功
     * @param data  baseCategory1List
     * @param <T>
     * @return
     */
    public static<T> Result<T> ok(T data){
        return build(data, ResultCodeEnum.SUCCESS);
    }
    
    // get set方法略
}

在SysUserController专门再根据这个需求 写一个业务方法checkUsernameUsed

  • 默认是正常放行
  • 如果注册的时候查到该用户了,那么就传入ResultCodeEnum.USERNAME_USED(用到了JavaSE里韩老师过关斩将的编程思想)
  • 接下来就是把对象转换成JSON,并告知浏览器这个是JSON串,使用Jackson(需导入依赖)的ObjectMapper.writeValueAsString方法实现
    • 再回忆一下先死后活的优化编程思想,我们把他抽象为一个工具类,简化代码的同时,也便于后续重用(只要有一个响应JSON映射对象就可以构建JSON发送)
    /**
     * 注册时接收要接受的用户名,校验用户名是否被占用的业务接口
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    protected void checkUsernameUsed(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 接收用户名
        String username = req.getParameter("username");

        // 调用service层业务处理方法,查询是否有该用户
        SysUser sysUser = userService.findUserName(username);

        Result<Object> result = Result.ok(null);

        // 如果有,响应占用,否则响应可用
        if (sysUser != null) {
            result = Result.build(null, ResultCodeEnum.USERNAME_USED);
        }
//        // 将result对象转换为JSON串响应给客户端(ObjectMapper),已封装为一个工具方法
//        ObjectMapper objectMapper = new ObjectMapper();
//        String info = objectMapper.writeValueAsString(result);
//
//        // 告诉客户端这是JSON串
//        resp.setContentType("application/json;charset=utf-8");
//        resp.getWriter().write(info);
        WebUtil.writeJson(resp, result); // 直接向客户返回JSON串,后续SpringMVC会封装为一个注解,了解即可

    }

工具类封装WebUtil,注意工具类的要点

  • 公共变量作为一个私有变量,并在静态代码块初始化
  • 该方法额外提供了JSON的读,通过反射和泛型技术来构建未确定对象,那么这个JackSon也帮我们封装好了后面直接用就行,深究原理需要的时候再回顾一下概念即可,见之前的JavaSE相关技术的笔记
package com.atguigu.schedule.util;

import com.atguigu.schedule.common.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.BufferedReader;
import java.io.IOException;
import java.text.SimpleDateFormat;

/**
 * @author yuezi2048
 * @version 1.0
 */
public class WebUtil {
    private static ObjectMapper objectMapper;
    // 初始化objectMapper
    static{
        objectMapper=new ObjectMapper();
        // 设置JSON和Object转换时的时间日期格式
        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    }

    // 从请求中获取JSON串并转换为Object
    public static <T> T readJson(HttpServletRequest request,Class<T> clazz){
        T t =null;
        BufferedReader reader = null;
        try {
            reader = request.getReader();
            StringBuffer buffer =new StringBuffer();
            String line =null;
            while((line = reader.readLine())!= null){
                buffer.append(line);
            }

            t= objectMapper.readValue(buffer.toString(),clazz);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return t;
    }

    // 将Result对象转换成JSON串并放入响应对象
    public static void writeJson(HttpServletResponse response, Result result){
        response.setContentType("application/json;charset=UTF-8");
        try {
            String json = objectMapper.writeValueAsString(result);
            response.getWriter().write(json);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

测试

image-20240915212322452 image-20240915212332977
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值