Servlet(三)

🍵 Servlet简称Server Applet(即服务器端小程序),是一种基于Java技术的Web组件,运行在服务器端。

1 Cookie会话

  • Cookie会话
    • HTTP(即超文本传输协议)是一个基于请求与响应模式的无状态协议。
    • 用户通过浏览器访问Web应用时,服务器需要保存并跟踪用户的状态。
    • HTTP协议无状态,无法保存和跟踪用户状态,采用会话技术来解决。
    • 从用户打开浏览器访问某个网站到关闭浏览器的过程,称为一次会话。
    • 会话技术指的是在会话过程中帮助服务器记录用户状态和数据的技术。
    • 常用的会话技术:Cookie客户端会话技术、Session服务器端会话技术。
    • Cookie是服务器发送给浏览器的小段文本信息,存储在客户端浏览器内存或硬盘上。
    • 浏览器保存Cookie之后,每次访问服务器,都会在HTTP请求头中将其回传给服务器。
    • 分类
      • 持久的Cookie:Cookie会以文本文件的形式保存到用户的硬盘上。
      • 会话级别Cookie(默认):保存到浏览器内存,浏览器关闭则失效。
    • 访问

1-1 访问时间类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import java.util.Date;
import java.io.IOException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import javax.servlet.http.Cookie;
import java.text.SimpleDateFormat;
import javax.servlet.http.HttpServlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/MyServlet37")
public class MyServlet37 extends HttpServlet {
private static final long serialVersionUID = -5604481158386227221L;

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
/**
* 1、获取所有的cookie,判断是否是第一次访问
* 2、如果是第一次访问,输出欢迎,记录当前的时间,回写到浏览器
* 3、如果不是第一次访问,获取时间,输出到浏览器,记录当前的时间,回写到浏览器
* 4、记录当前的时间,回写到浏览器
*/
// 设置字符中文乱码问题
response.setContentType("text/html;charset=UTF-8");
// 获取所有的cookie
Cookie[] cookies = request.getCookies();
// 通过指定cookie名称来查找cookie:Cookie c = new Cookie("last","当前的时间");
Cookie cookie = getCookieByName(cookies, "lastTime");
// 判断,如果cookie==null,说明是第一次访问
if (cookie == null) {
// 输出欢迎,记录当前的时间,回写到浏览器
response.getWriter().write(
"<div style='text-align: center;'>"
+ "<div style='display: inline-block; text-align: left;'>"
+ "www.baidu.com" + "<br />"
+ "欢迎访问百度一下!" + "</div>" + "</div>"
);
} else {
// 获取cookie的值,输出浏览器,记录当前的时间,回写到浏览器
String value = cookie.getValue();
// 输出浏览器(cookie的值中含有“ ”,需要进行解码)
response.getWriter().write(
"<div style='text-align: center;'>"
+ "<div style='display: inline-block; text-align: left;'>"
+ "www.baidu.com" + "<br />"
+ "百度欢迎您的归来!<br />您上次的访问时间:" + URLDecoder.decode(value, "UTF-8") + "<br />"
+ "<a href=\"/servletDemo/MyServlet38\">清除Cookie</a>" + "</div>" + "</div>"
);
}
// 记录当前的时间
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String sDate = sdf.format(date);
// 回写到浏览器
// 使用cookie回写(cookie的值中含有“ ”,需要进行编码才能使用)
Cookie c = new Cookie("lastTime", URLEncoder.encode(sDate, "UTF-8"));
// 设置有效时间为一天,单位秒
c.setMaxAge(60 * 60 * 24);
// 设置有效路径
c.setPath("/servletDemo");
// 回写,在响应头中增加一个相应的Set-Cookie头字段
response.addCookie(c);
}

public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}

// 通过指定名称查找指定的cookie
public static Cookie getCookieByName(Cookie[] cookies, String name) {
// 如果数组是null
if (cookies == null) {
return null;
} else {
// 循环遍历,目的是和name进行匹配,匹配成功则返回当前的cookie
for (Cookie cookie : cookies) {
// 获取cookie的名称,和name进行匹配
if (cookie.getName().equals(name)) {
return cookie;
}
}
return null;
}
}
}

