背景

本文是《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)严格执行,核心是:

1
“创建→初始化→处理请求→销毁→回收”

完整流程,共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 启动时,它会读取你的应用配置,核心是“读取配置→判断加载时机→创建并初始化实例”,具体步骤如下:

  1. 读取配置:容器启动时,扫描应用的web.xml文件或Servlet类上的@WebServlet注解,获取Servlet的全类名、访问路径、load-on-startup值等信息。

  2. 创建包装对象:容器为每个Servlet创建ServletWrapper对象,存储其配置信息(如ServletConfig),统一管理生命周期。

  3. 判断加载时机

    • 若配置load-on-startup≥0(如@WebServlet(loadOnStartup = 1)或web.xml中<load-on-startup>1</load-on-startup>):容器启动时立即加载并初始化。
    • load-on-startup<0或未配置:容器延迟加载,直到第一个请求访问该Servlet时才加载并初始化。
  4. 执行初始化:通过反射创建实例并调用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. 取值含义

    • 正整数(如1、2)或0:容器启动时加载,数字越小,加载优先级越高(1比2先加载)。
    • 负数(如-1)或未配置:延迟加载(首次请求时加载),无优先级顺序。
  2. 多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>
    
  3. 关键注意点:若多个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)单实例+多线程的底层逻辑

  • 单实例:容器对每个Servlet仅创建1个实例,所有请求共享该实例(减少内存开销)。

  • 多线程:容器维护一个线程池,每个请求分配1个独立线程,线程调用Servlet的service()/doGet()等方法(提高处理效率)。

(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配置与获取

  • web.xml配置:顶层全局参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    <!-- 全局参数:所有组件可共享 -->
    <context-param>
        <param-name>appName</param-name>
        <param-value>UserManagementSystem</param-value>
    </context-param>
    <context-param>
        <param-name>dbUrl</param-name>
        <param-value>jdbc:mysql://localhost:3306/user_db</param-value>
    </context-param>
    
  • 代码获取:任何组件中通过getServletContext()获取

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    public class LoginServlet extends HttpServlet {
        @Override
        protected void doPost(...) {
            ServletContext context = getServletContext();
            // 获取全局参数
            String appName = context.getInitParameter("appName"); // UserManagementSystem
            String dbUrl = context.getInitParameter("dbUrl"); // jdbc:mysql://...
    
            // 共享应用级数据(所有Servlet可访问)
            context.setAttribute("onlineUserCount", 100);
        }
    }
    
    // 另一个Servlet中获取共享数据
    public class IndexServlet extends HttpServlet {
        @Override
        protected void doGet(...) {
            ServletContext context = getServletContext();
            int onlineCount = (int) context.getAttribute("onlineUserCount"); // 100
        }
    }
    

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)响应流与缓冲机制

  • 缓冲机制:Tomcat默认启用响应缓冲(默认大小2KB),response.getWriter()getOutputStream()的输出先写入缓冲区,满足以下条件时才真正发送给客户端:

    • 缓冲区满;
    • 调用response.flushBuffer()手动刷新;
    • 响应处理完成(Servlet方法执行结束)。
  • 关闭缓冲response.setBufferSize(0)可禁用缓冲,但可能导致响应头无法修改(因为数据已发送)。

(2)核心响应头与传输编码

  • Content-Type:指定响应数据类型及编码,如text/html;charset=UTF-8

    1
    
    response.setContentType("application/json;charset=UTF-8");
    
  • Chunked Transfer Encoding:当响应长度未知时(未设置Content-Length),HTTP/1.1采用分块传输,格式为Transfer-Encoding: chunked,Tomcat自动处理分块编码。

  • 字符流 vs 字节流

    • PrintWriter getWriter():字符流,自动按Content-Type的编码转换为字节。
    • ServletOutputStream getOutputStream():字节流,直接输出原始字节(适合二进制数据如文件)。
    • 注意:两者不可同时使用,否则抛出IllegalStateException

