背景
本文是《JavaEE 后端从小白到大神》修仙系列第二篇,正式进入JavaEE后端世界。若想详细学习请点击首篇博文,我们开始把。
第二篇:Servlet 深入理解与实战
- Servlet 生命周期;
- HttpServletRequest / HttpServletResponse;
- 过滤器 Filter 与监听器 Listener;
- ServletContext;
- 异步 Servlet;
- Session 管理机制;
- Tomcat 的工作原理(Connector + Container)。
一、 Servlet 是什么
Servlet(Server + Applet)是运行在 Web 容器(例如 Tomcat)中,用来处理客户端请求并返回响应的 Java 组件。
换句话说:
当你在浏览器里输入:
1
|
http://localhost:8080/login
|
请求会被 Tomcat 接收,然后交给你编写的 Servlet 类 去处理。
Servlet 在整个 Web 流程中的位置:
1
2
3
4
5
6
7
8
9
|
浏览器 --> HTTP 请求 --> Tomcat (Connector)
↓
Servlet 容器 (Container)
↓
找到对应的 Servlet 实例
↓
调用 service() → doGet() / doPost()
↓
生成 Http 响应返回给浏览器
|
所以 Servlet 就是:
- 一个 运行在服务端的 Java 类;
- 是 Tomcat 等 Web 容器执行的最小单元;
- 每一个 HTTP 请求最终都由某个 Servlet 处理。
你整理的内容覆盖了Servlet生命周期的核心知识点,逻辑框架清晰,但部分细节存在表述偏差和冗余,比如生命周期阶段划分、方法调用顺序等。下面我会基于“知识点准确落地、逻辑连贯紧凑”的原则,对内容进行优化重构,确保每个点都有明确依据且无空话。
二、Servlet生命周期
1. 周期流程
生命周期是怎么来的,Servlet生命周期是由Servlet 规范(Servlet Specification) 定义的标准流程,由Servlet容器(如Tomcat)严格执行,核心是:
完整流程,共5个阶段。
(1)阶段定义与执行流程
| 阶段 |
核心操作 |
执行时机 |
执行次数 |
| 1. 加载与实例化 |
容器读取配置(web.xml/注解),通过Class.forName()加载Servlet类,用newInstance()创建实例 |
① 容器启动时(配置load-on-startup≥0);② 首次请求时(未配置或load-on-startup<0) |
1次(单实例特性) |
| 2. 初始化(init()) |
调用init(ServletConfig config),初始化资源(如加载配置文件、创建连接池) |
实例创建后立即执行 |
1次 |
| 3. 请求处理(service()) |
容器为每个请求分配独立线程,调用service(req, resp),内部根据请求方式分发到doGet()/doPost() |
每次请求到达时 |
N次(与请求数一致) |
| 4. 销毁(destroy()) |
调用destroy(),释放资源(如关闭连接池、清理缓存) |
容器关闭/应用卸载前 |
1次 |
| 5. 垃圾回收(GC) |
JVM回收已销毁的Servlet实例内存 |
实例无引用后(destroy()执行后) |
1次 |
(2)执行流程示意图
1
2
3
4
5
|
Tomcat启动 → 解析web.xml/注解 → 加载Servlet类 → new Servlet实例 → init(config) → 等待请求
↓
每接收1个请求 → 分配线程 → 调用service(req, resp) → 处理并响应
↓
Tomcat关闭/应用卸载 → 调用destroy() → 释放资源 → JVM GC回收实例
|
2. 容器是怎么初始化 Servlet 的
Servlet 的初始化由容器主动触发,当 Tomcat 启动时,它会读取你的应用配置,核心是“读取配置→判断加载时机→创建并初始化实例”,具体步骤如下:
-
读取配置:容器启动时,扫描应用的web.xml文件或Servlet类上的@WebServlet注解,获取Servlet的全类名、访问路径、load-on-startup值等信息。
-
创建包装对象:容器为每个Servlet创建ServletWrapper对象,存储其配置信息(如ServletConfig),统一管理生命周期。
-
判断加载时机:
- 若配置
load-on-startup≥0(如@WebServlet(loadOnStartup = 1)或web.xml中<load-on-startup>1</load-on-startup>):容器启动时立即加载并初始化。
- 若
load-on-startup<0或未配置:容器延迟加载,直到第一个请求访问该Servlet时才加载并初始化。
-
执行初始化:通过反射创建实例并调用init(),代码逻辑如下(容器内部逻辑简化):
1
2
3
4
5
6
|
// 1. 加载类
Class<?> servletClass = Class.forName("com.example.LoginServlet");
// 2. 创建实例(无参构造器必须存在,否则报错)
Servlet servlet = (Servlet) servletClass.newInstance();
// 3. 初始化(传入ServletConfig,包含该Servlet的局部配置)
servlet.init(servletConfig);
|
3. 加载优先级与多Servlet协同
load-on-startup的核心作用是“控制Servlet的加载顺序”,仅对“容器启动时加载”的Servlet生效,规则无冗余且严格落地:
-
取值含义:
- 正整数(如1、2)或0:容器启动时加载,数字越小,加载优先级越高(1比2先加载)。
- 负数(如-1)或未配置:延迟加载(首次请求时加载),无优先级顺序。
-
多Servlet协同示例(web.xml配置):
1
2
3
4
5
6
7
8
9
10
11
12
|
<!-- 1号优先级:最先加载 -->
<servlet>
<servlet-name>DBInitServlet</servlet-name>
<servlet-class>com.example.DBInitServlet</servlet-class>
<load-on-startup>1</load-on-startup> <!-- 初始化数据库连接池 -->
</servlet>
<!-- 2号优先级:1加载完成后再加载 -->
<servlet>
<servlet-name>LoginServlet</servlet-name>
<servlet-class>com.example.LoginServlet</servlet-class>
<load-on-startup>2</load-on-startup> <!-- 依赖数据库连接池,需后加载 -->
</servlet>
|
-
关键注意点:若多个Servlet配置相同的load-on-startup值(如都为1),加载顺序由容器解析配置的顺序决定(如web.xml中先定义的先加载),规范未统一,需避免此情况。
4. 生命周期方法调用
以@WebServlet注解配置为例,代码仅保留核心生命周期方法,输出与实际执行完全对应:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@WebServlet(urlPatterns = "/hello", loadOnStartup = 1) // 启动时加载
public class HelloServlet extends HttpServlet {
// 1. 初始化:仅执行1次
@Override
public void init() throws ServletException {
System.out.println("1. HelloServlet 初始化完成(加载配置、创建资源)");
}
// 2. 请求处理:每次请求执行1次(由service()分发到doGet(),此处简化)
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
System.out.println("2. 处理 /hello 请求");
resp.getWriter().println("Hello, Servlet!");
}
// 3. 销毁:仅执行1次
@Override
public void destroy() {
System.out.println("3. HelloServlet 销毁(释放资源)");
}
}
|
实际执行输出
1
2
3
4
5
6
7
8
9
10
11
|
# Tomcat启动时(触发init())
1. HelloServlet 初始化完成(加载配置、创建资源)
# 浏览器第1次访问 http://localhost:8080/hello(触发doGet())
2. 处理 /hello 请求
# 浏览器第2次访问 http://localhost:8080/hello(再次触发doGet())
2. 处理 /hello 请求
# Tomcat关闭时(触发destroy())
3. HelloServlet 销毁(释放资源)
|
5. Servlet单例特性与线程安全
Servlet的“单实例、多线程”是容器设计的核心,线程安全问题必须结合代码示例说明,避免模糊表述:
(1)单实例+多线程的底层逻辑
(2) 线程安全问题根源:共享可变成员变量
错误示例:成员变量loginCounter被所有线程共享,并发修改时会出现“计数错误”(如2个线程同时读counter=1,加1后都写为2,实际应是3)。
1
2
3
4
5
6
7
8
9
10
11
|
public class UnsafeLoginServlet extends HttpServlet {
// 错误:共享可变成员变量
private int loginCounter = 0;
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 非原子操作(读→改→写),并发时数据不一致
loginCounter++;
resp.getWriter().println("登录次数:" + loginCounter);
}
}
|
(3)3种落地解决方案
| 方案 |
核心原理 |
| 1. 原子类(推荐) |
AtomicInteger等原子类保证“读-改-写”操作原子性,无需手动同步 |
代码示例
1
2
3
4
5
6
7
8
|
private AtomicInteger loginCounter = new AtomicInteger(0);
@Override
protected void doPost(...) {
// 原子操作,线程安全
int count = loginCounter.incrementAndGet();
resp.getWriter().println("登录次数:" + count);
}
|
| 方案 |
核心原理 |
| 2. 同步代码块 |
synchronized锁定当前实例,确保同一时间只有1个线程执行修改逻辑 |
1
2
3
4
5
6
7
8
9
10
|
private int loginCounter = 0;
@Override
protected void doPost(...) {
// 锁定Servlet实例,避免并发修改
synchronized (this) {
loginCounter++;
}
resp.getWriter().println("登录次数:" + loginCounter);
}
|
| 方案 |
核心原理 |
| 3. ThreadLocal(请求级隔离) |
为每个线程创建独立变量副本,线程间数据不共享(适合“请求内复用数据”场景) |
1
2
3
4
5
6
7
8
9
10
11
12
|
// 每个线程初始值为0
private ThreadLocal<Integer> threadLocalCounter = ThreadLocal.withInitial(() -> 0);
@Override
protected void doPost(...) {
int count = threadLocalCounter.get();
count++;
threadLocalCounter.set(count);
resp.getWriter().println("当前线程登录次数:" + count);
// 必须移除,避免线程池复用导致内存泄漏
threadLocalCounter.remove();
}
|
6. ServletConfig vs ServletContext区别
两者的区别需紧扣“作用范围”,结合如下配置文件和代码示例:
(1)核心区别对比
| 对比项 |
ServletConfig(局部配置) |
ServletContext(全局配置) |
| 作用范围 |
仅对应1个Servlet,私有配置 |
整个Web应用,所有组件(Servlet/Filter/Listener)共享 |
| 实例数量 |
1个Servlet对应1个ServletConfig |
1个Web应用对应1个ServletContext |
| 配置位置 |
web.xml的<servlet>内部<init-param>;或@WebServlet(initParams = {...}) |
web.xml的顶层<context-param>;或Spring的application.properties(间接) |
| 核心用途 |
获取当前Servlet的专属参数(如编码格式、专属路径) |
获取全局参数(如应用名、数据库URL)、共享应用级数据、访问web资源(如读取WEB-INF下文件) |
(2)配置与获取示例
(2-1)ServletConfig配置与获取
-
注解配置:@WebServlet中指定局部参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@WebServlet(
urlPatterns = "/login",
// 局部参数:仅当前Servlet可访问
initParams = {
@WebInitParam(name = "encoding", value = "UTF-8"),
@WebInitParam(name = "maxLoginTimes", value = "5")
}
)
public class LoginServlet extends HttpServlet {
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
// 获取局部参数
String encoding = config.getInitParameter("encoding"); // UTF-8
int maxTimes = Integer.parseInt(config.getInitParameter("maxLoginTimes")); // 5
}
}
|
(2-2)ServletContext配置与获取
7. 实战:LoginServlet完整实现
基于前面的知识点,实现一个“初始化配置→处理请求→销毁资源”的完整Servlet,解决线程安全问题:
(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 javax.servlet.*;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;
// 配置:访问路径、启动时加载、局部参数
@WebServlet(
urlPatterns = "/login",
loadOnStartup = 2, // 依赖DBInitServlet(loadOnStartup=1),后加载
initParams = {
@WebInitParam(name = "encoding", value = "UTF-8"), // 局部参数:编码
@WebInitParam(name = "maxTry", value = "3") // 局部参数:最大登录尝试次数
}
)
public class LoginServlet extends HttpServlet {
// 1. 初始化资源:数据库配置(从ServletContext获取全局配置)
private Properties dbConfig;
// 2. 线程安全计数器:记录总登录次数(原子类)
private AtomicInteger totalLoginCount = new AtomicInteger(0);
// 3. 局部参数:最大登录尝试次数(从ServletConfig获取)
private int maxTry;
/**
* 初始化:加载资源,仅执行1次
*/
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
try {
// (1)获取ServletContext(全局配置),加载数据库配置文件
ServletContext context = getServletContext();
dbConfig = new Properties();
// 读取WEB-INF/classes下的db.properties(应用级资源)
dbConfig.load(context.getResourceAsStream("/WEB-INF/classes/db.properties"));
// (2)获取ServletConfig(局部配置),初始化最大尝试次数
maxTry = Integer.parseInt(config.getInitParameter("maxTry"));
System.out.println("LoginServlet初始化完成:dbUrl=" + dbConfig.getProperty("db.url") + ", maxTry=" + maxTry);
} catch (IOException e) {
throw new ServletException("初始化失败:加载配置文件出错", e);
}
}
/**
* 处理登录请求:每次请求执行1次
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// (1)设置编码(使用局部参数)
String encoding = getServletConfig().getInitParameter("encoding");
req.setCharacterEncoding(encoding);
resp.setContentType("text/html;charset=" + encoding);
PrintWriter out = resp.getWriter();
// (2)获取请求参数
String username = req.getParameter("username");
String password = req.getParameter("password");
// (3)模拟数据库验证(从全局配置获取管理员账号)
String adminUser = dbConfig.getProperty("admin.username");
String adminPwd = dbConfig.getProperty("admin.password");
boolean loginSuccess = adminUser.equals(username) && adminPwd.equals(password);
// (4)线程安全计数:总登录次数+1
int currentTotal = totalLoginCount.incrementAndGet();
// (5)响应结果
out.println("<h1>" + (loginSuccess ? "登录成功!" : "用户名/密码错误(最多尝试" + maxTry + "次)") + "</h1>");
out.println("<p>当前系统总登录尝试次数:" + currentTotal + "</p>");
out.close();
}
/**
* 销毁:释放资源,仅执行1次
*/
@Override
public void destroy() {
// 清空配置资源,避免内存泄漏
dbConfig.clear();
System.out.println("LoginServlet销毁:释放数据库配置资源,总登录尝试次数=" + totalLoginCount.get());
super.destroy();
}
}
|
(2)配套资源与验证
- db.properties(放在
src/main/resources,编译后在WEB-INF/classes):
8. Servlet 与 Spring Controller 的关系
| 对比项 |
Servlet |
Spring Controller |
| 生命周期 |
由容器控制 |
由 Spring 容器管理 |
| 请求分发 |
由 URL 匹配 |
DispatcherServlet 统一调度 |
| 实例数 |
单例 |
通常也是单例 |
| 功能 |
处理请求 |
封装 + 依赖注入 + 切面支持 |
其实 Spring MVC 最底层的执行者也是一个特殊的 Servlet:DispatcherServlet(继承自 HttpServlet)
9. 小结
| 问题 |
答案 |
| Servlet 是什么? |
运行在服务器上的 Java 类,负责处理 HTTP 请求 |
| 谁调用 Servlet? |
Web 容器(Tomcat) |
| Servlet 生命周期有哪几步? |
加载 → 初始化 → 处理请求 → 销毁 |
| Servlet 何时加载? |
启动时或首次请求时(由 load-on-startup 决定) |
| 为什么 Servlet 是单例? |
容器只创建一个实例来处理多线程请求 |
| 为什么需要生命周期? |
容器必须在合适的时机创建、使用、销毁 Servlet 以管理资源 |
三、HttpServletRequest / HttpServletResponse
1. HttpServletRequest 请求机制
(1)Tomcat对HTTP报文的解析流程
当Tomcat的Connector组件接收到客户端HTTP报文后,会经历以下解析过程映射为HttpServletRequest对象:
- 报文解析:通过
org.apache.catalina.connector.CoyoteAdapter将底层字节流解析为org.apache.coyote.Request(Coyote层对象)。
- 对象转换:将Coyote Request转换为
org.apache.catalina.connector.Request(Servlet层对象,实现HttpServletRequest接口)。
- 数据填充:提取报文的请求行(方法、URL、协议)、请求头(Header)、请求体(Body),分别存入
HttpServletRequest的对应字段。
(2)Request的核心结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public interface HttpServletRequest extends ServletRequest {
// 1. 请求行信息
String getMethod(); // 获取请求方法(GET/POST等)
String getRequestURI(); // 获取请求路径(如/context/login)
String getProtocol(); // 获取协议版本(如HTTP/1.1)
// 2. 请求头信息
String getHeader(String name); // 获取指定头字段(如User-Agent)
Enumeration<String> getHeaders(String name); // 获取多值头字段
// 3. 请求参数(QueryString或Form表单数据)
String getParameter(String name); // 获取参数值
Map<String, String[]> getParameterMap(); // 获取所有参数的键值对
// 4. 请求体(原始字节流)
ServletInputStream getInputStream() throws IOException;
}
|
(3)不同HTTP方法的解析差异
| 方法 |
参数位置 |
解析逻辑 |
长度限制 |
| GET |
URL的QueryString(?name=xxx&age=18) |
直接从请求行解析,存入ParameterMap |
受浏览器/服务器限制(通常2-8KB) |
| POST |
请求体(Body) |
需根据Content-Type解析: - application/x-www-form-urlencoded:与GET参数格式一致,解析后存入ParameterMap - multipart/form-data:用于文件上传,需通过Part接口解析 - application/json:原始JSON字符串,需手动读取getInputStream()解析 |
无默认限制,可通过服务器配置调整 |
| PUT/DELETE |
通常在请求体 |
服务器默认不解析为ParameterMap,需手动读取流解析(如JSON/XML) |
无默认限制 |
2. 请求分派与转发
(1)forward() vs sendRedirect() 底层区别
| 特性 |
RequestDispatcher.forward(request, response) |
response.sendRedirect(url) |
| 本质 |
服务器内部资源跳转(一次请求) |
客户端重定向(两次请求) |
| 实现原理 |
由Servlet容器内部转发请求,目标资源共享同一个request/response对象 |
服务器返回302 Found响应,携带Location头,客户端自动发起第二次请求 |
| URL变化 |
浏览器地址栏URL不变 |
地址栏显示重定向后的URL |
| 数据共享 |
可通过request.setAttribute()共享数据 |
无法共享request属性,需通过URL参数/会话传递 |
| 适用场景 |
同一Web应用内的资源跳转(如Servlet→JSP) |
跨应用跳转或需要改变URL的场景 |
(2)转发中的request属性共享示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// 源Servlet(ForwardServlet)
@WebServlet("/forward")
public class ForwardServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 设置共享属性
req.setAttribute("user", "admin");
// 转发到目标Servlet
RequestDispatcher dispatcher = req.getRequestDispatcher("/target");
dispatcher.forward(req, resp); // 共享同一个request
}
}
// 目标Servlet(TargetServlet)
@WebServlet("/target")
public class TargetServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 获取共享属性
String user = (String) req.getAttribute("user"); // 输出"admin"
resp.getWriter().println("User: " + user);
}
}
|
(3) request属性的生命周期
3. HttpServletResponse 响应机制
(1)响应流与缓冲机制
(2)核心响应头与传输编码
(3)Gzip压缩响应实现(Filter示例)
通过Filter对响应流进行Gzip压缩,核心是包装ServletResponse:
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
|
@WebFilter("/*")
public class GzipFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 包装响应对象,使用Gzip压缩
GzipResponseWrapper gzipResponse = new GzipResponseWrapper((HttpServletResponse) response);
chain.doFilter(request, gzipResponse);
gzipResponse.finish(); // 完成压缩并输出
}
// 自定义响应包装类
static class GzipResponseWrapper extends HttpServletResponseWrapper {
private GZIPOutputStream gzipOut;
private ServletOutputStream out;
public GzipResponseWrapper(HttpServletResponse response) throws IOException {
super(response);
// 设置压缩响应头
response.setHeader("Content-Encoding", "gzip");
gzipOut = new GZIPOutputStream(response.getOutputStream());
out = new ServletOutputStream() {
@Override
public void write(int b) throws IOException {
gzipOut.write(b);
}
@Override
public boolean isReady() { return true; }
@Override
public void setWriteListener(WriteListener listener) {}
};
}
@Override
public ServletOutputStream getOutputStream() {
return out;
}
public void finish() throws IOException {
gzipOut.finish(); // 完成压缩
}
}
}
|
4. 请求体读取的坑与解决方案
(1)InputStream只能读取一次的原因
(2)用RequestWrapper实现重复读取
通过包装HttpServletRequest,将请求体缓存到字节数组中,实现多次读取:
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
|
public class RepeatableRequestWrapper extends HttpServletRequestWrapper {
private byte[] body; // 缓存请求体
public RepeatableRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 读取原始流并缓存
body = IOUtils.toByteArray(request.getInputStream());
}
// 重写getInputStream(),返回缓存的字节流
@Override
public ServletInputStream getInputStream() {
return new ServletInputStream() {
private final ByteArrayInputStream bais = new ByteArrayInputStream(body);
@Override
public int read() {
return bais.read();
}
@Override
public boolean isReady() { return true; }
@Override
public void setWriteListener(WriteListener listener) {}
};
}
// 重写getReader(),基于缓存的字节流构建
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
}
// 在Filter中使用
@WebFilter("/*")
public class RequestLogFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 包装请求,使其可重复读取
RepeatableRequestWrapper wrapper = new RepeatableRequestWrapper((HttpServletRequest) request);
// 第一次读取请求体(日志记录)
String body = IOUtils.toString(wrapper.getInputStream(), wrapper.getCharacterEncoding());
System.out.println("Request Body: " + body);
// 放行,后续组件可再次读取
chain.doFilter(wrapper, response);
}
}
|
5. 实战案例
(1)自定义RequestLoggerFilter(记录请求详情)
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
|
@WebFilter("/*")
public class RequestLoggerFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
long startTime = System.currentTimeMillis();
// 记录请求方法、URL
String log = String.format("Method: %s, URL: %s", req.getMethod(), req.getRequestURI());
// 记录请求参数(GET/POST表单)
if (!req.getMethod().equals("GET")) {
// 使用前面的RepeatableRequestWrapper读取Body
RepeatableRequestWrapper wrapper = new RepeatableRequestWrapper(req);
String body = IOUtils.toString(wrapper.getInputStream(), wrapper.getCharacterEncoding());
log += ", Body: " + body;
request = wrapper; // 替换为包装类
}
// 执行后续处理
chain.doFilter(request, response);
// 记录耗时
long endTime = System.currentTimeMillis();
log += ", Time: " + (endTime - startTime) + "ms";
System.out.println(log);
}
}
|
(2)实现ResponseWrapper(修改响应内容)
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
|
public class ResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream buffer; // 缓存响应数据
private ServletOutputStream out;
public ResponseWrapper(HttpServletResponse response) {
super(response);
buffer = new ByteArrayOutputStream();
out = new ServletOutputStream() {
@Override
public void write(int b) {
buffer.write(b);
}
@Override
public boolean isReady() { return true; }
@Override
public void setWriteListener(WriteListener listener) {}
};
}
@Override
public ServletOutputStream getOutputStream() {
return out;
}
@Override
public PrintWriter getWriter() {
return new PrintWriter(new OutputStreamWriter(buffer, getCharacterEncoding()));
}
// 重写响应内容(如添加统一响应头)
public byte[] getResponseData() throws IOException {
out.flush();
// 示例:在响应体末尾添加版权信息
String original = buffer.toString(getCharacterEncoding());
String modified = original + "\n© 2024 MyApp";
return modified.getBytes(getCharacterEncoding());
}
}
// 在Filter中使用
@WebFilter("/*")
public class ResponseModifyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
ResponseWrapper wrapper = new ResponseWrapper((HttpServletResponse) response);
chain.doFilter(request, wrapper);
// 输出修改后的响应
byte[] data = wrapper.getResponseData();
response.getOutputStream().write(data);
}
}
|
6. 提升思考
(1)底层实现跨域(CORS)支持
通过Filter添加CORS响应头,允许跨域请求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@WebFilter("/*")
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse resp = (HttpServletResponse) response;
// 允许所有源(生产环境需指定具体域名)
resp.setHeader("Access-Control-Allow-Origin", "*");
// 允许的请求方法
resp.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
// 允许的请求头
resp.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization");
// 预检请求有效期(秒)
resp.setHeader("Access-Control-Max-Age", "3600");
chain.doFilter(request, response);
}
}
|
(2)请求体加解密机制实现
结合RequestWrapper和ResponseWrapper,在Filter中对请求体解密、响应体加密:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// 解密请求Filter
@WebFilter("/*")
public class DecryptFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
RepeatableRequestWrapper wrapper = new RepeatableRequestWrapper(req);
// 读取加密的请求体并解密(示例:AES解密)
String encryptedBody = IOUtils.toString(wrapper.getInputStream(), "UTF-8");
String decryptedBody = AESUtils.decrypt(encryptedBody, "secretKey");
// 替换为解密后的字节流
wrapper = new RepeatableRequestWrapper(req, decryptedBody.getBytes("UTF-8"));
chain.doFilter(wrapper, response);
}
}
// 加密响应Filter(类似实现,对ResponseWrapper的输出进行加密)
|
7. 小结
HttpServletRequest封装了HTTP请求的所有信息,需注意请求体只能读取一次的特性,可通过包装类解决。
HttpServletResponse用于构建响应,需关注缓冲机制、编码设置及流的正确使用。
- 转发(forward)是服务器内部跳转,重定向(redirect)是客户端跳转,适用场景不同。
- 实战中可通过Filter+包装类实现请求日志、响应修改、加解密等功能。
四、 过滤器 Filter 与监听器 Listener
1. Filter 链机制
(1)多个 Filter 的执行顺序(@WebFilter vs web.xml)
Filter 链是多个 Filter 按序对请求/响应进行拦截处理的流程,核心是“责任链设计模式”的落地。
| 配置方式 |
排序规则 |
示例 |
web.xml 配置 |
按 <filter-mapping> 标签的上下顺序执行,先配置的先拦截请求 |
<!– 1. 先执行 LogFilter –> <filter> <filter-name>LogFilter</filter-name> <filter-class>com.example.LogFilter</filter-class> </filter> <filter-mapping> <filter-name>LogFilter</filter-name> <url-pattern>/</url-pattern> </filter-mapping>
<!– 2. 后执行 AuthFilter –> <filter> <filter-name>AuthFilter</filter-name> <filter-class>com.example.AuthFilter</filter-class> </filter> <filter-mapping> <filter-name>AuthFilter</filter-name> <url-pattern>/</url-pattern> </filter-mapping>
|
@WebFilter 注解 |
无默认排序规则(依赖容器实现,如 Tomcat 按类名ASCII码排序),需通过 order 属性指定优先级(值越小,执行越靠前) |
// order=1,先执行 @WebFilter(urlPatterns = “/”, order = 1) public class LogFilter implements Filter { … }
// order=2,后执行 @WebFilter(urlPatterns = “/”, order = 2) public class AuthFilter implements Filter { … }
|
关键注意点:若同时使用两种配置方式,web.xml 中配置的 Filter 会优先于注解配置的 Filter 执行。
(2)FilterChain 的实现与责任链设计模式
责任链模式核心逻辑
Filter 链通过 FilterChain 接口实现,其核心是“将请求依次传递给下一个 Filter,直到所有 Filter 执行完毕后,再调用目标 Servlet”,流程如下:
- 容器创建
FilterChain 实例,将当前请求匹配的所有 Filter 按序存入链中;
- 调用第一个 Filter 的
doFilter() 方法,传入 request、response 和 FilterChain;
- 每个 Filter 处理完请求后,调用
filterChain.doFilter() 触发下一个 Filter;
- 最后一个 Filter 调用
doFilter() 时,容器会执行目标 Servlet(如 HelloServlet);
- Servlet 处理完成后,响应会沿“Filter 链反向返回”,每个 Filter 可再次处理响应。
Filter 核心方法与 FilterChain 示例
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
|
// 自定义 LogFilter(记录请求耗时)
@WebFilter(urlPatterns = "/*", order = 1)
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Filter 初始化:仅执行1次(Filter 实例创建时)
System.out.println("LogFilter 初始化");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 1. 处理请求(前置处理:记录开始时间)
long startTime = System.currentTimeMillis();
HttpServletRequest req = (HttpServletRequest) request;
System.out.println("请求URL:" + req.getRequestURI() + ",开始时间:" + startTime);
// 2. 传递请求给下一个 Filter 或 Servlet
chain.doFilter(request, response); // 关键:触发链的下一个节点
// 3. 处理响应(后置处理:计算耗时)
long endTime = System.currentTimeMillis();
System.out.println("请求" + req.getRequestURI() + "耗时:" + (endTime - startTime) + "ms");
}
@Override
public void destroy() {
// Filter 销毁:仅执行1次(容器关闭时)
System.out.println("LogFilter 销毁");
}
}
|
(3)典型应用:日志记录、鉴权、XSS 过滤、性能统计。
| 应用场景 |
核心逻辑 |
代码片段(关键部分) |
| 日志记录 |
前置记录请求信息(URL、方法、参数),后置记录响应状态和耗时 |
// 前置记录请求 System.out.println(“请求方法:” + req.getMethod() + “,参数:” + req.getParameterMap()); // 后置记录响应 System.out.println(“响应状态码:” + ((HttpServletResponse) response).getStatus());
|
| 登录鉴权 |
拦截未登录请求,跳转到登录页;已登录则放行 |
HttpSession session = req.getSession(); if (session.getAttribute(“user”) == null) { // 未登录:重定向到登录页 ((HttpServletResponse) response).sendRedirect("/login"); return; } // 已登录:放行 chain.doFilter(request, response);
|
| XSS 过滤 |
包装请求,过滤请求参数中的 XSS 脚本(如 <script> 标签) |
// 自定义 XssRequestWrapper 过滤参数 XssRequestWrapper xssReq = new XssRequestWrapper(req); chain.doFilter(xssReq, response);
|
| 性能统计 |
前置记录开始时间,后置计算耗时,统计接口性能 |
long start = System.currentTimeMillis(); chain.doFilter(request, response); long cost = System.currentTimeMillis() - start; // 记录耗时到监控系统 MonitorSystem.record(req.getRequestURI(), cost);
|
2. Listener 分类与触发时机
Listener 是“事件监听器”,用于监听 Servlet 容器或组件(Context、Session、Request)的生命周期事件或属性变化事件,核心是“事件驱动”。根据监听对象不同,分为三大类:生命周期监听器、属性监听器、其他监听器(如 HttpSessionBindingListener)。
(1)生命周期监听器(核心三类)
| 监听器接口 |
监听对象 |
核心方法(触发时机) |
代码示例 |
ServletContextListener |
整个 Web 应用(ServletContext) |
- contextInitialized():应用启动时(容器创建 ServletContext 后) - contextDestroyed():应用卸载/容器关闭时(ServletContext 销毁前) |
@WebListener public class AppListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { // 应用启动:初始化全局资源(如连接池) ServletContext context = sce.getServletContext(); context.setAttribute(“appName”, “MyWebApp”); System.out.println(“应用启动,初始化连接池”); }
@Override public void contextDestroyed(ServletContextEvent sce) { // 应用关闭:释放全局资源(如关闭连接池) System.out.println(“应用关闭,释放连接池”); } }
|
HttpSessionListener |
用户会话(HttpSession) |
- sessionCreated():用户首次访问时(容器创建 HttpSession 后,如调用 request.getSession()) - sessionDestroyed():会话超时/手动销毁时(如 session.invalidate()) |
@WebListener public class SessionListener implements HttpSessionListener { private int onlineCount = 0; // 在线人数(需注意线程安全)
@Override public void sessionCreated(HttpSessionEvent se) { // 会话创建:在线人数+1 synchronized (this) { onlineCount++; } se.getSession().getServletContext().setAttribute(“onlineCount”, onlineCount); System.out.println(“新用户上线,当前在线:” + onlineCount); }
@Override public void sessionDestroyed(HttpSessionEvent se) { // 会话销毁:在线人数-1 synchronized (this) { onlineCount–; } se.getSession().getServletContext().setAttribute(“onlineCount”, onlineCount); System.out.println(“用户下线,当前在线:” + onlineCount); } }
|
ServletRequestListener |
单个请求(ServletRequest) |
- requestInitialized():请求到达时(容器创建 ServletRequest 后) - requestDestroyed():请求处理完成后(响应发送给客户端后) |
@WebListener public class RequestListener implements ServletRequestListener { @Override public void requestInitialized(ServletRequestEvent sre) { // 请求初始化:记录请求ID ServletRequest req = sre.getServletRequest(); req.setAttribute(“requestId”, UUID.randomUUID().toString()); System.out.println(“请求创建,ID:” + req.getAttribute(“requestId”)); }
@Override public void requestDestroyed(ServletRequestEvent sre) { // 请求销毁:清理资源 System.out.println(“请求销毁,ID:” + sre.getServletRequest().getAttribute(“requestId”)); } }
|
(2) AttributeListener 族(属性变化监听器)
监听 ServletContext、HttpSession、ServletRequest 中属性的增删改事件(setAttribute、removeAttribute、replaceAttribute),核心方法对应“添加”“移除”“替换”三个事件。
以 ServletContextAttributeListener 为例(HttpSessionAttributeListener、ServletRequestAttributeListener 用法类似):
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
|
@WebListener
public class ContextAttrListener implements ServletContextAttributeListener {
// 属性添加时触发(如 context.setAttribute("key", "value"))
@Override
public void attributeAdded(ServletContextAttributeEvent scae) {
String key = scae.getName();
Object value = scae.getValue();
System.out.println("全局属性添加:" + key + "=" + value);
}
// 属性移除时触发(如 context.removeAttribute("key"))
@Override
public void attributeRemoved(ServletContextAttributeEvent scae) {
System.out.println("全局属性移除:" + scae.getName());
}
// 属性替换时触发(如再次调用 context.setAttribute("key", "newValue"))
@Override
public void attributeReplaced(ServletContextAttributeEvent scae) {
String key = scae.getName();
Object oldValue = scae.getValue(); // 旧值
Object newValue = scae.getServletContext().getAttribute(key); // 新值
System.out.println("全局属性替换:" + key + ",旧值:" + oldValue + ",新值:" + newValue);
}
}
|
(3) 生命周期事件触发顺序
当一个请求从“到达”到“处理完成”,三大组件的生命周期事件触发顺序严格遵循“从大到小”(应用→会话→请求),具体流程如下:
- 应用启动阶段(仅一次):
ServletContextListener.contextInitialized() → 初始化全局资源;
- 用户首次访问(创建会话):
ServletRequestListener.requestInitialized() → 请求创建;
HttpSessionListener.sessionCreated() → 会话创建(若首次访问,触发 getSession());
- 请求处理阶段:Filter 链 → 目标 Servlet → 处理请求;
- 请求销毁阶段:
ServletRequestListener.requestDestroyed() → 请求处理完成,响应发送;
- 会话销毁阶段(超时/手动销毁):
HttpSessionListener.sessionDestroyed() → 会话销毁;
- 应用关闭阶段(仅一次):
ServletContextListener.contextDestroyed() → 释放全局资源。
3. Filter 与 Listener 的协同
Filter 擅长“拦截请求/响应并处理”,Listener 擅长“监听事件并记录状态”,二者协同可实现更复杂的监控或业务逻辑,典型场景为“请求耗时统计 + 在线人数统计”。
协同案例:请求监控体系
需求:统计“当前在线人数”和“每个请求的耗时”,并将耗时记录到全局监控中。
(1) 组件分工
OnlineUserListener(HttpSessionListener):监听会话创建/销毁,统计在线人数;
PerformanceFilter(Filter):拦截所有请求,计算单个请求耗时;
AppListener(ServletContextListener):初始化全局监控容器(如存储耗时的列表)。
(2) 代码实现
AppListener(初始化全局监控)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@WebListener
public class AppListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
// 初始化全局监控容器:存储所有请求的耗时(线程安全的列表)
List<Long> requestCostList = Collections.synchronizedList(new ArrayList<>());
sce.getServletContext().setAttribute("requestCostList", requestCostList);
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
// 应用关闭:输出平均耗时
List<Long> costList = (List<Long>) sce.getServletContext().getAttribute("requestCostList");
if (!costList.isEmpty()) {
long avgCost = costList.stream().mapToLong(Long::longValue).average().getAsDouble();
System.out.println("应用关闭,平均请求耗时:" + avgCost + "ms");
}
}
}
|
OnlineUserListener(统计在线人数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@WebListener
public class OnlineUserListener implements HttpSessionListener {
private AtomicInteger onlineCount = new AtomicInteger(0); // 原子类保证线程安全
@Override
public void sessionCreated(HttpSessionEvent se) {
// 会话创建:在线人数+1
int count = onlineCount.incrementAndGet();
se.getSession().getServletContext().setAttribute("onlineCount", count);
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
// 会话销毁:在线人数-1
int count = onlineCount.decrementAndGet();
se.getSession().getServletContext().setAttribute("onlineCount", count);
}
}
|
PerformanceFilter(统计请求耗时并关联监控)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@WebFilter(urlPatterns = "/*", order = 1)
public class PerformanceFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 1. 前置:记录请求开始时间和当前在线人数
long startTime = System.currentTimeMillis();
ServletContext context = request.getServletContext();
int onlineCount = (int) context.getAttribute("onlineCount");
System.out.println("当前在线人数:" + onlineCount + ",请求开始处理");
// 2. 放行请求
chain.doFilter(request, response);
// 3. 后置:计算耗时并加入全局监控
long cost = System.currentTimeMillis() - startTime;
List<Long> costList = (List<Long>) context.getAttribute("requestCostList");
costList.add(cost);
System.out.println("请求耗时:" + cost + "ms,累计请求数:" + costList.size());
}
}
|
协同逻辑
- 应用启动时,
AppListener 初始化全局监控列表;
- 用户首次访问时,
OnlineUserListener 创建会话并更新在线人数;
- 每个请求到达时,
PerformanceFilter 先获取当前在线人数,再计算请求耗时;
- 请求处理完成后,
PerformanceFilter 将耗时存入全局监控列表;
- 应用关闭时,
AppListener 计算并输出平均请求耗时。
4. 实战案例
编写:
- 一个
AuthFilter 实现登录拦截;
- 一个
OnlineUserListener 统计当前在线人数;
- 一个
PerformanceListener 计算平均请求耗时。
(1) AuthFilter(登录拦截)
需求:拦截未登录用户访问“/user/*”路径,已登录用户放行。
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
|
@WebFilter(urlPatterns = "/user/*", order = 2)
public class AuthFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 1. 排除登录页(避免死循环)
String url = req.getRequestURI();
if (url.contains("/login")) {
chain.doFilter(request, response);
return;
}
// 2. 检查会话中的用户信息
HttpSession session = req.getSession(false); // 无会话则返回null,不创建新会话
if (session == null || session.getAttribute("loginUser") == null) {
// 未登录:重定向到登录页
resp.sendRedirect("/login?redirect=" + url); // 携带原请求URL,登录后跳转
return;
}
// 3. 已登录:放行
chain.doFilter(request, response);
}
}
|
(2) OnlineUserListener(统计在线人数)
需求:实时统计当前在线用户数,支持会话超时和手动登出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@WebListener
public class OnlineUserListener implements HttpSessionListener {
// 原子类:保证多线程下在线人数计数安全
private final AtomicInteger onlineCount = new AtomicInteger(0);
@Override
public void sessionCreated(HttpSessionEvent se) {
// 会话创建:在线人数+1,存入全局上下文
int count = onlineCount.incrementAndGet();
ServletContext context = se.getSession().getServletContext();
context.setAttribute("onlineCount", count);
System.out.println("用户上线,当前在线:" + count);
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
// 会话销毁:在线人数-1,更新全局上下文
int count = onlineCount.decrementAndGet();
ServletContext context = se.getSession().getServletContext();
context.setAttribute("onlineCount", Math.max(count, 0)); // 避免负数
System.out.println("用户下线,当前在线:" + count);
}
}
|
(3) PerformanceListener(计算平均请求耗时)
需求:监听所有请求的创建与销毁,统计每个请求的耗时,并计算全局平均耗时。
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
|
@WebListener
public class PerformanceListener implements ServletRequestListener {
// 全局监控:存储所有请求耗时(线程安全)
private final List<Long> totalCostList = Collections.synchronizedList(new ArrayList<>());
@Override
public void requestInitialized(ServletRequestEvent sre) {
// 请求创建:记录开始时间,存入请求属性
long startTime = System.currentTimeMillis();
sre.getServletRequest().setAttribute("startTime", startTime);
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {
// 请求销毁:计算耗时,更新全局统计
ServletRequest req = sre.getServletRequest();
long startTime = (long) req.getAttribute("startTime");
long cost = System.currentTimeMillis() - startTime;
// 加入全局列表
totalCostList.add(cost);
// 计算并输出平均耗时
double avgCost = totalCostList.stream().mapToLong(Long::longValue).average().orElse(0);
System.out.println("请求" + ((HttpServletRequest) req).getRequestURI() + "耗时:" + cost + "ms,平均耗时:" + String.format("%.2f", avgCost) + "ms");
}
}
|
5. 提升思考
实现可插拔的 Filter 框架
类似 Spring Boot 的 FilterRegistrationBean,可插拔 Filter 框架的核心是“通过代码动态注册 Filter,支持灵活配置 URL 模式、执行顺序、是否启用”,避免硬编码或 XML 配置的耦合。
(1) 设计思路
- 定义“Filter 配置类”:封装 Filter 的核心参数(Filter 实例、URL 模式、order、启用状态);
- 提供“Filter 注册器”:管理所有 Filter 配置,在应用启动时动态注册到容器;
- 支持“动态启用/禁用”:通过配置(如配置文件)控制 Filter 是否生效。
(2) 代码实现
Filter 配置类(FilterConfig)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 封装 Filter 的核心配置参数
public class FilterConfig {
private Filter filter; // Filter 实例
private String[] urlPatterns; // 拦截的 URL 模式
private int order; // 执行顺序
private boolean enabled; // 是否启用
// 构造器、getter、setter
public FilterConfig(Filter filter, String[] urlPatterns, int order, boolean enabled) {
this.filter = filter;
this.urlPatterns = urlPatterns;
this.order = order;
this.enabled = enabled;
}
// getter 和 setter 省略
}
|
Filter 注册器(FilterRegistry)
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
|
// 管理 Filter 配置,动态注册到 Servlet 容器
public class FilterRegistry implements ServletContextListener {
// 存储所有 Filter 配置(线程安全)
private final List<FilterConfig> filterConfigs = Collections.synchronizedList(new ArrayList<>());
// 添加 Filter 配置
public void addFilterConfig(FilterConfig config) {
if (config != null && config.isEnabled()) {
filterConfigs.add(config);
}
}
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext context = sce.getServletContext();
// 1. 按 order 排序 Filter 配置
filterConfigs.sort(Comparator.comparingInt(FilterConfig::getOrder));
// 2. 遍历配置,动态注册 Filter 到容器
for (FilterConfig config : filterConfigs) {
Filter filter = config.getFilter();
String filterName = filter.getClass().getSimpleName();
// 2.1 注册 Filter 实例
context.addFilter(filterName, filter);
// 2.2 配置拦截的 URL 模式
FilterRegistration.Dynamic dynamic = context.getFilterRegistration(filterName);
dynamic.addMappingForUrlPatterns(
EnumSet.of(DispatcherType.REQUEST), // 仅拦截请求
false, // 是否匹配 servletContext 的路径
config.getUrlPatterns() // URL 模式
);
System.out.println("动态注册 Filter:" + filterName + ",URL模式:" + Arrays.toString(config.getUrlPatterns()) + ",order:" + config.getOrder());
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
// 销毁时清理配置
filterConfigs.clear();
}
}
|
使用示例(应用启动时注册 Filter)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// 应用启动类(模拟 Spring Boot 的启动逻辑)
public class AppStarter {
public static void main(String[] args) {
// 1. 创建 Filter 实例
LogFilter logFilter = new LogFilter();
AuthFilter authFilter = new AuthFilter();
// 2. 创建 Filter 注册器
FilterRegistry registry = new FilterRegistry();
// 3. 添加 Filter 配置(可从配置文件读取参数,实现动态配置)
registry.addFilterConfig(new FilterConfig(logFilter, new String[]{"/*"}, 1, true)); // 启用 LogFilter
registry.addFilterConfig(new FilterConfig(authFilter, new String[]{"user/*"}, 2, false)); // 禁用 AuthFilter(后续可动态启用)
// 4. 将注册器注册为 ServletContextListener(应用启动时触发注册)
// 实际 Web 应用中,可通过 web.xml 或注解注册 registry
}
}
|
- 可插拔:通过
enabled 参数控制 Filter 是否生效,无需修改代码;
- 动态配置:URL 模式、执行顺序可从配置文件读取(如
application.properties),避免硬编码;
- 解耦:Filter 实例与注册逻辑分离,便于维护和扩展(如新增 Filter 只需添加配置)。
五、 ServletContext
ServletContext 是 Servlet 规范中定义的全局上下文对象,代表整个 Web 应用,贯穿应用的全生命周期,负责全局数据共享、资源访问和组件通信。以下从核心知识点、实战案例和扩展思考三个维度展开说明。
1. 全局共享数据与生命周期
(1)ServletContext 的初始化与销毁
ServletContext 的生命周期与 Web 应用完全一致,由 Servlet 容器(如 Tomcat)统一管理:
- 初始化:Web 应用启动时(容器加载应用并解析
web.xml 或注解后),容器创建唯一的 ServletContext 实例,所有组件(Servlet/Filter/Listener)共享该实例。
- 销毁:Web 应用卸载(如 Tomcat 关闭、应用被移除)时,容器销毁 ServletContext 实例,释放其占用的资源。
监听初始化与销毁的代码示例(通过 ServletContextListener):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@WebListener
public class AppContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
// 应用启动时,ServletContext 初始化完成后触发
ServletContext context = sce.getServletContext();
System.out.println("ServletContext 初始化完成,应用启动");
// 初始化全局数据(如应用名称、版本)
context.setAttribute("appName", "UserManagementSystem");
context.setAttribute("version", "1.0.0");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
// 应用关闭时,ServletContext 销毁前触发
ServletContext context = sce.getServletContext();
System.out.println("ServletContext 即将销毁,应用关闭");
// 清理全局资源(如移除所有属性)
context.removeAttribute("appName");
context.removeAttribute("version");
}
}
|
(2)全局参数与资源加载
ServletContext 可存储两类全局配置:全局初始化参数(通过配置文件定义)和动态属性(通过代码设置)。
全局初始化参数(web.xml 配置)
全局参数在 web.xml 中通过 <context-param> 标签定义,可被所有组件通过 ServletContext 获取,适合存储静态配置(如数据库连接URL、应用编码)。
配置示例:
1
2
3
4
5
6
7
8
9
|
<!-- web.xml 中定义全局参数 -->
<context-param>
<param-name>db.url</param-name>
<param-value>jdbc:mysql://localhost:3306/user_db</param-value>
</context-param>
<context-param>
<param-name>charset</param-name>
<param-value>UTF-8</param-value>
</context-param>
|
获取示例:
1
2
3
4
5
6
7
|
// 在任意 Servlet/Filter/Listener 中获取
ServletContext context = getServletContext(); // Servlet 中
// 或:Filter 中通过 filterConfig.getServletContext()
// 或:Listener 中通过 sce.getServletContext()
String dbUrl = context.getInitParameter("db.url"); // 结果:jdbc:mysql://localhost:3306/user_db
String charset = context.getInitParameter("charset"); // 结果:UTF-8
|
动态属性(setAttribute 动态设置)
通过 setAttribute(String name, Object value) 动态存储全局共享数据(如在线人数、缓存数据),适合运行时需要动态更新的信息。
使用示例:
1
2
3
4
5
6
7
8
9
10
11
|
// 设置全局属性
ServletContext context = getServletContext();
context.setAttribute("onlineUserCount", 100); // 存储在线人数
context.setAttribute("systemTime", new Date()); // 存储系统时间
// 获取全局属性
int onlineCount = (int) context.getAttribute("onlineUserCount");
Date sysTime = (Date) context.getAttribute("systemTime");
// 移除全局属性
context.removeAttribute("systemTime");
|
(3)Context attribute 的线程安全问题
ServletContext 的属性(attribute)是全局共享的,多线程(如并发请求)同时读写时会存在线程安全问题,需通过同步机制保证安全。
问题示例:多线程并发修改在线人数
1
2
3
4
|
// 非线程安全的操作
ServletContext context = getServletContext();
int count = (int) context.getAttribute("onlineUserCount");
context.setAttribute("onlineUserCount", count + 1); // 可能导致计数错误
|
解决方案:
2. 资源访问
ServletContext 提供了访问 Web 应用内资源(如配置文件、静态资源)的能力,核心是通过虚拟路径映射到真实磁盘路径,或直接读取资源流。
(1)getResourceAsStream() 的内部机制
getResourceAsStream(String path) 用于读取 Web 应用内的资源文件(如 WEB-INF/classes/config.properties),返回输入流 InputStream。
内部原理
- 接收虚拟路径(以
/ 开头,相对于 Web 应用根目录),如 /WEB-INF/classes/db.properties;
- 容器将虚拟路径映射到应用在磁盘上的真实路径(如
tomcat/webapps/yourapp/WEB-INF/classes/db.properties);
- 打开文件输入流并返回,若资源不存在则返回
null。
使用示例:读取配置文件
1
2
3
4
5
6
7
8
9
10
11
12
|
// 读取 src/main/resources 下的 config.properties(编译后位于 WEB-INF/classes)
ServletContext context = getServletContext();
try (InputStream is = context.getResourceAsStream("/WEB-INF/classes/config.properties")) {
if (is != null) {
Properties props = new Properties();
props.load(is);
String appName = props.getProperty("app.name"); // 读取配置项
System.out.println("应用名称:" + appName);
}
} catch (IOException e) {
e.printStackTrace();
}
|
(2)Web 应用根路径与真实磁盘路径映射
ServletContext 提供 getRealPath(String path) 方法,将虚拟路径转换为服务器上的真实磁盘路径,便于操作文件系统(如上传文件保存)。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
ServletContext context = getServletContext();
// 1. 获取 Web 应用根目录的真实路径
String rootPath = context.getRealPath("/");
// 结果示例:D:/apache-tomcat-9.0/webapps/yourapp/
// 2. 获取静态资源的真实路径(如 img/logo.png)
String logoPath = context.getRealPath("/img/logo.png");
// 结果示例:D:/apache-tomcat-9.0/webapps/yourapp/img/logo.png
// 3. 获取 WEB-INF 目录下文件的真实路径
String webInfPath = context.getRealPath("/WEB-INF/web.xml");
// 结果示例:D:/apache-tomcat-9.0/webapps/yourapp/WEB-INF/web.xml
|
注意:
- 若应用以 WAR 包形式运行(未解压),
getRealPath() 可能返回 null(部分容器不支持),推荐优先使用 getResourceAsStream() 读取资源。
- 虚拟路径必须以
/ 开头,否则会抛出 IllegalArgumentException。
3. 应用内通信机制
ServletContext 是 Web 应用内所有组件(Servlet、Filter、Listener)的“共享容器”,通过它可以实现不同组件之间的通信。
(1)不同 Servlet 之间通过 Context 共享数据
多个 Servlet 可通过 ServletContext 的 setAttribute 和 getAttribute 交换数据,无需直接耦合。
示例:ServletA 存储数据,ServletB 读取数据
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
|
// ServletA:存储用户登录信息到全局上下文
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String username = req.getParameter("username");
// 登录成功后,将用户名存入全局上下文(实际场景更适合用 Session)
ServletContext context = getServletContext();
context.setAttribute("currentLoginUser", username);
resp.getWriter().println("登录成功,用户名已存入全局上下文");
}
}
// ServletB:读取全局上下文的登录用户
@WebServlet("/userinfo")
public class UserInfoServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
ServletContext context = getServletContext();
String currentUser = (String) context.getAttribute("currentLoginUser");
if (currentUser != null) {
resp.getWriter().println("当前登录用户:" + currentUser);
} else {
resp.getWriter().println("未检测到登录用户");
}
}
}
|
(2)ContextListener 中的初始化逻辑注入
ServletContextListener 在应用启动时初始化全局资源(如连接池、缓存),并将资源存入 ServletContext,供后续组件直接使用,实现“初始化逻辑与业务逻辑解耦”。
示例:启动时初始化数据库连接池并注入全局上下文
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
|
// 1. 监听器:初始化连接池
@WebListener
public class DBInitListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext context = sce.getServletContext();
// 从全局参数获取数据库配置
String dbUrl = context.getInitParameter("db.url");
String username = context.getInitParameter("db.username");
String password = context.getInitParameter("db.password");
// 初始化连接池
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(dbUrl);
dataSource.setUsername(username);
dataSource.setPassword(password);
// 将连接池存入全局上下文
context.setAttribute("dataSource", dataSource);
System.out.println("数据库连接池初始化完成,已注入 ServletContext");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
// 应用关闭时销毁连接池
ServletContext context = sce.getServletContext();
HikariDataSource dataSource = (HikariDataSource) context.getAttribute("dataSource");
if (dataSource != null) {
dataSource.close();
System.out.println("数据库连接池已关闭");
}
}
}
// 2. Servlet:从全局上下文获取连接池并使用
@WebServlet("/userlist")
public class UserListServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 从全局上下文获取连接池
ServletContext context = getServletContext();
HikariDataSource dataSource = (HikariDataSource) context.getAttribute("dataSource");
// 查询数据库(简化示例)
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT * FROM user");
ResultSet rs = ps.executeQuery()) {
// 处理结果...
resp.getWriter().println("查询到用户数据");
} catch (SQLException e) {
e.printStackTrace();
}
}
}
|
4. 实战案例
(1)实现全局配置中心
需求:通过 ServletContext 存储应用所有配置(包括静态参数和动态参数),提供统一的配置访问接口。
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
|
// 1. 配置中心工具类(封装 ServletContext 操作)
public class ConfigCenter {
private static ServletContext context;
// 初始化:在 Listener 中调用,传入 ServletContext
public static void init(ServletContext servletContext) {
context = servletContext;
}
// 获取静态配置(从 web.xml 的 context-param 读取)
public static String getStaticConfig(String key) {
return context.getInitParameter(key);
}
// 获取动态配置(从 ServletContext attribute 读取)
public static Object getDynamicConfig(String key) {
return context.getAttribute(key);
}
// 设置动态配置
public static void setDynamicConfig(String key, Object value) {
context.setAttribute(key, value);
}
}
// 2. 监听器:初始化配置中心
@WebListener
public class ConfigInitListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
// 初始化配置中心
ConfigCenter.init(sce.getServletContext());
// 加载动态配置(如从数据库/配置文件读取)
ConfigCenter.setDynamicConfig("maxUploadSize", 1024 * 1024 * 10); // 10MB
ConfigCenter.setDynamicConfig("cacheEnable", true);
}
}
// 3. 使用示例(在 Servlet 中)
@WebServlet("/config")
public class ConfigServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 读取静态配置
String dbUrl = ConfigCenter.getStaticConfig("db.url");
// 读取动态配置
int maxUpload = (int) ConfigCenter.getDynamicConfig("maxUploadSize");
boolean cacheEnable = (boolean) ConfigCenter.getDynamicConfig("cacheEnable");
resp.getWriter().println("数据库地址:" + dbUrl + "<br>");
resp.getWriter().println("最大上传大小:" + maxUpload + "字节<br>");
resp.getWriter().println("缓存是否启用:" + cacheEnable);
}
}
|
(2)结合 Listener 加载配置文件
需求:应用启动时自动加载 classpath 下的多个配置文件(如 app.properties、db.properties),合并后存入 ServletContext。
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
|
@WebListener
public class MultiConfigLoaderListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext context = sce.getServletContext();
Properties allProps = new Properties();
// 定义需要加载的配置文件路径(相对于 WEB-INF/classes)
String[] configFiles = {
"/WEB-INF/classes/app.properties",
"/WEB-INF/classes/db.properties"
};
// 逐个加载并合并配置
for (String filePath : configFiles) {
try (InputStream is = context.getResourceAsStream(filePath)) {
if (is != null) {
Properties props = new Properties();
props.load(is);
allProps.putAll(props); // 合并配置
System.out.println("已加载配置文件:" + filePath);
} else {
System.err.println("配置文件不存在:" + filePath);
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 将合并后的配置存入 ServletContext
context.setAttribute("allConfigs", allProps);
}
}
// 使用合并后的配置
@WebServlet("/allconfigs")
public class AllConfigServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
Properties allConfigs = (Properties) getServletContext().getAttribute("allConfigs");
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().println("所有配置:<br>");
allConfigs.forEach((k, v) -> {
try {
resp.getWriter().println(k + "=" + v + "<br>");
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
|
5. 提升思考
分布式多节点共享 ServletContext 数据。
ServletContext 是单节点内的全局对象,在分布式系统(多服务器节点)中,各节点的 ServletContext 相互独立,无法直接共享数据。要实现分布式共享,需借助外部中间件,设计思路如下:
(1)核心方案:引入分布式存储中间件
基于 Redis 的分布式共享
- 原理:将原本存储在 ServletContext 中的数据迁移到 Redis(分布式缓存),所有节点通过 Redis 读写数据,保证一致性。
- 实现步骤:
- 封装 Redis 操作工具类(如
RedisUtil),提供 set/get/delete 方法;
- 用 Redis 替代 ServletContext 的
attribute 存储全局数据;
- 节点启动时从 Redis 加载初始化数据,运行时读写操作通过 Redis 完成。
代码示例(伪代码):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// 分布式配置中心(基于 Redis)
public class DistributedConfigCenter {
private static RedisUtil redisUtil = new RedisUtil(); // 封装 Redis 操作
// 设置全局数据(存入 Redis)
public static void set(String key, Object value) {
redisUtil.set(key, value, 3600); // 过期时间按需设置
}
// 获取全局数据(从 Redis 读取)
public static Object get(String key) {
return redisUtil.get(key);
}
}
// 使用示例:替代 ServletContext attribute
// 原代码:context.setAttribute("onlineCount", 100);
// 分布式代码:DistributedConfigCenter.set("onlineCount", 100);
|
基于 ZooKeeper 的配置同步
- 原理:ZooKeeper 提供分布式协调能力,适合存储需要动态更新且实时同步的配置(如系统开关、限流阈值)。
- 优势:支持配置变更通知(Watcher 机制),当一个节点修改配置时,其他节点可实时感知并更新。
适用场景:需要强一致性和实时同步的全局配置(如分布式锁、服务发现)。
(2)数据同步策略
- 读写分离:频繁读取的数据(如静态配置)可缓存到本地 ServletContext,定期从中间件更新;写入时直接操作中间件。
- 冲突处理:多节点并发修改数据时,通过 Redis 的
SETNX(分布式锁)或 ZooKeeper 的临时节点保证原子性。
- 容错机制:中间件不可用时,降级使用本地 ServletContext 数据,避免服务中断。
6. 小结
(1)ServletContext 本质:Web 应用的全局上下文,单应用内唯一,生命周期与应用一致。
(2)核心功能:
- 存储全局参数(静态配置)和动态属性(运行时数据);
- 提供资源访问能力(
getResourceAsStream、getRealPath);
- 实现应用内组件通信(跨 Servlet/Filter 共享数据)。
(3)线程安全:全局属性需通过原子类或同步机制保证多线程安全。
(4)分布式扩展:单节点 ServletContext 无法跨节点共享,需借助 Redis、ZooKeeper 等中间件实现分布式数据同步。
六、 异步 Servlet
异步 Servlet 是 Servlet 3.0 引入的重要特性,通过非阻塞 I/O 模型解决同步处理中线程长时间阻塞的问题,显著提升高并发场景下的系统吞吐量。以下从机制原理、线程模型、应用场景到实战案例展开说明。
1. Servlet 3.0 异步处理机制
1. AsyncContext 的工作原理
异步 Servlet 的核心是 AsyncContext 对象,它将请求处理从“主线程阻塞等待”转为“异步线程处理 + 回调响应”,流程如下:
- 开启异步:在 Servlet 中调用
request.startAsync() 标记请求进入异步模式,返回 AsyncContext 实例,此时容器主线程可释放回线程池。
- 异步处理:通过
AsyncContext 启动异步线程(或提交到自定义线程池)处理耗时操作(如数据库查询、远程调用)。
- 完成响应:异步处理完成后,调用
asyncContext.complete() 或 asyncContext.dispatch() 触发响应发送,容器负责将结果返回给客户端。
核心代码示例:
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
|
@WebServlet(urlPatterns = "/async", asyncSupported = true) // 必须开启异步支持
public class AsyncDemoServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 1. 开启异步模式,获取 AsyncContext
AsyncContext asyncContext = req.startAsync();
// 设置超时时间(毫秒),超时后触发 onTimeout 事件
asyncContext.setTimeout(10000);
// 2. 注册异步监听器(可选,监听生命周期事件)
asyncContext.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) throws IOException {
System.out.println("异步处理完成");
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
System.out.println("异步处理超时");
event.getAsyncContext().getResponse().getWriter().write("Timeout!");
event.getAsyncContext().complete(); // 超时后手动完成
}
@Override
public void onError(AsyncEvent event) throws IOException {
System.out.println("异步处理出错:" + event.getThrowable());
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
System.out.println("异步处理开始");
}
});
// 3. 启动异步处理(使用容器线程池或自定义线程池)
asyncContext.start(() -> {
try {
// 模拟耗时操作(如数据库查询、远程调用)
Thread.sleep(3000);
// 4. 异步处理完成,写入响应
resp.getWriter().write("Async processing done!");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 5. 标记异步处理完成,触发响应发送
asyncContext.complete();
}
});
// 此时容器主线程已释放,无需等待异步处理完成
System.out.println("主线程已释放,可处理其他请求");
}
}
|
2. 非阻塞 I/O 模型(NIO Connector 支持)
异步 Servlet 依赖容器的 NIO 连接器(如 Tomcat 的 NioConnector)实现非阻塞 I/O,核心差异如下:
| 模型 |
线程行为 |
适用场景 |
容器配置 |
| 同步阻塞(BIO) |
主线程阻塞等待 I/O 完成(如数据库响应) |
短请求、低并发 |
Tomcat 默认 BIO 连接器 |
| 异步非阻塞(NIO) |
主线程释放后处理其他请求,I/O 完成后回调 |
长请求、高并发(如长轮询) |
Tomcat 配置 NioConnector |
Tomcat 启用 NIO 连接器(conf/server.xml):
1
2
3
|
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="20000"
redirectPort="8443"/>
|
原理:NIO 连接器通过“事件驱动”模式管理连接,一个线程可处理多个连接的 I/O 事件(读就绪、写就绪),避免同步模型中“一个连接占用一个线程”的资源浪费。
3. 超时机制与异常处理
超时与异常处理示例(基于上面的监听器代码):
- 当异步处理耗时超过 10 秒,
onTimeout 被调用,返回 “Timeout!” 给客户端。
- 若异步线程中发生异常(如
NullPointerException),onError 被调用,打印异常信息。
2. 异步线程模型
1. AsyncContext.start() 与容器线程池
自定义线程池示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// 初始化自定义线程池(建议在 ServletContextListener 中创建)
private static final ExecutorService asyncExecutor = Executors.newFixedThreadPool(10);
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
AsyncContext asyncContext = req.startAsync();
// 使用自定义线程池执行异步任务
asyncExecutor.submit(() -> {
try {
// 耗时操作
Thread.sleep(2000);
resp.getWriter().write("Custom thread pool processing done!");
} catch (Exception e) {
e.printStackTrace();
} finally {
asyncContext.complete();
}
});
}
|
2. 容器如何避免线程占用等待 I/O
同步处理中,容器线程会一直阻塞到 I/O 操作完成(如等待数据库返回结果),导致线程资源浪费。异步处理通过以下方式避免阻塞:
- 主线程在开启异步后立即释放,回到线程池处理新请求;
- I/O 操作(如数据库查询)在异步线程中执行,此时线程处于“运行态”而非“阻塞态”;
- 若 I/O 操作本身支持非阻塞(如 NIO 数据库驱动),异步线程可在等待期间处理其他任务,进一步提升效率。
对比示意图:
- 同步:
线程 -> 处理请求 -> 阻塞等待 I/O -> 返回响应(线程全程占用)
- 异步:
线程1 -> 开启异步 -> 释放;线程2 -> 处理 I/O -> 返回响应(线程1不阻塞)
3. 与 CompletableFuture 的集成
CompletableFuture 提供更灵活的异步编程模型,可与异步 Servlet 结合实现复杂的异步流程(如多任务并行、结果聚合)。
示例:使用 CompletableFuture 处理多个异步任务
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
|
@WebServlet(urlPatterns = "/async-future", asyncSupported = true)
public class AsyncFutureServlet extends HttpServlet {
private ExecutorService executor = Executors.newFixedThreadPool(5);
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
AsyncContext asyncContext = req.startAsync();
resp.setContentType("text/html;charset=UTF-8");
// 任务1:查询用户信息
CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
return "User: admin";
}, executor);
// 任务2:查询订单信息
CompletableFuture<String> orderFuture = CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(1500); } catch (InterruptedException e) { e.printStackTrace(); }
return "Order: 12345";
}, executor);
// 聚合结果并响应
CompletableFuture.allOf(userFuture, orderFuture).thenRun(() -> {
try {
String result = userFuture.get() + "<br>" + orderFuture.get();
resp.getWriter().write(result);
} catch (Exception e) {
e.printStackTrace();
} finally {
asyncContext.complete();
}
});
}
}
|
3. 应用场景
异步 Servlet 适用于处理时间长、I/O 密集型的场景,核心优势是减少线程阻塞,提升系统并发能力。
| 场景 |
同步处理的问题 |
异步处理的优势 |
| 长轮询(如消息通知) |
线程长期阻塞等待新消息,资源浪费 |
线程释放后可处理其他请求,消息到达后回调 |
| 文件上传/下载 |
大文件传输时线程长时间阻塞 |
非阻塞 I/O 分块处理,线程利用率提升 |
| 大数据处理 |
数据计算耗时久,线程被长时间占用 |
计算任务交给异步线程,主线程快速释放 |
| 消息推送 |
需维持长连接,同步模型线程耗尽 |
少量线程即可支撑大量长连接 |
长轮询示例:客户端发起请求后,服务器hold住连接直到有新消息或超时
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
|
@WebServlet(urlPatterns = "/long-polling", asyncSupported = true)
public class LongPollingServlet extends HttpServlet {
// 存储等待中的异步上下文(实际应使用线程安全集合)
private List<AsyncContext> waitingContexts = new CopyOnWriteArrayList<>();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
AsyncContext asyncContext = req.startAsync();
asyncContext.setTimeout(30000); // 30秒超时
// 注册超时监听器:超时后返回空消息
asyncContext.addListener(new AsyncListener() {
@Override
public void onTimeout(AsyncEvent event) {
try {
event.getAsyncContext().getResponse().getWriter().write("No new message");
} catch (IOException e) {
e.printStackTrace();
}
event.getAsyncContext().complete();
waitingContexts.remove(asyncContext);
}
// 其他监听器方法省略...
});
// 将异步上下文加入等待列表
waitingContexts.add(asyncContext);
System.out.println("客户端加入长轮询,当前等待数:" + waitingContexts.size());
}
// 模拟有新消息时通知所有等待的客户端(实际由其他事件触发,如消息队列)
public void onNewMessage(String message) {
for (AsyncContext ctx : waitingContexts) {
try {
ctx.getResponse().getWriter().write("New message: " + message);
ctx.complete(); // 发送响应并结束异步
} catch (IOException e) {
e.printStackTrace();
}
}
waitingContexts.clear(); // 清空等待列表
}
}
|
4. 实战案例
1. 异步 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
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
|
// 1. 消息推送 Servlet
@WebServlet(urlPatterns = "/message-push", asyncSupported = true)
public class MessagePushServlet extends HttpServlet {
// 线程安全的集合存储等待的异步上下文
private final List<AsyncContext> listeners = new CopyOnWriteArrayList<>();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// 客户端请求等待消息
resp.setContentType("text/event-stream;charset=UTF-8"); // SSE 格式
AsyncContext asyncContext = req.startAsync();
asyncContext.setTimeout(60000); // 1分钟超时
// 超时处理:移除上下文
asyncContext.addListener(new AsyncListener() {
@Override
public void onTimeout(AsyncEvent event) {
listeners.remove(asyncContext);
try {
event.getAsyncContext().getResponse().getWriter().write("data: timeout\n\n");
event.getAsyncContext().complete();
} catch (IOException e) {
e.printStackTrace();
}
}
// 其他监听器方法省略...
});
listeners.add(asyncContext);
System.out.println("客户端已连接,当前连接数:" + listeners.size());
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 服务端接收消息并推送给所有客户端
String message = req.getParameter("message");
if (message != null && !message.isEmpty()) {
for (AsyncContext ctx : listeners) {
try {
// 以 SSE 格式写入消息(data: 前缀 + 换行)
ctx.getResponse().getWriter().write("data: " + message + "\n\n");
ctx.getResponse().getWriter().flush(); // 立即推送
} catch (IOException e) {
listeners.remove(ctx); // 移除失效连接
}
}
resp.getWriter().write("消息推送成功,接收人数:" + listeners.size());
}
}
}
// 2. 前端页面(简单示例)
<!DOCTYPE html>
<html>
<body>
<div id="messages"></div>
<script>
// 建立长连接等待消息
const eventSource = new EventSource('/message-push');
eventSource.onmessage = function(event) {
document.getElementById('messages').innerHTML += event.data + '<br>';
};
</script>
</body>
</html>
|
2. 同步与异步请求性能对比
通过压测工具(如 JMeter)对比同步和异步 Servlet 的性能差异,关键指标如下:
| 指标 |
同步 Servlet(BIO) |
异步 Servlet(NIO) |
结论 |
| 并发数 |
低(线程易耗尽) |
高(线程利用率高) |
异步支持更高并发 |
| 响应时间 |
稳定(无线程切换) |
略长(线程切换开销) |
短请求同步更优,长请求异步更优 |
| 资源占用 |
高(线程多) |
低(线程少) |
异步更节省服务器资源 |
压测场景:模拟 1000 个并发请求,每个请求处理耗时 1 秒,结果显示异步 Servlet 的吞吐量是同步的 5-10 倍(取决于线程池配置)。
5. 提升思考
WebSocket 提供全双工通信能力,而异步 Servlet 可作为基础实现类似功能(通过长轮询或 HTTP 流),核心思路如下:
-
基于长轮询的模拟:
- 客户端发送异步请求,服务器 hold 住连接直到有数据或超时;
- 数据推送后,客户端立即发起新的异步请求,维持“准实时”通信;
- 缺点:存在一定延迟(取决于超时时间),每次通信需重新建立连接。
-
基于 HTTP 流的模拟:
- 服务器通过
AsyncContext 保持响应流打开,持续向客户端写入数据(如 SSE 格式);
- 客户端通过
EventSource 接收流数据,实现单向持续推送;
- 如需客户端向服务器发送数据,需额外发起 POST 请求。
-
与 WebSocket 的差异:
- WebSocket 是持久连接,全双工,开销低;
- 基于异步 Servlet 的模拟依赖 HTTP,半双工(或通过双连接实现全双工),开销较高;
- 适用场景:浏览器不支持 WebSocket 时的降级方案。
扩展实现关键点:
- 使用线程安全的集合管理所有活跃的
AsyncContext;
- 实现消息广播机制(如上面的
onNewMessage 方法);
- 处理连接超时和异常关闭,避免资源泄漏。
六. Session 管理机制
Session 是 Web 应用中用于跟踪用户状态的核心机制,通过在服务器端存储用户会话信息,解决 HTTP 无状态协议的局限。以下从 Session 的创建原理、同步安全、安全防护到实战案例展开说明。
1. Session 的创建与标识
1. JSESSIONID Cookie 与 URL Rewriting
Session 的核心是“服务器端存储会话数据 + 客户端携带会话标识”,客户端通过两种方式传递会话标识:
1)JSESSIONID Cookie(默认方式)
2)URL Rewriting(Cookie 禁用时的降级方案)
2. Session 的持久化与钝化
Session 默认存储在服务器内存中,为避免服务器重启或内存溢出导致数据丢失,需通过持久化(Persistence)或钝化(Passivation)机制持久化数据。
1)钝化与活化(Tomcat 等容器支持)
- 钝化:当 Session 长时间未使用或服务器内存不足时,容器将 Session 数据序列化到磁盘(如 Tomcat 的
work/Catalina/ 目录)。
- 活化:当 Session 被再次访问时,容器从磁盘反序列化数据到内存。
- 要求:Session 中存储的对象必须实现
Serializable 接口,否则无法序列化。
示例:Session 存储可序列化对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 实现 Serializable 接口的用户类
public class User implements Serializable {
private String username;
private int age;
// 构造器、getter、setter...
}
// 存储到 Session
@WebServlet("/save-user")
public class SaveUserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
HttpSession session = req.getSession();
session.setAttribute("user", new User("admin", 20)); // 可被钝化/活化
}
}
|
2)持久化到数据库
通过自定义代码将 Session 数据存储到数据库(如 MySQL),适合需要长期保存的会话信息,实现方式:
- 监听 Session 生命周期事件(
HttpSessionListener),在 Session 创建/销毁时同步数据到数据库;
- 定期将内存中的 Session 数据持久化到数据库。
3. 超时与销毁机制
Session 不会永久存在,当满足以下条件时会被销毁:
1)超时销毁(最常见)
2)手动销毁
通过 session.invalidate() 主动销毁当前 Session(如用户登出):
1
2
3
4
5
6
7
8
9
10
11
|
@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
HttpSession session = req.getSession(false); // 不创建新 Session
if (session != null) {
session.invalidate(); // 销毁 Session
}
resp.sendRedirect("/login");
}
}
|
3)服务器关闭
服务器正常关闭时,会将 Session 钝化到磁盘;强制关闭时,内存中的 Session 数据会丢失(未钝化的部分)。
2. Session 同步与并发安全
1. 并发访问 Session 属性时的线程安全
同一用户的多个并发请求(如多标签页操作)会共享同一个 Session,并发修改 Session 属性可能导致数据不一致。
1)问题示例
1
2
3
4
5
6
7
8
9
10
11
12
|
@WebServlet("/counter")
public class CounterServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
HttpSession session = req.getSession();
Integer count = (Integer) session.getAttribute("count");
if (count == null) count = 0;
count++; // 并发时可能导致计数错误
session.setAttribute("count", count);
resp.getWriter().println("Count: " + count);
}
}
|
- 问题:两个并发请求同时读取
count=1,各自加 1 后都写入 2,实际应是 3。
2)解决方案
2. 分布式环境下 Session 共享
在多服务器节点的分布式架构中,默认的内存 Session 无法跨节点共享(节点 A 创建的 Session 在节点 B 中不可见),需通过以下方案解决:
1)Sticky Session(粘性会话)
2)Redis Session(推荐)
简化示例(核心逻辑):
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
|
// Redis Session 管理器(简化版)
public class RedisSessionManager {
private JedisPool jedisPool = new JedisPool("localhost", 6379);
// 从 Redis 获取 Session 数据
public Map<String, Object> getSession(String sessionId) {
try (Jedis jedis = jedisPool.getResource()) {
String json = jedis.get("session:" + sessionId);
return json != null ? JSON.parseObject(json, Map.class) : new HashMap<>();
}
}
// 保存 Session 数据到 Redis
public void saveSession(String sessionId, Map<String, Object> data, int timeout) {
try (Jedis jedis = jedisPool.getResource()) {
String json = JSON.toJSONString(data);
jedis.setex("session:" + sessionId, timeout, json); // 设置超时时间
}
}
}
// 过滤器替换默认 Session
@WebFilter("/*")
public class RedisSessionFilter implements Filter {
private RedisSessionManager sessionManager = new RedisSessionManager();
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 1. 获取或生成 sessionId
String sessionId = req.getCookieValue("JSESSIONID");
if (sessionId == null) {
sessionId = UUID.randomUUID().toString();
resp.addCookie(new Cookie("JSESSIONID", sessionId));
}
// 2. 从 Redis 加载 Session 数据
Map<String, Object> sessionData = sessionManager.getSession(sessionId);
// 3. 包装 request,替换 getSession() 方法
HttpServletRequest wrappedReq = new HttpServletRequestWrapper(req) {
@Override
public HttpSession getSession(boolean create) {
return new RedisHttpSession(sessionId, sessionData, sessionManager);
}
// 其他方法省略...
};
chain.doFilter(wrappedReq, response);
// 4. 保存 Session 数据到 Redis
sessionManager.saveSession(sessionId, sessionData, 3600); // 1小时超时
}
}
|
3. 安全性问题
1. 会话固定攻击(Session Fixation)
2. 会话劫持(Session Hijacking)
3. Session Token + CSRF 防护
跨站请求伪造(CSRF)攻击中,攻击者诱导用户在已登录状态下访问恶意链接,利用用户的 Session 身份执行操作。结合 Session Token 防护:
4. 实战建议
1. 登录 + Session 登录态校验
实现完整的登录流程,包括 Session 存储登录状态、拦截未登录请求。
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
|
// 1. 登录 Servlet
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
req.setCharacterEncoding("UTF-8");
String username = req.getParameter("username");
String password = req.getParameter("password");
// 模拟数据库验证(实际应查询数据库)
if ("admin".equals(username) && "123456".equals(password)) {
// 登录成功:重置 Session ID 防固定攻击,存储用户信息
HttpSession session = req.getSession();
session.invalidate();
session = req.getSession();
session.setAttribute("loginUser", username);
session.setMaxInactiveInterval(1800); // 30分钟超时
resp.sendRedirect("/home");
} else {
resp.sendRedirect("/login.html?error=1");
}
}
}
// 2. 登录态拦截 Filter
@WebFilter({"/home", "/user/*"}) // 拦截需要登录的路径
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 检查 Session 中的登录状态
HttpSession session = req.getSession(false);
if (session == null || session.getAttribute("loginUser") == null) {
// 未登录:重定向到登录页
resp.sendRedirect("/login.html?redirect=" + req.getRequestURI());
return;
}
// 已登录:放行
chain.doFilter(request, response);
}
}
// 3. 首页 Servlet(需登录)
@WebServlet("/home")
public class HomeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String user = (String) req.getSession().getAttribute("loginUser");
resp.getWriter().println("欢迎 " + user + " 登录!<br><a href='/logout'>退出</a>");
}
}
|
2. Session 监听器统计活跃会话
通过 HttpSessionListener 监听 Session 创建/销毁,实时统计活跃会话数。
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
|
@WebListener
public class ActiveSessionListener implements HttpSessionListener {
// 原子类统计活跃会话数(线程安全)
private final AtomicInteger activeSessions = new AtomicInteger(0);
@Override
public void sessionCreated(HttpSessionEvent se) {
// 会话创建:计数 +1
int count = activeSessions.incrementAndGet();
System.out.println("新会话创建,活跃会话数:" + count);
// 存入 ServletContext,供前端展示
se.getSession().getServletContext().setAttribute("activeSessions", count);
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
// 会话销毁:计数 -1
int count = activeSessions.decrementAndGet();
System.out.println("会话销毁,活跃会话数:" + count);
se.getSession().getServletContext().setAttribute("activeSessions", count);
}
}
// 展示活跃会话数的 Servlet
@WebServlet("/active-sessions")
public class ActiveSessionsServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
int count = (int) req.getServletContext().getAttribute("activeSessions");
resp.getWriter().println("当前活跃会话数:" + count);
}
}
|
3. Redis 实现分布式 Session 共享
基于 Spring Session 简化分布式 Session 配置(实际项目常用方案):
-
引入依赖(Maven):
1
2
3
4
5
6
7
8
9
10
|
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.8.0</version>
</dependency>
|
-
配置 Redis 连接(application.properties):
1
2
3
4
|
spring.redis.host=localhost
spring.redis.port=6379
spring.session.store-type=redis
server.servlet.session.timeout=3600s
|
-
使用方式:与原生 Session 完全兼容,自动存储到 Redis
1
2
3
4
5
6
7
8
9
|
@WebServlet("/distributed-session")
public class DistributedSessionServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
HttpSession session = req.getSession();
session.setAttribute("distributedKey", "跨节点共享的值");
resp.getWriter().println("Session 已存储到 Redis:" + session.getAttribute("distributedKey"));
}
}
|
5. 提升思考
如何将 Session 与 JWT 混合使用以兼容分布式架构?JWT(JSON Web Token)是无状态的身份认证令牌,适合分布式架构,但缺乏 Session 的灵活性(如主动失效)。混合使用可兼顾两者优势:
1. 混合方案设计
-
登录流程:
- 用户登录成功后,服务器生成 JWT(包含用户 ID、过期时间等),同时创建 Session 存储用户详细信息(如权限、角色);
- 客户端存储 JWT(如
localStorage 或 Cookie),每次请求携带 JWT。
-
请求处理:
- 服务器验证 JWT 有效性,解析出用户 ID;
- 通过用户 ID 从分布式缓存(如 Redis)获取对应的 Session 数据(若 Session 不存在或失效,拒绝请求);
- 支持通过 Session 主动使 JWT 失效(如登出时删除 Redis 中的 Session)。
2. 核心优势
-
分布式兼容:JWT 无状态,避免 Session 共享难题;
-
灵活控制:通过 Session 实现 JWT 主动失效(弥补 JWT 无法撤回的缺陷);
-
安全性:JWT 签名防篡改,Session 存储敏感信息(避免 JWT 过大)。
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
61
62
63
64
|
// 登录时生成 JWT 和 Session
@WebServlet("/jwt-login")
public class JwtLoginServlet extends HttpServlet {
private JwtUtil jwtUtil = new JwtUtil("secretKey"); // 自定义 JWT 工具类
private RedisTemplate<String, Object> redisTemplate; // Spring Redis 模板
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String username = req.getParameter("username");
// 验证通过后...
String userId = "1001"; // 从数据库获取用户 ID
// 1. 生成 JWT(过期时间 1 小时)
String jwt = jwtUtil.generateToken(userId, 3600);
// 2. 创建 Session 并存储到 Redis(key:userId,value:用户信息)
Map<String, Object> sessionData = new HashMap<>();
sessionData.put("username", username);
sessionData.put("role", "admin");
redisTemplate.opsForValue().set("session:" + userId, sessionData, 3600, TimeUnit.SECONDS);
// 3. 返回 JWT 给客户端
resp.getWriter().write(jwt);
}
}
// 验证 JWT 和 Session 的过滤器
@WebFilter("/*")
public class JwtSessionFilter implements Filter {
private JwtUtil jwtUtil = new JwtUtil("secretKey");
private RedisTemplate<String, Object> redisTemplate;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String jwt = req.getHeader("Authorization");
if (jwt == null || !jwt.startsWith("Bearer ")) {
((HttpServletResponse) response).sendError(401, "未携带令牌");
return;
}
// 验证 JWT
String token = jwt.substring(7);
if (!jwtUtil.verifyToken(token)) {
((HttpServletResponse) response).sendError(401, "令牌无效");
return;
}
// 解析用户 ID
String userId = jwtUtil.getUserId(token);
// 从 Redis 获取 Session 数据
Map<String, Object> sessionData = (Map<String, Object>) redisTemplate.opsForValue().get("session:" + userId);
if (sessionData == null) {
((HttpServletResponse) response).sendError(401, "会话已失效");
return;
}
// 将 Session 数据存入请求属性,供后续使用
req.setAttribute("sessionData", sessionData);
chain.doFilter(request, response);
}
}
|
6. 小结
- Session 核心机制:通过
JSESSIONID 标识用户,服务器端存储会话数据,解决 HTTP 无状态问题。
- 关键特性:
- 标识方式:Cookie 优先,URL Rewriting 作为降级;
- 生命周期:超时自动销毁,支持手动 invalidate;
- 分布式共享:依赖 Redis 等中间件,或使用 Sticky Session。
- 安全防护:
- 防会话固定:登录后重置 Session ID;
- 防劫持:HTTPS + Cookie 安全属性;
- 防 CSRF:Session Token 验证。
- 扩展方案:与 JWT 混合使用,兼顾分布式兼容性和会话可控性。
Session 管理是 Web 应用身份认证的基础,合理设计可兼顾安全性、性能和分布式扩展性。
七. Tomcat 的工作原理(Connector + Container)
Tomcat 作为主流的 Servlet 容器,其核心架构由连接器(Connector) 和容器(Container) 组成,二者协同完成请求处理。以下从架构核心、请求流程、线程模型到性能优化展开详细说明。
1. Tomcat 架构核心
1. 核心组件:Connector 与 Container
Tomcat 的核心功能通过两大组件实现,二者通过 Service 组件关联(一个 Service 可包含多个 Connector 和一个 Container):
-
Connector(连接器):
- 作用:监听网络端口(如 8080),接收客户端 HTTP 请求,解析请求数据为
ServletRequest 对象,最终将 ServletResponse 转换为 HTTP 响应发送给客户端。
- 核心职责:网络通信、协议解析(HTTP/HTTPS)、请求报文解析。
-
Container(容器):
- 作用:负责处理 Connector 传递的请求,加载并执行对应的 Servlet,生成响应数据。
- 核心职责:Servlet 管理、请求分发、业务逻辑执行。
2. 三层容器模型(Hierarchical Container)
Tomcat 的 Container 采用层级结构,从上到下依次为:
| 容器类型 |
作用范围 |
配置示例(server.xml) |
| Engine |
整个 Tomcat 服务(顶级容器) |
<Engine name="Catalina" defaultHost="localhost"> |
| Host |
虚拟主机(对应一个域名) |
<Host name="localhost" appBase="webapps"> |
| Context |
Web 应用(一个 WAR 包) |
<Context path="/myapp" docBase="myapp"> |
| Wrapper |
单个 Servlet 实例 |
由 Tomcat 自动创建,对应 @WebServlet 或 web.xml 中的 Servlet 配置 |
层级关系:Engine → Host → Context → Wrapper,每个上层容器可包含多个下层容器(如一个 Host 可部署多个 Context)。
请求匹配流程:客户端请求 URL(如 http://localhost:8080/myapp/login)会先匹配 Host(localhost),再匹配 Context(/myapp),最后匹配 Wrapper(/login 对应的 Servlet)。
2. 请求处理流程
Tomcat 处理一个 HTTP 请求的完整流程可分为 Connector 接收解析 和 Container 处理响应 两大阶段,具体步骤如下:
1. 阶段一:Connector 接收与解析请求
-
Socket 接入:
- Connector 通过
Endpoint(如 NioEndpoint)监听端口,接收客户端 TCP 连接,生成 Socket 对象。
- 连接由
Acceptor 线程接收,交给 Poller 线程(NIO 模式)通过多路复用管理 I/O 事件(读/写就绪)。
-
协议处理(ProtocolHandler):
Processor 组件(如 Http11Processor)将 Socket 字节流解析为 HTTP 报文(请求行、请求头、请求体)。
- 生成 Tomcat 内部的
org.apache.coyote.Request 和 Response 对象(与 Servlet API 无关的底层对象)。
-
请求转换:
- 通过
CoyoteAdapter 将底层 Request 转换为 Servlet 规范的 HttpServletRequest 对象,Response 转换为 HttpServletResponse。
2. 阶段二:Container 处理与响应
-
Mapper 映射:
Mapper 组件根据请求的 URL(如 /myapp/login)在容器层级中匹配对应的 Wrapper(即目标 Servlet)。
- 映射结果(包含 Context、Wrapper 等信息)存入
HttpServletRequest。
-
Pipeline + Valve 责任链执行:
- 每个容器(Engine/Host/Context/Wrapper)都有一个
Pipeline(管道),管道中包含多个 Valve(阀门)。
- 请求沿容器层级依次流经各 Valve:
EngineValve → HostValve → ContextValve → WrapperValve。
- 最终由
WrapperValve 调用目标 Servlet 的 service() 方法(触发 doGet/doPost 等)。
-
响应返回:
- Servlet 处理完成后,响应数据沿 Valve 链反向传回,经 Connector 转换为 HTTP 响应报文,通过 Socket 发送给客户端。
流程图简化版:
1
2
3
|
客户端 → Socket → Endpoint(Acceptor/Poller)→ Processor(解析HTTP)→ CoyoteAdapter(转换为Servlet请求)→
Mapper(匹配Servlet)→ Pipeline/Valve(容器处理)→ Servlet.service() →
响应沿原路径返回 → 客户端
|
3. 线程模型
Tomcat 的线程模型决定了其处理并发请求的能力,核心依赖 Executor 线程池 和 I/O 模型 的配合。
1. Executor 线程池
Tomcat 通过线程池管理请求处理线程,避免频繁创建/销毁线程的开销,核心配置参数:
maxThreads:最大线程数(默认 200),决定同时处理的最大请求数。
minSpareThreads:核心线程数(默认 10),线程池保持的最小线程数。
acceptCount:请求排队数(默认 100),当线程数达 maxThreads 时,新请求进入队列等待。
配置示例(server.xml):
1
2
3
4
5
6
7
8
9
|
<Executor name="tomcatThreadPool"
namePrefix="catalina-exec-"
maxThreads="500"
minSpareThreads="50"
acceptCount="200"/>
<Connector executor="tomcatThreadPool"
port="8080"
protocol="HTTP/1.1"/>
|
2. I/O 模型(NIO / APR)
Tomcat 支持多种 I/O 模型,影响 Connector 处理连接的效率:
| 模型 |
特点 |
适用场景 |
| BIO(阻塞 I/O) |
一个连接对应一个线程,线程阻塞等待 I/O 完成,资源占用高。 |
低并发、简单场景(已过时) |
| NIO(非阻塞 I/O) |
基于 Java NIO,通过 Poller 线程多路复用管理多个连接,非阻塞处理 I/O。 |
中高并发,主流选择 |
| APR(原生库) |
基于 C 语言库(APR),性能最优,支持 OS 级别的优化(如 Sendfile)。 |
高并发、生产环境(需额外安装库) |
配置方式:通过 protocol 参数指定:
1
2
3
4
5
|
<!-- NIO 模型 -->
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"/>
<!-- APR 模型 -->
<Connector port="8080" protocol="org.apache.coyote.http11.Http11AprProtocol"/>
|
3. I/O 多路复用与非阻塞通信
NIO 模型的核心是多路复用(通过 Selector 实现):
Poller 线程通过 Selector 监听多个 Socket 的 I/O 事件(如读就绪),无需为每个连接创建线程。
- 当事件触发时,
Poller 唤醒工作线程(从线程池获取)处理请求,处理完成后线程返回池,实现“少量线程处理大量连接”。
优势:减少线程上下文切换,降低内存占用,显著提升高并发场景下的吞吐量。
4. 性能优化方向
Tomcat 性能优化需围绕 Connector 配置、资源利用和类加载机制展开:
1. Connector 线程池参数调优
-
maxThreads:根据服务器 CPU 核心数调整(如 4 核 CPU 可设为 200-500),过大会导致线程竞争资源,过小则无法利用多核性能。
-
acceptCount:设置为 maxThreads 的 1-2 倍,避免请求被直接拒绝,但过大会增加响应延迟。
-
connectionTimeout:连接超时时间(默认 20000ms),缩短无效连接的占用时间。
优化示例:
1
2
3
4
5
6
|
<Connector port="8080"
protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="500"
minSpareThreads="50"
acceptCount="1000"
connectionTimeout="30000"/>
|
2. 连接与传输优化
-
KeepAlive:启用长连接(keepAliveTimeout="60000",maxKeepAliveRequests="100"),减少 TCP 握手开销,适合频繁请求的场景。
-
压缩传输:开启 Gzip 压缩(compression="on",compressableMimeType="text/html,text/xml"),减少响应数据大小。
-
缓冲区大小:调整 socketBuffer(默认 9000 字节)和 bufferSize(默认 8192 字节),匹配平均请求/响应大小。
配置示例:
1
2
3
4
5
6
|
<Connector ...
keepAliveTimeout="60000"
maxKeepAliveRequests="100"
compression="on"
compressionMinSize="2048"
socketBuffer="16384"/>
|
3. 热部署与类加载机制
5. 实战建议
1. Tomcat 请求处理流程图
┌─────────┐ ┌─────────────────────────────────────── Connector ────────────────────────────────────────┐
│ │ │ │
│ 客户端 │────▶│ Endpoint(Socket监听)→ Processor(HTTP解析)→ CoyoteAdapter(转换为Servlet请求) │
│ │ │ │
└─────────┘ └───────────────────────────┬──────────────────────────────────────────────────────────────┘
│
▼
┌─────────┐ ┌─────────────────────────────────────── Container ────────────────────────────────────────┐
│ │ │ │
│ 响应返回 │◀────│ Mapper(匹配Servlet)→ Pipeline/Valve(Engine→Host→Context→Wrapper)→ Servlet.service() │
│ │ │ │
└─────────┘ └───────────────────────────────────────────────────────────────────────────────────────────┘
2. 调整线程池参数验证性能变化
3. 使用 VisualVM 观察请求线程执行
6. 提升思考
如果要自己写一个轻量版 Servlet 容器,你会如何设计 Connector 与 Dispatcher?
设计轻量版 Servlet 容器时,需简化 Tomcat 的复杂架构,核心实现 Connector(请求接收解析) 和 Dispatcher(请求分发) 两大模块:
1. Connector 设计
简化代码示例:
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
|
public class SimpleConnector {
private final Selector selector;
private final ServerSocketChannel serverSocket;
private final SimpleDispatcher dispatcher;
public SimpleConnector(int port, SimpleDispatcher dispatcher) throws IOException {
this.dispatcher = dispatcher;
selector = Selector.open();
serverSocket = ServerSocketChannel.open();
serverSocket.bind(new InetSocketAddress(port));
serverSocket.configureBlocking(false);
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
}
public void start() throws IOException {
while (true) {
selector.select(); // 监听 I/O 事件
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isAcceptable()) {
// 接收新连接
SocketChannel socket = serverSocket.accept();
socket.configureBlocking(false);
socket.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 读取请求并解析
SocketChannel socket = (SocketChannel) key.channel();
SimpleServletRequest request = HttpParser.parse(socket); // 自定义解析器
SimpleServletResponse response = new SimpleServletResponse(socket);
// 交给 Dispatcher 处理
dispatcher.dispatch(request, response);
socket.close();
}
}
keys.clear();
}
}
}
|
2. Dispatcher 设计
简化代码示例:
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
|
public class SimpleDispatcher {
private final Map<String, Servlet> servletMap = new HashMap<>();
// 注册 Servlet(启动时调用)
public void registerServlet(String urlPattern, Servlet servlet) {
servletMap.put(urlPattern, servlet);
}
public void dispatch(SimpleServletRequest request, SimpleServletResponse response) {
String uri = request.getRequestURI();
Servlet servlet = servletMap.get(uri);
if (servlet == null) {
response.setStatus(404);
return;
}
try {
// 初始化 Servlet(仅一次)
servlet.init(new SimpleServletConfig());
// 调用 service 方法
servlet.service(request, response);
} catch (ServletException | IOException e) {
response.setStatus(500);
}
}
}
|
3. 关键简化点
-
省略容器层级(仅保留 Servlet 映射),无需 Engine/Host/Context。
-
简化协议解析(仅支持基本 HTTP 方法和头字段)。
-
使用固定线程池处理请求,避免复杂的 NIO 线程模型。
通过以上设计,可实现一个支持基本 Servlet 规范的轻量容器,理解 Tomcat 核心工作原理。
总结
深入理解servlet是核心,必须掌握。