1-2 移除会话类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.io.IOException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/MyServlet38")
public class MyServlet38 extends HttpServlet {
private static final long serialVersionUID = 1L;

protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取cookie
Cookie cookie = new Cookie("lastTime", "");
// 设置有效时间为0,删除cookie
cookie.setMaxAge(0);
// 设置有效路径,必须与要删除的Cookie的路径一致
cookie.setPath("/servletDemo");
// 回写
response.addCookie(cookie);
// 重定向商品列表页面
response.sendRedirect("/servletDemo/MyServlet37");
}

protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}

2 Session会话

  • Session会话
    • 服务器端会话,浏览器访问Web服务器资源时,服务器为每个用户浏览器创建一个Session对象。
    • 每个浏览器独占一个Session对象,用户访问服务器资源时,可以将数据保存在各自的Session中。
    • Session vs Cookie
      • Session不支持跨域名访问,Cookie支持跨域名访问。
      • Session存放的数据类型是对象,Cookie存放的数据类型则是字符串。
      • Session数据存在服务器端,安全性高,Cookie明文传递,安全性低。
      • Session每个用户独占一个,占用资源,Cookie在客户端,不占资源。
      • Session将数据存储在服务器端,Cookie存放在客户端浏览器内存或硬盘上。
      • Session的大小和数量一般不受限制,浏览器对Cookie的大小和数量有限制。
    • 生命周期
      • Session对象在容器中第一次调用request.getSession()方法时创建。
      • 客户端访问的Web资源是静态资源时,服务器不会创建Session对象。
      • 对象被销毁的三种情况
        • Session过期。
        • 调用session.invalidate()方法,手动销毁。
        • 服务器关闭或应用在服务器中部署被卸载。

2-1 设置过期时间

  • 设置过期时间
    • Session对象在服务器中驻留一段时间后未被使用,就会被销毁,这个时间即过期时间。
    • 默认过期时间是30分钟,可通过使用<session-config>或调用方法两种方式进行设置。

(1) <session-config>元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
id="WebApp_ID" metadata-complete="false" version="4.0">
<display-name>servletDemo</display-name>

<!-- 设置Session过期时间 -->
<session-config>
<!-- 指定默认Session的过期时间,单位分钟,必须为整数,值为0或负数表示永不过期 -->
<session-timeout>10</session-timeout>
</session-config>
</web-app>

(2) setMaxInactiveInterval()

1
2
// 设置会话的过期时间,单位秒,0或负数表示永不过期
request.getSession().setMaxInactiveInterval(60);

2-2 Session域对象

  • Session域对象
    • Session也是域对象,可对属性进行操作,实现会话中请求之间的数据通讯和数据共享。
    • Session、Request、ServletContext合称为Servlet的三大域对象,都能保存和传递数据。
不同 Session Request ServletContext
类型 HttpSession HttpServletRequest ServletContext
数量 多个Session 多个Request对象 唯一Context对象
创建 容器第一次调用getSession()时创建 客户端向容器发送请求时创建 Servlet容器启动时创建
销毁 过期、手动调用invalidate()、关闭服务器 容器对这次请求做出响应之后 容器关闭或应用被移除时
数据共享 会话内多请求共享 请求内组件共享 跨会话共享
有效范围 会话期间的所有Servlet 当前请求相关的Servlet 整个应用内的所有Servlet

2-3 创建访问时间类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import java.util.Date;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/MyServlet39")
public class MyServlet39 extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 设置页面输出的格式
response.setContentType("text/html;charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.write(
"<div style='text-align: center;'>"
+ "<div style='display: inline-block; text-align: left;'>"
+ "https://www.baidu.com/" + "<br />"
+ "百度一下,欢迎您的到来!"
);
// 记录当前的时间
Date date = new Date();
// 时间的格式
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 会话创建时间
Date CreationTime = new Date(request.getSession().getCreationTime());
// 会话上次关联的时间
Date LastAccessedTime = new Date(request.getSession().getLastAccessedTime());
// 格式化
String sDate = sdf.format(date);
// 将当前时间赋值到session域对象中
request.getSession().setAttribute("lastTime", sDate);
// 设置会话的失效时间
request.getSession().setMaxInactiveInterval(100);
// 对session中各个信息输出到页面
writer.write(
"当前时间:" + sDate + "<br />"
+ "创建此会话的时间:" + sdf.format(CreationTime) + "<br />"
+ "当前会话的SessionID: " + request.getSession().getId() + "<br />"
+ "Sesssion上次关联的时间:" + sdf.format(LastAccessedTime) + "<br />"
+ "会话保持打开状态的最大时间间隔:" + request.getSession().getMaxInactiveInterval()
+ "<br />" + "</div>" + "</div>"
);
// 浏览器不支持Cookie
String url = response.encodeURL("/servletDemo/MyServlet40");
writer.write("<center><a href=" + url + ">再次访问!</a></center>");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}