(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只能读取一次的原因

  • HTTP请求体的输入流(ServletInputStream)是单向流动的字节流,读取后指针到达末尾,无法重置(类似一次性管道)。

  • 若过滤器先读取流,后续Servlet将无法获取数据,导致getParameter()返回空。

(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)请求体加解密机制实现

结合RequestWrapperResponseWrapper,在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”,流程如下:

  1. 容器创建 FilterChain 实例,将当前请求匹配的所有 Filter 按序存入链中;
  2. 调用第一个 Filter 的 doFilter() 方法,传入 requestresponseFilterChain
  3. 每个 Filter 处理完请求后,调用 filterChain.doFilter() 触发下一个 Filter;
  4. 最后一个 Filter 调用 doFilter() 时,容器会执行目标 Servlet(如 HelloServlet);
  5. 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 族(属性变化监听器) 监听 ServletContextHttpSessionServletRequest属性的增删改事件(setAttributeremoveAttributereplaceAttribute),核心方法对应“添加”“移除”“替换”三个事件。 以 ServletContextAttributeListener 为例(HttpSessionAttributeListenerServletRequestAttributeListener 用法类似):

 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) 生命周期事件触发顺序

当一个请求从“到达”到“处理完成”,三大组件的生命周期事件触发顺序严格遵循“从大到小”(应用→会话→请求),具体流程如下:

  1. 应用启动阶段(仅一次):ServletContextListener.contextInitialized() → 初始化全局资源;
  2. 用户首次访问(创建会话):
    • ServletRequestListener.requestInitialized() → 请求创建;
    • HttpSessionListener.sessionCreated() → 会话创建(若首次访问,触发 getSession());
  3. 请求处理阶段:Filter 链 → 目标 Servlet → 处理请求;
  4. 请求销毁阶段
    • ServletRequestListener.requestDestroyed() → 请求处理完成,响应发送;
  5. 会话销毁阶段(超时/手动销毁):HttpSessionListener.sessionDestroyed() → 会话销毁;
  6. 应用关闭阶段(仅一次):ServletContextListener.contextDestroyed() → 释放全局资源。

3. Filter 与 Listener 的协同

Filter 擅长“拦截请求/响应并处理”,Listener 擅长“监听事件并记录状态”,二者协同可实现更复杂的监控或业务逻辑,典型场景为“请求耗时统计 + 在线人数统计”。

协同案例:请求监控体系

需求:统计“当前在线人数”和“每个请求的耗时”,并将耗时记录到全局监控中。

(1) 组件分工

  • OnlineUserListenerHttpSessionListener):监听会话创建/销毁,统计在线人数;
  • PerformanceFilter(Filter):拦截所有请求,计算单个请求耗时;
  • AppListenerServletContextListener):初始化全局监控容器(如存储耗时的列表)。

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

协同逻辑

  1. 应用启动时,AppListener 初始化全局监控列表;
  2. 用户首次访问时,OnlineUserListener 创建会话并更新在线人数;
  3. 每个请求到达时,PerformanceFilter 先获取当前在线人数,再计算请求耗时;
  4. 请求处理完成后,PerformanceFilter 将耗时存入全局监控列表;
  5. 应用关闭时,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) 设计思路

  1. 定义“Filter 配置类”:封装 Filter 的核心参数(Filter 实例、URL 模式、order、启用状态);
  2. 提供“Filter 注册器”:管理所有 Filter 配置,在应用启动时动态注册到容器;
  3. 支持“动态启用/禁用”:通过配置(如配置文件)控制 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
    }
}
  1. 可插拔:通过 enabled 参数控制 Filter 是否生效,无需修改代码;
  2. 动态配置:URL 模式、执行顺序可从配置文件读取(如 application.properties),避免硬编码;
  3. 解耦: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); // 可能导致计数错误

