第六章 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
- Cookie记录少量数据,通过
1.2 Cookie
1.2.1 Cookie原理
Cookie是客户端的会话技术,由服务端产生,客户端访问该服务器后,服务器则携带该Cookie给服务端,过程如下
- 用户一开始不携带cookie请求服务端,服务端先创建cookie,将cookie放入相应对象中,Tomcat容器自动将cookie转化为set-cookie响应头,相应给客户端
- 客户端收到set-cookie响应头后,下次请求会携带cookie放入请求体中,发送给服务端
- cookie是
键值对
数据,tomcat8.5版本后可存放中文,但不推荐 - cookie存储于客户端,容易暴露
不安全
,一般存储不敏感的数据- 记录用户名(帮助下次登录快速填充用户名)
- 保存电影播放进度(帮助关闭页面后自动跳转到上次播放进度)
- cookie是
上述过程图解
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报文
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的提交路径
服务端控制台也能打印出来相关Cookie信息
1.3 Session
1.3.1 HttpSession原理
HttpSession用于保留用户更多信息,服务器会为用户开辟一块内存空间(存储Session对象
),后续客户端发来请求后,服务端可以通过Session来得到和返回指定客户端的状态了,具体的流程如下。
- 客户第一次请求后,服务端首先为客户创建Session,同时会将session对象的id(
JSESSIONID
)通过cookie的方式放入响应对象,进而生成响应体响应给客户 - 客户则会接收到JSESSIONID的特殊Cookie,后续请求的时候就会携带上该Cookie,后端再接收到的时候就会根据id寻找对应的Session对象
- 后面会提到,Session对象是
三大域对象
之一 - Session可以在服务端存储用户的一些重要信息
- 用户的登录状态(用户登陆后的敏感信息)
- 用户操作历史(访问痕迹,购物车等临时信息)
- 后面会提到,Session对象是
上述流程的图解
1.3.2 HttpSession使用
Servlet1:创建Sessision对象,并构建Cookie响应给客户端
- 通过请求对象获取Session对象,包括了以下操作
- 如果req包含了JSESSIONID 的Cookie,则会找对应的Session,如果找到返回Session,没找到则创建Session并返回cookie
- 如果req没有包含JESSIONID,则创建session并返回cookie
-
可设置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了
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传参后也是能获取到用户信息的
1.4 三大域对象
1.4.1 域对象说明
- 存储数据和传递数据的对象,数据共享的范围是不一样的,因此我们划分了三大域对象,不同的对象代表了不同的域
- 请求域对象
HttpServletRequest
:传递数据的范围是一次请求内的请求转发 - 会话域对象
HttpSession
:传递数据的范围是一次会话内,可跨请求 - 应用域对象
ServletContext
:传递数据的范围是本应用内,可跨会话 - 生活举例:热水器的使用范围,张三工位 只能张三用,实验室内 整个实验室用,外面走廊 该层所有人都可以用
上述对象图解
- 请求转发时,请求域可以传递数据
请求域内一般放本次请求业务有关的数据,如:查询到的所有的部门信息
- 同一个会话内,不用请求转发,会话域可以传递数据
会话域内一般放本次会话的客户端有关的数据,如:当前客户端登录的用户
- 同一个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在同一个请求域中,都能收到请求域对象的相关信息,一般后面对应的业务应该就是查询数据库等一次性请求
重定向测试:在servletB重定向后,响应请求域的信息没了
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的工作位置是项目中所有目标资源之前,容器在创建
HttpServletRequest
和HttpServletResponse
之后,会先调用doFilter
方法- doFilter方法可以控制请求是否继续,如果放行,则继续,否则请求被拦住,过滤器本身做出响应
- Filter不仅仅是可以对请求做响应,也可以在目标资源响应前,对响应再次处理
- Filter是GOF中责任链的典型案例
- 常用应用包括但不限于:登录权限、乱码处理、过滤敏感字符、日志记录、性能分析、事务控制、跨域处理…
- 生活案例:地铁站安保,会检查行李,再根据情况放行还是拦下
图解(filter也可以在转换成响应报文前,抓响应对象resp)
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...");
}
}
控制台输出
上述过程图解
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>
的顺序(优先级要低) - 注:如果是注解的方式,是根据类名来排序的
注:每个过滤器过滤的范围是不同的,即客户访问不同的目标资源,过滤器的个数是可以不一样的
上述过程的图解:
新建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
上述代码运行图解
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 的一种方式。
- 我们之前学的是通过form表单标签、a标签来发起请求,而现在只需要通过触发js代码就可以动态请求
- 通过JS运行代码,无需跳转界面(当然也可以自行设定是否跳转界面)
- 当接收到返回结果后,通过DOM编程渲染到相关元素上,实现局部更新
5.2 Ajax的使用
原生JS写法(了解),后面优化的话,jquery可以封装这个过程但不是最优解,后续我们直接用VUE框架的axios promise帮助我们完成即可
- 创建
XMLHttpRequest
对象 - 执行回调函数
onreadystatechange
,解析回送报文,动态更新组件- xmlhttp.readyState=4表示请求完成,xmlhttp.status表示
响应状态码
(请区别后续JSON的业务状态码
)
- xmlhttp.readyState=4表示请求完成,xmlhttp.status表示
- 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);
}
}
}
测试