2-4 创建再次访问类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.util.Date;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/MyServlet40")
public class MyServlet40 extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter writer = response.getWriter();
// 从session中获取上次访问的时间
String value = (String) request.getSession().getAttribute("lastTime");
// 不是第一次访问
writer.write(
"<div style='text-align: center;'>"
+ "<div style='display: inline-block; text-align: left;'>"
+ "https://www.baidu.com/" + "<br />"
+ "百度一下,欢迎您的归来!" + "<br />"
+ "您上次的访问时间是" + value + "</div>" + "</div>"
);
Date date = new Date();
// 时间的格式
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 格式化
String sDate = sdf.format(date);
// 将当前时间赋值到session域对象中
request.getSession().setAttribute("lastTime", sDate);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}

3 Filter过滤器

  • Filter过滤器
    • Filter过滤器在Servlet版本2.3中新定义,支持对对象进行检查修改。
    • 是Servlet规范中最实用的技术,通过拦截从而实现一些特殊的功能。
    • 生命周期
      • 初始化阶段:调用init()方法初始化Filter实例,init()方法在生命周期内只执行一次。
      • 拦截和过滤阶段:容器将对象以参数形式传递给doFilter(),调用方法进行拦截和过滤。
      • 销毁阶段:调用destory()释放过滤器占用的资源,destory()在生命周期内只执行一次。
    • 过滤器拦截资源被容器调用的方式
      • FORWARD:目标资源通过RequestDispatcher的forward()方法访问,过滤器将被调用。
      • INCLUDE:目标资源通过RequestDispatcher的include()方法访问,该过滤器将被调用。
      • ERROR:目标资源通过声明式异常处理机制访问,该过滤器将被调用,除此之外不被调用。
      • REQUEST:目标资源通过RequestDispatcher的include()或forward()访问,过滤器不被调用。

3-1 web.xml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
id="WebApp_ID" metadata-complete="false" version="4.0">
<display-name>servletDemo</display-name>

<!-- 注册Filter -->
<filter>
<!-- 过滤器的注册名,必填 -->
<filter-name>MyServlet41</filter-name>
<!-- 过滤器的完整限定名(包名+类名) -->
<filter-class>MyServlet41</filter-class>

<!-- 为过滤器指定初始化参数 -->
<init-param>
<!-- 为过滤器指定初始化参数的名称 -->
<param-name>name</param-name>
<!-- 为过滤器指定初始化参数的值 -->
<param-value>百度一下</param-value>
</init-param>
<init-param>
<param-name>URL</param-name>
<param-value>https://www.baidu.com/</param-value>
</init-param>
</filter>

<!-- 映射Filter,设置拦截的资源 -->
<filter-mapping>
<!-- 过滤器的注册名,必须在<filter-name>中声明过 -->
<filter-name>MyServlet41</filter-name>
<!-- 过滤器拦截的请求路径 -->
<url-pattern>/MyServlet41</url-pattern>
<!-- 过滤器拦截的资源被Servlet容器调用的方式,默认REQUEST -->
<!-- 可以是REQUEST、INCLUDE、FORWARD、ERROR之一,支持设置多个 -->
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>
<filter-mapping>
<filter-name>MyServlet41</filter-name>
<!-- 过滤器拦截的Servlet名称 -->
<servlet-name>ServletDemo</servlet-name>
</filter-mapping>
</web-app>

3-2 @WebFilter配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.DispatcherType;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;