解决方案

  • 使用线程安全的数据类型(如 AtomicInteger):

    1
    2
    3
    4
    5
    6
    
    // 初始化时存储 AtomicInteger 实例
    context.setAttribute("onlineUserCount", new AtomicInteger(0));
    
    // 并发修改时(线程安全)
    AtomicInteger count = (AtomicInteger) context.getAttribute("onlineUserCount");
    count.incrementAndGet(); // 原子操作,无线程安全问题
    
  • 对临界区加锁(synchronized):

    1
    2
    3
    4
    
    synchronized (context) { // 锁定 ServletContext 实例
        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

内部原理

  1. 接收虚拟路径(以 / 开头,相对于 Web 应用根目录),如 /WEB-INF/classes/db.properties
  2. 容器将虚拟路径映射到应用在磁盘上的真实路径(如 tomcat/webapps/yourapp/WEB-INF/classes/db.properties);
  3. 打开文件输入流并返回,若资源不存在则返回 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 的 setAttributegetAttribute 交换数据,无需直接耦合。

示例: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.propertiesdb.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 读写数据,保证一致性。
  • 实现步骤
    1. 封装 Redis 操作工具类(如 RedisUtil),提供 set/get/delete 方法;
    2. 用 Redis 替代 ServletContext 的 attribute 存储全局数据;
    3. 节点启动时从 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)核心功能

  • 存储全局参数(静态配置)和动态属性(运行时数据);
  • 提供资源访问能力(getResourceAsStreamgetRealPath);
  • 实现应用内组件通信(跨 Servlet/Filter 共享数据)。

(3)线程安全:全局属性需通过原子类或同步机制保证多线程安全。
(4)分布式扩展:单节点 ServletContext 无法跨节点共享,需借助 Redis、ZooKeeper 等中间件实现分布式数据同步。

六、 异步 Servlet

异步 Servlet 是 Servlet 3.0 引入的重要特性,通过非阻塞 I/O 模型解决同步处理中线程长时间阻塞的问题,显著提升高并发场景下的系统吞吐量。以下从机制原理、线程模型、应用场景到实战案例展开说明。

1. Servlet 3.0 异步处理机制

1. AsyncContext 的工作原理

异步 Servlet 的核心是 AsyncContext 对象,它将请求处理从“主线程阻塞等待”转为“异步线程处理 + 回调响应”,流程如下:

  1. 开启异步:在 Servlet 中调用 request.startAsync() 标记请求进入异步模式,返回 AsyncContext 实例,此时容器主线程可释放回线程池。
  2. 异步处理:通过 AsyncContext 启动异步线程(或提交到自定义线程池)处理耗时操作(如数据库查询、远程调用)。
  3. 完成响应:异步处理完成后,调用 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. 超时机制与异常处理

  • 超时机制:通过 asyncContext.setTimeout(long timeoutMs) 设置超时时间,超时后触发 AsyncListener.onTimeout(),需在监听器中手动处理响应(如返回超时提示),否则容器可能返回 503 错误。

  • 异常处理:异步线程中抛出的异常会触发 AsyncListener.onError(),可在监听器中捕获并处理(如记录日志、返回错误响应)。

超时与异常处理示例(基于上面的监听器代码):

  • 当异步处理耗时超过 10 秒,onTimeout 被调用,返回 “Timeout!” 给客户端。
  • 若异步线程中发生异常(如 NullPointerException),onError 被调用,打印异常信息。

2. 异步线程模型

1. AsyncContext.start() 与容器线程池

  • asyncContext.start(Runnable) 会将任务提交到容器的异步线程池(如 Tomcat 的 AsyncExecutor),默认线程池配置可通过容器参数调整(如 Tomcat 的 maxThreads)。

  • 自定义线程池:为避免依赖容器线程池,可使用 ExecutorService 手动管理线程,更灵活控制线程数量和优先级。