@WebFilter(
dispatcherTypes = {
DispatcherType.REQUEST, DispatcherType.FORWARD,
DispatcherType.INCLUDE, DispatcherType.ERROR
},
asyncSupported = true, description = "过滤器Filter",
urlPatterns = { "/MyServlet41" }, initParams = {
@WebInitParam(name = "name", value = "百度一下", description = "name的描述")
}, servletNames = { "servletDemo" }
)
public class MyServlet41 implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
}
}

3-3 创建一个过滤器类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.DispatcherType;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;

@WebFilter(
dispatcherTypes = {
DispatcherType.REQUEST, DispatcherType.FORWARD,
DispatcherType.INCLUDE, DispatcherType.ERROR
},
urlPatterns = { "/MyServlet42" }
)
public class MyServlet42 implements Filter {
public void destroy() {
}

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 设置向页面输出的格式与编码
response.setContentType("text/html;charset=UTF-8");
// 通过Filter向页面输出内容
response.getWriter().write("<center>欢迎访问【MyServlet42】页面!</center>");
}

public void init(FilterConfig fConfig) throws ServletException {
}
}

3-4 创建过滤器拦截类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.io.IOException;
import javax.servlet.http.HttpServlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/MyServlet43")
public class MyServlet43 extends HttpServlet {
private static final long serialVersionUID = 1L;

public MyServlet43() {
super();
}

protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 设置向页面输出的格式与编码
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("<center>欢迎访问【MyServlet43】页面!</center>");
}

protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}

4 FilterChain

  • FilterChain
    • 在Web应用中可以部署多个Filter,如果这些Filter都拦截同一目标资源,那么就组成一个过滤器链。
    • 过滤器链中的过滤器负责特定的操作和任务,客户端的请求在它们之间传递,直到传递给目标资源。
    • javax.servlet包中提供了一个FilterChain接口,该接口由容器实现。
    • 使用FilterChain对象调用过滤器链中的下一个Filter的doFilter()方法。
    • 如果Filter是链中的最后一个过滤器,则调用目标资源的service()方法。
    • 过滤器链中Filter的执行顺序
      • web.xml配置的Filter,执行顺序由<filter-mapping>决定,靠前先执行。
      • @WebFilter注解配置的Filter,无法进行排序,需排序建议使用web.xml。

4-1 web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
id="WebApp_ID" metadata-complete="false" version="4.0">
<display-name>servletDemo</display-name>

<!-- 过滤器链中FirstChainFilter配置 -->
<filter>
<filter-name>FirstChainFilter</filter-name>
<filter-class>MyServlet45</filter-class>
</filter>
<filter-mapping>
<filter-name>FirstChainFilter</filter-name>
<url-pattern>/MyServlet44</url-pattern>
</filter-mapping>

<!-- 过滤器链中SecondChainFilter配置 -->
<filter>
<filter-name>SecondChainFilter</filter-name>
<filter-class>MyServlet46</filter-class>
</filter>
<filter-mapping>
<filter-name>SecondChainFilter</filter-name>
<url-pattern>/MyServlet44</url-pattern>
</filter-mapping>
</web-app>

4-2 创建登录类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/MyServlet44")
public class MyServlet44 extends HttpServlet {
private static final long serialVersionUID = 1L;

public MyServlet44() {
super();
}

protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("<center>欢迎访问【MyServlet44】页面!</center>");
}

protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}

4-3 FirstChainFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.ServletException;

public class MyServlet45 implements Filter {
public MyServlet45() {
}

public void destroy() {
}

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 设置向页面输出的格式与编码
response.setContentType("text/html;charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.write("<div style='width: fit-content; margin: 0 auto; text-align: left;'>");
writer.write("FirstChainFilter 对请求进行处理!<br />");
chain.doFilter(request, response);
writer.write("FirstChainFilter 对响应进行处理!<br />");
writer.write("</div>");
}

public void init(FilterConfig fConfig) throws ServletException {
}
}

4-4 SecondChainFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.ServletException;

public class MyServlet46 implements Filter {
public MyServlet46() {
}

public void destroy() {
}

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 设置向页面输出的格式与编码
response.setContentType("text/html;charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.write("<div style='width: fit-content; margin: 0 auto; text-align: left;'>");
writer.write("SecondChainFilter 对请求进行处理!<br />");
chain.doFilter(request, response);
writer.write("SecondChainFilter 对响应进行处理!<br />");
writer.write("</div>");
}

public void init(FilterConfig fConfig) throws ServletException {
}
}

5 FilterConfig

  • FilterConfig
    • javax.servet包中提供了一个FilterCofig接口,与ServletConfig接口相似,由容器实现的。
    • 用于在过滤器初始化期间向其传递信息,容器将FilterCofig作为参数传入过滤器init()中。
    • 启动Tomcat服务,访问:http://localhost:8080/servletDemo/WebContent/logon.html
    • 如果用户输入admin、root或user,则提示账号存在风险,如果输入其他则显示欢迎用语。

5-1 web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
id="WebApp_ID" metadata-complete="false" version="4.0">
<display-name>servletDemo</display-name>

<filter>
<filter-name>BlackListFilter</filter-name>
<filter-class>MyServlet47</filter-class>

<init-param>
<param-name>blackList1</param-name>
<param-value>admin</param-value>
</init-param>

<init-param>
<param-name>blackList2</param-name>
<param-value>root</param-value>
</init-param>

<init-param>
<param-name>blackList3</param-name>
<param-value>user</param-value>
</init-param>
</filter>

<filter-mapping>
<filter-name>BlackListFilter</filter-name>
<url-pattern>/logon</url-pattern>
</filter-mapping>
</web-app>

5-2 黑名单类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import java.io.IOException;
import javax.servlet.Filter;
import java.util.Enumeration;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.ServletException;

public class MyServlet47 implements Filter {
private FilterConfig fConfig;

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
response.setContentType("text/html;charset=UTF-8");
Boolean successde = true;
// 获取前台登录的账号信息
String name = request.getParameter("username");
// 获取过滤器中的初始化参数
Enumeration<String> blackListNames = fConfig.getInitParameterNames();
// 判断前台登录账号是否为空
if (name == null || "".equals(name)) {
response.getWriter().write("<center>用户名不能为空!</center>");
} else {
// 登录账号不为空,循环遍历黑名单
while (blackListNames.hasMoreElements()) {
// 若登录账号是黑名单账号则不允许登录
if (fConfig.getInitParameter(blackListNames.nextElement()).equals(name)) {
successde = false;
}
}
if (successde) {
chain.doFilter(request, response);
} else {
response.getWriter().write(
"<h1 align=\"center\" style=\"font-family:arial;color:red;\">"
+ "温馨提示:您的账号存在风险,暂时不能为您提供服务!</h1>\n"
);
}
}
}

public void init(FilterConfig fConfig) throws ServletException {
this.fConfig = fConfig;
}
}

5-3 用户登录类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/logon")
public class MyServlet48 extends HttpServlet {
private static final long serialVersionUID = 1L;

public MyServlet48() {
super();
}

protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("<h1 align=\"center\">百度百科 欢迎您!</h1>");
}

protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}

5-4 logon.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html>

<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>FilterConfig</title>
</head>

<body>
<form action="/servletDemo/logon" method="GET" style="text-align:center;">
<table border="1" width="50%" style="margin: 0 auto;">
<tr>
<td colspan="2" align="center">
FilterConfig
</td>
</tr>
<tr>
<td>用户</td>
<td>
<input type="text" name="username" />
</td>
</tr>
<tr>
<td>密码</td>
<td>
<input type="password" name="password" />
</td>
</tr>
<tr>
<td colspan="2" align="center">
<input type="submit" value="提交" />
</td>
</tr>
</table>
</form>
</body>

</html>

6 Servlet监听器

  • Servlet监听器
    • Listener是一个实现特定接口的Java程序,用于监听另一Java对象的方法调用或属性改变。
    • Servlet规范定义了8个监听器接口,开发监听器需实现相应的监听器接口并重写接口方法。
    • 监听对象的创建和销毁:ServletContextListener、HttpSessionListener、ServletRequestListener。
    • 监听对象中的属性变更
      • HttpSessionAttributeListener。
      • ServletContextAttributeListener。
      • ServletRequestAttributeListener。
    • 监听HttpSession的对象状态改变:HttpSessionBindingListener、HttpSessionActivationListener。
    • 用HttpSessionBindingListener、HttpSessionActivationListener时不必注册,直接创建类实现接口。