自定义线程池示例

 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 操作完成(如等待数据库返回结果),导致线程资源浪费。异步处理通过以下方式避免阻塞:

  1. 主线程在开启异步后立即释放,回到线程池处理新请求;
  2. I/O 操作(如数据库查询)在异步线程中执行,此时线程处于“运行态”而非“阻塞态”;
  3. 若 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 流),核心思路如下:

  1. 基于长轮询的模拟

    • 客户端发送异步请求,服务器 hold 住连接直到有数据或超时;
    • 数据推送后,客户端立即发起新的异步请求,维持“准实时”通信;
    • 缺点:存在一定延迟(取决于超时时间),每次通信需重新建立连接。
  2. 基于 HTTP 流的模拟

    • 服务器通过 AsyncContext 保持响应流打开,持续向客户端写入数据(如 SSE 格式);
    • 客户端通过 EventSource 接收流数据,实现单向持续推送;
    • 如需客户端向服务器发送数据,需额外发起 POST 请求。
  3. 与 WebSocket 的差异

    • WebSocket 是持久连接,全双工,开销低;
    • 基于异步 Servlet 的模拟依赖 HTTP,半双工(或通过双连接实现全双工),开销较高;
    • 适用场景:浏览器不支持 WebSocket 时的降级方案。

扩展实现关键点

  • 使用线程安全的集合管理所有活跃的 AsyncContext
  • 实现消息广播机制(如上面的 onNewMessage 方法);
  • 处理连接超时和异常关闭,避免资源泄漏。

六. Session 管理机制

Session 是 Web 应用中用于跟踪用户状态的核心机制,通过在服务器端存储用户会话信息,解决 HTTP 无状态协议的局限。以下从 Session 的创建原理、同步安全、安全防护到实战案例展开说明。

1. Session 的创建与标识

Session 的核心是“服务器端存储会话数据 + 客户端携带会话标识”,客户端通过两种方式传递会话标识:

1)JSESSIONID Cookie(默认方式)

  • 原理:当服务器创建 Session 时,会生成一个唯一标识 JSESSIONID(如 766F6D3A6A553572),并通过 Set-Cookie 响应头发送给客户端,客户端后续请求会自动携带该 Cookie。

  • 流程

    1
    2
    
    客户端首次请求 → 服务器创建 Session,生成 JSESSIONID → 响应头 Set-Cookie: JSESSIONID=xxx → 客户端存储 Cookie
    客户端后续请求 → 请求头 Cookie: JSESSIONID=xxx → 服务器通过 JSESSIONID 找到对应 Session
    

2)URL Rewriting(Cookie 禁用时的降级方案)

  • 原理:当客户端禁用 Cookie 时,服务器会将 JSESSIONID 拼接在 URL 中(如 /user?JSESSIONID=xxx),客户端通过 URL 传递会话标识。

  • 代码示例

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    @WebServlet("/url-rewrite")
    public class UrlRewriteServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
            // 获取或创建 Session
            HttpSession session = req.getSession();
            // 生成包含 JSESSIONID 的 URL
            String url = resp.encodeURL("/next-page"); // 自动拼接 JSESSIONID(若 Cookie 禁用)
            resp.getWriter().println("跳转链接:<a href='" + url + "'>下一页</a>");
        }
    }
    
    • resp.encodeURL():对 URL 进行重写,若客户端支持 Cookie 则返回原 URL,否则拼接 JSESSIONID

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)超时销毁(最常见)

  • 超时时间:默认 30 分钟(可通过 web.xml 或代码配置),从最后一次请求时间开始计时。

  • 配置方式

    1
    2
    3
    4
    
    <!-- web.xml 全局配置 -->
    <session-config>
        <session-timeout>60</session-timeout> <!-- 单位:分钟 -->
    </session-config>
    
    1
    2
    3
    
    // 代码中单独配置(单位:秒)
    HttpSession session = req.getSession();
    session.setMaxInactiveInterval(3600); // 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)解决方案

  • 同步代码块:锁定 Session 对象或属性本身

    1
    2
    3
    4
    5
    6
    
    synchronized (session) { // 锁定 Session 实例
        Integer count = (Integer) session.getAttribute("count");
        if (count == null) count = 0;
        count++;
        session.setAttribute("count", count);
    }
    
  • 使用线程安全的数据类型:如 AtomicInteger

    1
    2
    3
    4
    5
    6
    7
    
    HttpSession session = req.getSession();
    AtomicInteger count = (AtomicInteger) session.getAttribute("count");
    if (count == null) {
        count = new AtomicInteger(0);
        session.setAttribute("count", count);
    }
    count.incrementAndGet(); // 原子操作,线程安全
    