6-1 web.xml注册监听器

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
id="WebApp_ID" metadata-complete="false" version="4.0">
<display-name>servletDemo</display-name>

<listener>
<listener-class>MyServlet49</listener-class>
</listener>
</web-app>

6-2 @WebListener注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import java.io.InputStream;
import java.util.Properties;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

@WebListener
public class MyServlet49 implements ServletContextListener {
public MyServlet49() {
}

// 应用启动时调用
public void contextInitialized(ServletContextEvent sce) {
ServletContext context = sce.getServletContext();
System.out.println("应用启动,开始加载配置...");

try (InputStream in = context.getResourceAsStream("/WEB-INF/classes/db.properties")) {
Properties properties = new Properties();
if (in != null) {
properties.load(in);
// 将配置保存到ServletContext,方便后面获取
context.setAttribute("config", properties);
System.out.println("配置文件加载完成:" + properties);
} else {
System.out.println("配置文件并未找到!");
}
} catch (Exception e) {
e.printStackTrace();
}
}

// 应用关闭时调用
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("应用关闭,准备释放资源...");
ServletContext context = sce.getServletContext();

// 这里可以关闭数据库连接池或释放其他资源
// 例如:context.removeAttribute("config");
context.removeAttribute("config");

System.out.println("资源释放完毕,应用关闭完成。");
}
}

6-3 创建Servlet读取属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Properties;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/MyServlet50")
public class MyServlet50 extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

Properties config = (Properties) getServletContext().getAttribute("config");

response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();

out.println("<html><head><title>Servlet监听器</title></head><body>");
out.println("<center><h1>应用配置信息获取</h1></center>");

if (config != null) {
out.println("<div style='text-align:center;'>");
out.println("<ul style='display:inline-block; text-align:left; list-style:none; padding:0;'>");
for (String key : config.stringPropertyNames()) {
out.printf("<li>%s = %s</li>", key, config.getProperty(key));
}
out.println("</ul>");
out.println("</div>");
} else {
out.println("<center><p>未加载到配置信息。</p></center>");
}

out.println("</body></html>");
out.close();
}
}

7 结合实现网站在线人数统计

7-1 登录类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpSession;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/LoginServlet1")
public class MyServlet51 extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
request.setCharacterEncoding("utf-8");
PrintWriter writer = response.getWriter();

String username = request.getParameter("username");
if (username == null || "".equals(username)) {
response.sendRedirect("/servletDemo/WebContent/enroll1.html");
return;
}

HttpSession session = request.getSession();
String sessionUser = (String) session.getAttribute("username");

ConcurrentHashMap<String, String> map =
(ConcurrentHashMap<String, String>) getServletContext().getAttribute("onLineUserMap");
if (map == null) {
map = new ConcurrentHashMap<>();
getServletContext().setAttribute("onLineUserMap", map);
}
String sessionId = session.getId();
String existSid = map.get(username);

writer.write("<html><head><title>登录页面</title></head><body style='text-align:center;'>");

if (sessionUser != null && !"".equals(sessionUser)) {
// 当前session已登录
writer.write("<h3>您好,您已经登录了账户:" + sessionUser + "</h3>");
writer.write("<p>如要登录其他账号,请先退出当前账号重新登录!</p>");
} else if (existSid != null && !sessionId.equals(existSid)) {
// 账号已在其他session(浏览器)登录
writer.write("<h3>账号[" + username + "]已在其他浏览器登录,请先在其他地方退出!</h3>");
writer.write("<a href=\"/servletDemo/WebContent/enroll1.html\">返回登录!</a>");
writer.write("</body></html>");
return;
} else {
// 登录成功
session.setAttribute("username", username);
// 这里不需要再操作map,监听器已处理
writer.write("<h3>" + username + ",欢迎您的到来</h3>");
}

writer.write("<h3>当前在线人数为:" + map.size() + "人</h3><table border=\"1\" width=\"50%\" align=\"center\">");
for (String user : map.keySet()) {
writer.write("<tr><td align=\"center\">" + user + "</td></tr>");
}
writer.write("</table>");
writer.write("<br/><a href=\"/servletDemo/LogoutServlet1\">退出登录!</a>");
writer.write("</body></html>");
}

protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}

7-2 登出类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/LogoutServlet1")
public class MyServlet52 extends HttpServlet {
private static final long serialVersionUID = 1L;

protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 退出登录操作,将此次session进行销毁,触发HttpSessionListener监听器的sessionDestroyed方法
request.getSession().invalidate();
// 跳转回登录页面
response.sendRedirect("/servletDemo/WebContent/enroll1.html");
}

protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}

7-3 监听器类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionListener;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionAttributeListener;

@WebListener
public class MyServlet53 implements HttpSessionListener, HttpSessionAttributeListener {
@SuppressWarnings("unchecked")
@Override
public void attributeAdded(HttpSessionBindingEvent se) {
if ("username".equals(se.getName())) {
ServletContext application = se.getSession().getServletContext();
ConcurrentHashMap<String, String> map =
(ConcurrentHashMap<String, String>) application.getAttribute("onLineUserMap");
if (map == null) {
map = new ConcurrentHashMap<>();
application.setAttribute("onLineUserMap", map);
}
String username = (String) se.getValue();
String sid = se.getSession().getId();
// 判断,如果该账号不在列表或已被自己占用,则强制添加,更新为最新sessionId
map.put(username, sid);
}
}

@SuppressWarnings("unchecked")
@Override
public void sessionDestroyed(HttpSessionEvent se) {
HttpSession session = se.getSession();
ServletContext application = session.getServletContext();
ConcurrentHashMap<String, String> map =
(ConcurrentHashMap<String, String>) application.getAttribute("onLineUserMap");
if (map == null)
return;
String username = (String) session.getAttribute("username");
if (username != null) {
String nowSid = map.get(username);
// 仅删除自己(防止反复登录后,之前的会话销毁而踢掉别人)
if (session.getId().equals(nowSid)) {
map.remove(username);
}
}
}

@Override
public void attributeRemoved(HttpSessionBindingEvent se) {
}

@Override
public void attributeReplaced(HttpSessionBindingEvent se) {
}

@Override
public void sessionCreated(HttpSessionEvent se) {
}
}

7-4 enroll1.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html>

<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>结合实现网站在线人数统计</title>
</head>

<body>
<form action="/servletDemo/LoginServlet1" method="GET" style="text-align:center;">
<table border="1" width="50%" style="margin: 0 auto;">
<tr>
<td colspan="2" align="center">结合实现网站在线人数统计</td>
</tr>
<tr>
<td>账号</td>
<td><input type="text" name="username" /></td>
</tr>
<tr>
<td colspan="2" align="center"><input type="submit" value="提交" />
</td>
</tr>
</table>
</form>
</body>

</html>

8 HttpSessionBindingListener