2. 分布式环境下 Session 共享

在多服务器节点的分布式架构中,默认的内存 Session 无法跨节点共享(节点 A 创建的 Session 在节点 B 中不可见),需通过以下方案解决:

1)Sticky Session(粘性会话)

  • 原理:通过负载均衡器将同一用户的所有请求固定到同一节点(如 Nginx 的 ip_hash),避免跨节点 Session 问题。

  • 配置示例(Nginx):

    1
    2
    3
    4
    5
    
    upstream backend {
        ip_hash; # 基于客户端 IP 哈希,固定到同一节点
        server 192.168.1.101:8080;
        server 192.168.1.102:8080;
    }
    
  • 缺点:节点故障会导致 Session 丢失,负载可能不均。

2)Redis Session(推荐)

  • 原理:将 Session 数据存储到 Redis 集群(分布式缓存),所有节点通过 Redis 读写 Session,实现跨节点共享。

  • 实现步骤

    1. 引入 Redis 依赖(如 Jedis);
    2. 自定义 HttpSession 实现,将数据存储到 Redis;
    3. 通过过滤器拦截请求,替换默认 Session 为 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)

  • 攻击方式:攻击者诱导用户使用攻击者预先创建的 JSESSIONID,用户登录后,攻击者使用该 JSESSIONID 即可劫持会话。

  • 防护方案:用户登录成功后重置 Session ID(使旧 ID 失效):

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    @WebServlet("/login")
    public class LoginServlet extends HttpServlet {
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
            String username = req.getParameter("username");
            String password = req.getParameter("password");
    
            if ("admin".equals(username) && "123".equals(password)) {
                // 登录成功,重置 Session ID
                HttpSession session = req.getSession();
                session.invalidate(); // 销毁旧 Session
                session = req.getSession(true); // 创建新 Session(新 JSESSIONID)
                session.setAttribute("user", username); // 存储用户信息
                resp.sendRedirect("/home");
            }
        }
    }
    

2. 会话劫持(Session Hijacking)

  • 攻击方式:攻击者窃取用户的 JSESSIONID(如通过网络嗅探、XSS 攻击),使用该 ID 冒充用户访问系统。

  • 防护方案

    • 使用 HTTPS:加密传输,防止 JSESSIONID 被嗅探;

    • 设置 Cookie 安全属性

      1
      2
      3
      4
      5
      
      Cookie sessionCookie = new Cookie("JSESSIONID", session.getId());
      sessionCookie.setSecure(true); // 仅通过 HTTPS 传输
      sessionCookie.setHttpOnly(true); // 禁止 JavaScript 访问(防 XSS)
      sessionCookie.setPath("/"); // 限制 Cookie 作用路径
      resp.addCookie(sessionCookie);
      
    • 定期更换 Session ID:如每 30 分钟或敏感操作(付款)后重置 ID。

3. Session Token + CSRF 防护