8-1 登录类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import java.util.Set;
import java.util.HashSet;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpSession;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/LoginServlet2")
public class MyServlet54 extends HttpServlet {
private static final long serialVersionUID = 1L;

@SuppressWarnings("unchecked")
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
request.setCharacterEncoding("utf-8");
PrintWriter writer = response.getWriter();
String username = request.getParameter("username");
if (username == null || "".equals(username)) {
response.sendRedirect("/servletDemo/WebContent/enroll2.html");
return;
}

// 获取全局唯一用户名集合
Set<String> onlineUserSet = (Set<String>) getServletContext().getAttribute("onlineUserSet");
if (onlineUserSet == null) {
onlineUserSet = new HashSet<>();
getServletContext().setAttribute("onlineUserSet", onlineUserSet);
}

HttpSession session = request.getSession();
// 若当前session已登录过则直接展示
MyServlet56 loginedDemo = (MyServlet56) session.getAttribute("onlineUserBindingListener");

// 未登录才处理
if (loginedDemo == null) {
// 多线程同步
synchronized (onlineUserSet) {
if (onlineUserSet.contains(username)) {
writer.write("<center><h3>用户[" + username + "]已经登录!不能重复登录!</h3></center>");
writer.write("<center>如要重新登录,请先退出其他会话!</center><br/>");
writer.write("<center><a href=\"/servletDemo/LogoutServlet2\">退出登录!</a></center>");
return;
}
// 没有登录,允许添加
onlineUserSet.add(username);
session.setAttribute("onlineUserBindingListener", new MyServlet56(username));
writer.write("<center>" + username + ",欢迎您的到来!</center>");
}
} else {
String logined = loginedDemo.getUsername();
writer.write(
"<center><h3>您好,您已经登录了账户:" + logined + "</h3></center>" +
"<center>如要登录其他账号,请先退出当前账号!</center>"
);
}

// 显示在线用户
writer.write("<div style=\"width:50%; margin: 0 auto; text-align: center;\">");
writer.write("<h1>当前在线人数为:" + onlineUserSet.size() + "</h1>");
writer.write("<table border=\"1\" width=\"100%\" style=\"margin: 0 auto;\">");
for (String user : onlineUserSet) {
writer.write("<tr><td align=\"center\">" + user + "</td></tr>");
}
writer.write("</table>");
writer.write("<br/><a href=\"/servletDemo/LogoutServlet2\">退出登录!</a>");
writer.write("</div>");
}

protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}

8-2 登出类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.util.Set;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpSession;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/LogoutServlet2")
public class MyServlet55 extends HttpServlet {
private static final long serialVersionUID = 1L;

@SuppressWarnings("unchecked")
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession(false);
if (session != null) {
// 取出用户名
MyServlet56 user = (MyServlet56) session.getAttribute("onlineUserBindingListener");
if (user != null) {
String username = user.getUsername();
Set<String> onlineUserSet = (Set<String>) getServletContext().getAttribute("onlineUserSet");
if (onlineUserSet != null) {
synchronized (onlineUserSet) {
onlineUserSet.remove(username);
}
}
session.removeAttribute("onlineUserBindingListener");
}
session.invalidate();
}
response.sendRedirect("/servletDemo/WebContent/enroll2.html");
}

protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}

8-3 监听器类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import java.util.List;
import java.util.ArrayList;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;

public class MyServlet56 implements HttpSessionBindingListener {
private String username;

public MyServlet56(String username) {
super();
this.username = username;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public MyServlet56() {
}

@SuppressWarnings("unchecked")
public void valueBound(HttpSessionBindingEvent event) {
HttpSession session = event.getSession();
ServletContext application = session.getServletContext();
// 把用户名放入在线列表
List<String> onlineUserList = (List<String>) application.getAttribute("onlineUserList");
// 第一次使用前,需要初始化
if (onlineUserList == null) {
onlineUserList = new ArrayList<String>();
}
onlineUserList.add(this.username);
application.setAttribute("onlineUserList", onlineUserList);
System.out.println("***用户:" + this.username + ",已成功加入在线用户列表!");
}

@SuppressWarnings("unchecked")
public void valueUnbound(HttpSessionBindingEvent event) {
HttpSession session = event.getSession();
ServletContext application = session.getServletContext();
// 从在线列表中删除用户名
List<String> onlineUserList = (List<String>) application.getAttribute("onlineUserList");
onlineUserList.remove(this.getUsername());
application.setAttribute("onlineUserList", onlineUserList);
System.out.println("用户[" + this.username + "]退出。");
System.out.println("当前在线人数:" + onlineUserList.size() + "人");
}
}

8-4 enroll2.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html>

<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>HttpSessionBindingListener</title>
</head>

<body>
<form action="/servletDemo/LoginServlet2" method="GET" style="text-align:center;">
<table border="1" width="50%" style="margin: 0 auto;">
<tr>
<td colspan="2" align="center">HttpSessionBindingListener</td>
</tr>
<tr>
<td>账号</td>
<td><input type="text" name="username" /></td>
</tr>
<tr>
<td colspan="2" align="center"><input type="submit" value="提交" />
</td>
</tr>
</table>
</form>
</body>

</html>

Servlet(三)
https://stitch-top.github.io/2025/11/27/java/java07-servlet-san/
作者
Dr.626
发布于
2025年11月27日 21:40:00
许可协议