跨站请求伪造(CSRF)攻击中,攻击者诱导用户在已登录状态下访问恶意链接,利用用户的 Session 身份执行操作。结合 Session Token 防护:

  • 原理:服务器生成随机 Token 存入 Session,客户端提交请求时必须携带该 Token,服务器验证 Token 有效性。

  • 实现示例

     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
    
    // 生成并存储 CSRF Token
    @WebServlet("/form")
    public class FormServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
            HttpSession session = req.getSession();
            String csrfToken = UUID.randomUUID().toString();
            session.setAttribute("csrfToken", csrfToken); // 存储 Token 到 Session
    
            // 前端表单中嵌入 Token
            resp.getWriter().println("""
                <form action="/submit" method="post">
                    <input type="hidden" name="csrfToken" value="%s">
                    <input type="submit" value="提交">
                </form>
                """.formatted(csrfToken));
        }
    }
    
    // 验证 CSRF Token
    @WebServlet("/submit")
    public class SubmitServlet extends HttpServlet {
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
            String clientToken = req.getParameter("csrfToken");
            HttpSession session = req.getSession();
            String serverToken = (String) session.getAttribute("csrfToken");
    
            if (clientToken == null || !clientToken.equals(serverToken)) {
                resp.sendError(403, "CSRF 验证失败");
                return;
            }
    
            // 验证通过,处理请求
            resp.getWriter().println("操作成功");
        }
    }
    

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 配置(实际项目常用方案):

  1. 引入依赖(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>
    
  2. 配置 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
    
  3. 使用方式:与原生 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. 混合方案设计

  • 登录流程

    1. 用户登录成功后,服务器生成 JWT(包含用户 ID、过期时间等),同时创建 Session 存储用户详细信息(如权限、角色);
    2. 客户端存储 JWT(如 localStorage 或 Cookie),每次请求携带 JWT。
  • 请求处理

    1. 服务器验证 JWT 有效性,解析出用户 ID;
    2. 通过用户 ID 从分布式缓存(如 Redis)获取对应的 Session 数据(若 Session 不存在或失效,拒绝请求);
    3. 支持通过 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. 小结

  1. Session 核心机制:通过 JSESSIONID 标识用户,服务器端存储会话数据,解决 HTTP 无状态问题。
  2. 关键特性
    • 标识方式:Cookie 优先,URL Rewriting 作为降级;
    • 生命周期:超时自动销毁,支持手动 invalidate;
    • 分布式共享:依赖 Redis 等中间件,或使用 Sticky Session。
  3. 安全防护
    • 防会话固定:登录后重置 Session ID;
    • 防劫持:HTTPS + Cookie 安全属性;
    • 防 CSRF:Session Token 验证。
  4. 扩展方案:与 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 自动创建,对应 @WebServletweb.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 接收与解析请求

  1. Socket 接入

    • Connector 通过 Endpoint(如 NioEndpoint)监听端口,接收客户端 TCP 连接,生成 Socket 对象。
    • 连接由 Acceptor 线程接收,交给 Poller 线程(NIO 模式)通过多路复用管理 I/O 事件(读/写就绪)。
  2. 协议处理(ProtocolHandler)

    • Processor 组件(如 Http11Processor)将 Socket 字节流解析为 HTTP 报文(请求行、请求头、请求体)。
    • 生成 Tomcat 内部的 org.apache.coyote.RequestResponse 对象(与 Servlet API 无关的底层对象)。
  3. 请求转换

    • 通过 CoyoteAdapter 将底层 Request 转换为 Servlet 规范的 HttpServletRequest 对象,Response 转换为 HttpServletResponse

2. 阶段二:Container 处理与响应

  1. Mapper 映射

    • Mapper 组件根据请求的 URL(如 /myapp/login)在容器层级中匹配对应的 Wrapper(即目标 Servlet)。
    • 映射结果(包含 Context、Wrapper 等信息)存入 HttpServletRequest
  2. Pipeline + Valve 责任链执行

    • 每个容器(Engine/Host/Context/Wrapper)都有一个 Pipeline(管道),管道中包含多个 Valve(阀门)。
    • 请求沿容器层级依次流经各 Valve:EngineValve → HostValve → ContextValve → WrapperValve
    • 最终由 WrapperValve 调用目标 Servlet 的 service() 方法(触发 doGet/doPost 等)。
  3. 响应返回

    • 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. 热部署与类加载机制

  • 热部署:通过 Contextreloadable="true" 实现应用修改后自动重启(开发环境用,生产环境禁用,影响性能)。

  • 类加载优化:Tomcat 每个 Web 应用使用独立的 WebappClassLoaderBase 加载类,避免类冲突。生产环境可预加载常用类,减少运行时加载开销。

5. 实战建议

1. Tomcat 请求处理流程图

┌─────────┐     ┌─────────────────────────────────────── Connector ────────────────────────────────────────┐
│         │     │                                                                                          │
│  客户端  │────▶│  Endpoint(Socket监听)→ Processor(HTTP解析)→ CoyoteAdapter(转换为Servlet请求)       │
│         │     │                                                                                          │
└─────────┘     └───────────────────────────┬──────────────────────────────────────────────────────────────┘
                                            │
                                            ▼
┌─────────┐     ┌─────────────────────────────────────── Container ────────────────────────────────────────┐
│         │     │                                                                                          │
│  响应返回 │◀────│  Mapper(匹配Servlet)→ Pipeline/Valve(Engine→Host→Context→Wrapper)→ Servlet.service() │
│         │     │                                                                                          │
└─────────┘     └───────────────────────────────────────────────────────────────────────────────────────────┘

2. 调整线程池参数验证性能变化

  • 工具:使用 JMeter 模拟 1000 并发请求,测试不同 maxThreadsacceptCount 下的吞吐量(TPS)和响应时间。

  • 结论:在服务器负载范围内(CPU 利用率 < 80%),增大 maxThreads 可提升 TPS;超过负载后,继续增大可能导致响应时间急剧增加。

3. 使用 VisualVM 观察请求线程执行

  • 步骤

    1. 启动 Tomcat 时添加 JVM 参数:-Dcom.sun.management.jmxremote 开启 JMX 监控。
    2. 打开 VisualVM,连接到 Tomcat 进程,查看“线程”标签。
    3. 观察 catalina-exec-* 线程(处理请求的工作线程)的状态(运行、等待、阻塞),分析线程利用情况。
  • 优化点:若大量线程处于“阻塞”状态,可能是线程池过大导致资源竞争,需减少 maxThreads;若线程频繁处于“运行”状态且 TPS 低,可能是 CPU 瓶颈,需优化业务逻辑。

6. 提升思考

如果要自己写一个轻量版 Servlet 容器,你会如何设计 Connector 与 Dispatcher?

设计轻量版 Servlet 容器时,需简化 Tomcat 的复杂架构,核心实现 Connector(请求接收解析)Dispatcher(请求分发) 两大模块:

1. Connector 设计

  • 核心职责:监听端口、解析 HTTP 请求、封装为 ServletRequest

  • 实现步骤

    1. Socket 监听:使用 Java NIO 的 ServerSocketChannelSelector 实现非阻塞监听,避免 BIO 模型的性能问题。
    2. HTTP 解析:简易解析请求行(GET /login HTTP/1.1)、请求头(Host: localhost),忽略复杂场景(如分块传输)。
    3. 请求封装:将解析结果存入自定义的 SimpleServletRequest(实现 ServletRequest 接口)。

简化代码示例

 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 设计

  • 核心职责:根据请求 URL 映射到对应的 Servlet,执行并返回响应。

  • 实现步骤

    1. Servlet 注册:维护一个 Map<String, Servlet>(URL 路径 → Servlet 实例),启动时扫描 @WebServlet 注解注册。
    2. 请求分发:根据 request.getRequestURI() 从 Map 中获取 Servlet,调用其 service() 方法。
    3. 响应处理:将 ServletResponse 转换为 HTTP 响应报文,通过 Socket 发送。

简化代码示例

 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是核心,必须掌握。