一、为什么还要回头聊 Servlet?

如果你是这几年入行的 Java 后端,很可能日常写代码长这样:

@RestController
@RequestMapping("/api")
public class DemoController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello, World";
    }
}

项目启动的是 Spring Boot,依赖是 spring-boot-starter-web,网关是 Spring Cloud Gateway,部署在 K8s 上。一切都很“现代”。

于是一个很自然的问题是:

“现在都微服务、Spring Boot 了,我还需要关心 Servlet 吗?”

我自己的答案是:需要,而且非常值得。

原因有几条:

  1. 出问题时,你绕不过它
    当 JVM 里线程数突然暴涨、Tomcat 连接数被打满、Filter 链执行顺序不对、请求“莫名其妙卡住”时,你其实是在和 Servlet 这层打交道。
    如果你只会在 Controller 里打日志,而对下面这层一无所知,排查问题只能靠“猜”。

  2. 大部分框架都“踩在它头上”
    Spring MVC 的 DispatcherServlet 本身就是一个 Servlet;
    Spring Boot 默认的 Web 模式,本质就是:帮你自动起了一个 Servlet 容器,然后注册了好几个 Servlet/Filter/Listener。
    你现在写的绝大多数业务代码,其实都在 Servlet 模型之上工作——只是你没直接看到。

  3. 理解 Servlet = 掌握 Java Web 的“汇编层”
    就像学操作系统,不是为了天天写内核,而是为了搞懂进程、线程、系统调用到底在干嘛。
    Servlet 也是类似的角色:

    • 它定义了 Java 世界里 HTTP 请求是怎么表示的、

    • 生命周期怎么管理、

    • 线程是怎么调度、

    • 过滤器、监听器这些“横切能力”从哪来的。
      理解这层之后,你会发现:Spring MVC、Spring Security、各种网关和过滤链条,逻辑一下就通了。

  4. 对“框架替换”和“栈切换”的心里更有底
    比如,从 Spring MVC 切到 WebFlux,或者从内嵌 Tomcat 改成 Netty,你就能清楚地知道:

    • 我是还在 Servlet 栈里?

    • 还是已经彻底脱离 Servlet 了?

    • 哪些功能是 Servlet 提供的,哪些是框架自己造的轮子?

所以,这篇文章不会教你怎么“手写一个 Servlet 项目”——那确实没太大必要。
而是希望帮你搞清楚:

Servlet 在 Java Web 体系里到底处在什么位置,
HTTP 请求究竟是怎样一路走到你的业务代码里的。

理解了这些,当你再看 Spring MVC、Spring Boot、Tomcat 的时候,会有一种“整个技术栈对齐了”的感觉。


二、Servlet 到底是什么?规范 vs 容器 vs 应用

很多人一听 Servlet,会下意识想成“那个要配 web.xml、要写一堆继承的老东西”。
其实从架构视角看,Servlet 这俩字至少包含 三层角色

  1. Servlet 规范(Specification)

  2. Servlet 容器(Container)

  3. 你的 Servlet 应用代码(Application)

这三层如果混在一起理解,会特别乱。我们一个个拆开。

2.1 Servlet 规范:定义规则的人

首先,Servlet 最初是 Java EE(现在是 Jakarta EE)规范里的一部分。

它做几件事:

  • 定义一组接口:ServletHttpServletServletRequestServletResponseFilterListener……

  • 规定生命周期:

    • Servlet 何时创建,何时销毁?

    • 怎么接收请求,怎么返回响应?

  • 规定线程模型:

    • 容器可以用多个线程并发调用同一个 Servlet 实例;

    • service()/doGet()/doPost() 的调用需要是线程安全的。

  • 规定扩展点:

    • Filter 如何介入请求链路?

    • Listener 如何监听上下文、Session、Request 的创建和销毁?

你可以把“Servlet 规范”理解为一份协议文档,它只说:

“如果你要做一个 Java Web 容器,你必须按我这套接口和规则办事;
如果你要写 Java Web 应用,你只要实现这些接口,我就保证容器能认得你。”

它自己不提供任何“跑起来的能力”,就像 HTTP RFC 只定义协议,不帮你开服务器。

2.2 Servlet 容器:把规则实现出来的人

既然有了规范,就会有人来实现它。这就是我们熟悉的:

  • Tomcat

  • Jetty

  • Undertow

  • (还有一些集成在应用服务器里的实现,比如 WildFly、WebLogic 等)

这些东西的共同点是:它们都是 Servlet 容器

容器要做的事情就多了,比如:

  1. 打开端口,接收 Socket 连接

    • 在 8080、8081…… 上 accept() HTTP 请求;

    • 负责底层 I/O 模型(BIO、NIO、APR、Netty 等)。

  2. 解析 HTTP 报文,封装成对象

    • 把原始字节解析成请求行、请求头、请求体;

    • 封装到 HttpServletRequest / HttpServletResponse 实现类里。

  3. 根据 URL 映射找到对应的 Servlet

    • 启动时读取 web.xml 或注解 @WebServlet

    • 建立 “URL pattern → Servlet 实例” 的映射表。

  4. 管理 Servlet 的生命周期

    • 创建实例、调用 init()

    • 每个请求时调用 service()

    • 容器关闭时调用 destroy()

  5. 调度线程、管理并发

    • 维护线程池;

    • 多线程调用同一个 Servlet 实例;

    • 为异步 Servlet / 非阻塞 I/O 提供底层支持。

一句话概括 Servlet 容器的职责:

负责把“规范里的接口”落地成“一个真正能接 HTTP 请求并调用你代码的服务器”。

2.3 你的 Servlet 应用:实现回调的人

有了规范,有了容器,第三层才轮到你:

你写的 MyServlet extends HttpServlet,就是在“实现规范定义的回调接口”。

最经典的例子:

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        resp.setContentType("text/plain;charset=UTF-8");
        resp.getWriter().println("Hello Servlet");
    }
}

这一小段代码背后,其实发生了这些事:

  1. 容器启动时扫描到 @WebServlet("/hello"),记住:

    • URL /hello → 这个 HelloServlet 类。

  2. 第一次有请求打到 /hello

    • 容器创建一个 HelloServlet 实例,调用 init()

  3. 每次请求 /hello

    • 容器从线程池拿一个线程;

    • 构造 HttpServletRequestHttpServletResponse 实现类;

    • 调用 HelloServlet.service(req, resp)

    • HttpServlet 父类根据 HTTP 方法分发到 doGet() / doPost()

    • 你在 doGet() 里写业务逻辑、写响应。

也就是说,你的 Servlet 类本质上就是:

一个被容器“回调”的对象,
容器负责所有繁琐的网络、线程、协议解析,
你只负责“拿到 request/response 之后要干什么”。

2.4 三者关系用一张小图总结

可以在脑子里记住这样一个结构(画在纸上也很直观):

           ┌─────────────────────────────┐
           │         Servlet 规范         │
           │  (接口 & 规则 & 生命周期)   │
           └────────────┬────────────────┘
                        │
                        ▼
           ┌─────────────────────────────┐
           │         Servlet 容器         │
           │  Tomcat / Jetty / Undertow  │
           │  实现规范 + 负责网络 & 线程   │
           └────────────┬────────────────┘
                        │ 回调
                        ▼
           ┌─────────────────────────────┐
           │       你的 Web 应用           │
           │   MyServlet / Filter / JSP  │
           └─────────────────────────────┘
  • 规范:像“法律”和“契约”。

  • 容器:像“政府”或“执行机构”,负责按法律办事。

  • 你的代码:像“公民”和“企业”,只要按规则写,就能在这个环境里正常运行。

后面我们会在这个基础上继续往下挖:

  • 一次 HTTP 请求是如何穿过容器、Filter,再到 Servlet 的?

  • Servlet 的生命周期和线程模型具体会有哪些坑?

  • Spring MVC 的 DispatcherServlet 又是如何踩在这整套机制之上的?

这些,会在后续小节里一块展开。


三、一次 HTTP 请求是如何“走到” Servlet 的?

这一节我们就沿着一条“完整请求路径”,从浏览器一路顺着走到你的 doGet() 里,帮你把中间那一堆黑盒都打开。

可以先记住一句话:

Servlet 并不是“自己在那等着请求”
整个过程是:容器在外面监听 → 有请求就回调你的 Servlet。

我们拆成几个关键阶段来看。

3.1 阶段一:容器启动,把“环境”准备好

当你启动 Tomcat(或内嵌的 Spring Boot)时,大概会做这些事情:

  1. 打开端口,监听 Socket

    • 比如监听 8080 端口;

    • 根据配置使用 BIO / NIO / APR 等 I/O 模型;

    • 启动 Acceptor 线程,不停地 accept() 新连接。

  2. 加载并解析 Web 应用配置

    • 读取 web.xml 或扫描 @WebServlet@WebFilter@WebListener

    • 建立 URL 映射表:/helloHelloServlet/api/*ApiServlet 等。

  3. 初始化一些“急需” Servlet

    • 对于配置了 load-on-startup 的 Servlet:

      • 容器会在启动时就创建实例并调用 init()

    • 没配的,一般会在第一次请求到来时再懒加载。

到这里为止,容器相当于“门店开张,员工到位,业务窗口挂好牌子”,只是还没有顾客进来。

3.2 阶段二:请求进来,从字节流到 Request/Response 对象

当浏览器发起一个 HTTP 请求,例如:

GET /hello HTTP/1.1
Host: localhost:8080
User-Agent: ...
...

Tomcat 这类容器大致会这么干:

  1. Acceptor 线程接住连接

    • ServerSocket 收到一个新的 TCP 连接;

    • 把这个连接封装为一个 Socket / SocketChannel,交给 I/O 线程池或 Selector 维护。

  2. 读取并解析 HTTP 报文

    • 从这个 Socket 里读数据;

    • 解析出:

      • 请求行(方法、URL、协议版本);

      • 请求头(Header);

      • 请求体(Body)。

  3. 封装为 HttpServletRequestHttpServletResponse 实现类

    • 比如 org.apache.catalina.connector.Request / Response

    • 再对外暴露为接口 HttpServletRequest / HttpServletResponse

    • 里面已经封装好了:

      • URL、QueryString、Header、Cookie、Session、输入流、输出流等。

这一切你都看不到,但每次你在 doGet() 里拿到的那个 request/response,都是容器帮你先把“原始字节流”解析并包装好的结果。

3.3 阶段三:根据 URL 找到目标 Servlet

解析完 HTTP 请求后,容器会做一个“路由”步骤:

  1. 拿到 URL,比如 /hello

  2. 去启动时建立的映射表里查找:

    • 精确匹配:/helloHelloServlet

    • 前缀匹配:/api/*ApiServlet

    • 后缀匹配:*.doDispatcherServlet

  3. 找到对应的 Servlet 实例(如果还没创建,会先 new + init)。

这个过程就像你去政府大厅办事:

大厅前台看你拿的是哪类业务号,
然后把你分配到对应窗口。

3.4 阶段四:Filter 链先“过一遍”

在请求真正被交给 Servlet 之前,一般还会先通过一串 Filter 链。

你在 web.xml 或注解里配置的 Filter,其实就挂在这个链路上,例如:

@WebFilter("/*")
public class LoggingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp,
                         FilterChain chain) throws IOException, ServletException {
        long start = System.currentTimeMillis();
        try {
            chain.doFilter(req, resp); // 放行,后面可能会到其他 Filter 或目标 Servlet
        } finally {
            long cost = System.currentTimeMillis() - start;
            System.out.println("Request cost: " + cost + " ms");
        }
    }
}

调用顺序大致是:

Request
  └──> Filter1.doFilter()
          └──> Filter2.doFilter()
                  └──> ... 
                          └──> 目标 Servlet.service()/doGet()/doPost()
                  └──< ...
          └──< ...
  └──< Response

Filter 链提供了一个非常强大的“横切点”:

  • 你可以在“所有请求之前/之后”统一做:

    • 日志记录

    • 认证 / 鉴权

    • XSS 防御、跨域处理

    • 请求包装(如包装 request 以便多次读取 body)

后面你再看 Spring Security、链路追踪、全局日志,其实都是在玩这套思想。

3.5 阶段五:调用 service()doGet() / doPost()

当 Filter 链放行之后,终于轮到你的 Servlet 代码被调用了:

  1. 容器从线程池取出一个工作线程(假设是 T1);

  2. 调用 yourServlet.service(request, response)

  3. HttpServletservice() 默认根据 HTTP 方法分发到:

    • doGet()

    • doPost()

    • doPut()

    • doDelete() 等。

所以你写的代码其实是被调用链最末端的一环:

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws IOException {
    String name = req.getParameter("name");
    resp.setContentType("text/plain;charset=UTF-8");
    resp.getWriter().println("Hello " + name);
}

在这里你完全感受不到网络、线程、协议解析的存在,只能看到一个很像普通方法的接口——但那是因为容器帮你遮住了所有复杂度。

3.6 阶段六:写出响应,容器负责“送回去”

当你在 response.getWriter().println(...) 写完数据:

  1. 数据先写入 Servlet 容器内部的缓冲区;

  2. 当缓冲区满了、或你调用了 flush() / close() / 请求处理结束:

    • 容器把缓冲区内容编码成 HTTP 响应报文;

    • 加上状态码、响应头等;

    • 通过 Socket 写回给客户端。

  3. 写完后,容器可能:

    • 关闭连接;

    • 或根据 Connection: keep-alive 和 HTTP 版本决定是否复用连接。

整个请求-响应流程就此结束。

你只需要记住一个心智模型:

你只负责“写入 response 对象”,
真正推回浏览器的是容器。


四、Servlet 生命周期与线程模型:最容易被忽视的“坑点”

说完“请求怎么走到 Servlet”,接下来讲两个最容易踩坑的点:

  1. 生命周期:什么时候创建?什么时候销毁?

  2. 线程模型:多少个实例?多少个线程?共享状态在哪里?

4.1 生命周期:从 init()destroy()

先看一个典型的 Servlet 模板:

@WebServlet(urlPatterns = "/hello", loadOnStartup = 1)
public class HelloServlet extends HttpServlet {

    @Override
    public void init() throws ServletException {
        // 1. 初始化阶段:只调用一次
        System.out.println("HelloServlet init");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        // 2. 请求处理阶段:每次请求都会调用
        resp.getWriter().println("Hello");
    }

    @Override
    public void destroy() {
        // 3. 销毁阶段:容器关闭或应用卸载时调用一次
        System.out.println("HelloServlet destroy");
    }
}

它大致经历三个阶段:

(1)创建与初始化:init()

什么时候创建实例?

  • 默认:第一次有请求命中这个 Servlet 时创建;

  • 配置了 load-on-startup(或注解 loadOnStartup = 1):

    • 容器在启动时就创建实例;

    • 值越小优先级越高。

创建之后,容器会调用一次 init()

  • 适合做:

    • 读取配置

    • 初始化连接池

    • 准备一些缓存或不可变对象

  • 不建议做:

    • 特别耗时、可能阻塞很久的操作(会拖慢容器启动)

注意:构造方法不等于初始化
很多人一上来就在构造器里搞一堆逻辑,但 Servlet 规范鼓励你把“初始化”写在 init();构造器尽量保持轻量。

(2)请求阶段:service() / doGet() / doPost()

这一阶段发生很多次:

  • 每来一个请求:容器就用一个线程调用 service()

  • service() 根据 HTTP 方法再转到具体 doXXX()

所以:

init() 是“一次性的准备”,
doGet() / doPost() 是“重复执行的工作”。

所有请求级别的逻辑(读参数、调业务、写响应),都应该写在 doXXX() 里,而不是 init()

(3)销毁阶段:destroy()

当:

  • 容器关闭(比如关闭 Tomcat);或

  • 应用被卸载 / 重新部署;

容器会调用一次 destroy(),告诉你:

“这个 Servlet 要彻底退出舞台了,你有啥资源需要清理的赶紧清。”

典型场景:

  • 关闭数据库连接池(如果你是自己建的而不是交给 DataSource 管的)。

  • 关闭后台线程(定时任务、消息消费线程等)。

  • 刷新内存中的统计结果到磁盘等。

4.2 线程模型:一个实例,多线程并发访问

接下来是更容易被忽略的点:Servlet 的线程模型

牢记一个关键事实:

默认情况:容器只会创建一个 Servlet 实例,
但会用多个线程并发调用它的 service() / doGet() / doPost()

可以脑补成这样一张图:

        ┌─────────────────────┐
T1 ───▶ │                     │
T2 ───▶ │   HelloServlet 实例  │
T3 ───▶ │                     │
        └─────────────────────┘

这意味着什么?

  • 成员变量 / 静态变量是所有线程共享的;

  • 如果你在 Servlet 里写了共享可变状态,没有任何同步措施,一定存在线程安全问题

4.2.1 最常见的线程安全坑:把请求状态写到成员变量

错误示例:

@WebServlet("/counter")
public class CounterServlet extends HttpServlet {

    private int counter = 0; // 所有线程共享

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        counter++; // 非原子操作,多线程竞争
        resp.getWriter().println("counter = " + counter);
    }
}

在高并发情况下,你可能看到的输出是乱跳的:

  • counter 会丢失更新;

  • 甚至可能出现负数(如果你做减法的话);

  • 完全无法依赖。

更隐蔽的版本,比如:

  • SimpleDateFormat 作为成员变量复用;

  • ArrayListHashMap 当作共享缓存,多线程读写;

  • 把“当前用户”的信息放成成员变量。

这些都是典型的并发踩坑点。

4.2.2 正确的思路:Servlet 尽量无状态

最推荐的写法是:

把 Servlet 写成“无状态”的:不在成员变量里存任何请求相关的数据。

比如:

@WebServlet("/user")
public class UserServlet extends HttpServlet {

    private final UserService userService = new UserService(); // 服务可以是线程安全的

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        String id = req.getParameter("id"); // 局部变量,每个线程一份
        User user = userService.findById(id);
        resp.getWriter().println(user.toString());
    }
}

为什么这是安全的?

  • iduser 都是局部变量,存在线程栈里,不共享

  • userService 作为成员变量,只要自身是线程安全的(要么只读,要么内部做了同步),就没有问题。

一般的最佳实践是:

  • 请求相关数据 → 局部变量 / request 属性;

  • 系统配置、DAO、Service 等 → 成员变量,但要么只读,要么内部线程安全;

  • 尽量不要在 Servlet 里放“可变的全局状态”。

4.2.3 关于 SingleThreadModel:为什么被废弃

有些老文章会提到 SingleThreadModel 接口,说可以让每次请求都用不同 Servlet 实例,从而避免线程安全问题。这个接口现在已经被标记为废弃,原因很简单:

  • 每个请求一个实例,或者实例池,本身就会导致:

    • 创建销毁成本高;

    • 不可预测的性能;

  • 并且根本性问题没解决:你还是要搞清楚哪些数据应该共享,哪些不该。

规范最终的结论是:

与其给你一个“看上去线程安全”但非常昂贵的模式,
不如让你正视“Servlet 默认就是多线程访问”的事实,
再用正常的并发模式和无状态设计去解决问题。

4.3 生命周期 + 线程模型综合起来看:几个实战建议

把上面两点合在一起,可以得到一些简单但很有用的建议:

  1. 初始化逻辑放 init(),而不是构造器或 doGet()

    • 避免每次请求都重复初始化;

    • 避免在构造器里做太多事,影响容器管理。

  2. 清理逻辑放 destroy()

    • 数据库、线程池等资源如果是你自己创建的,就在这里收尾;

    • 不要完全指望 JVM 的 GC,尤其是有本地资源时。

  3. 不要在 Servlet 里保存请求级别的成员变量

    • 请求参数、当前用户、临时计算结果,都应该是局部变量;

    • 真的要把数据带到别的地方,用 request.setAttribute() 或封装成对象传给 Service。

  4. 把 Servlet 写成“细薄的一层”

    • 理想形态:Servlet / Controller 只负责:

      • 解析请求;

      • 调用 Service;

      • 组装响应。

    • 真正的业务和状态,放在 Service / Repository 层,并用框架(比如 Spring)帮你管理单例、多例、事务和线程安全。


到这里,我们已经把:

  • 一次请求从外面走进 Servlet 的整条链路;

  • Servlet 的生命周期;

  • 最容易踩坑的线程模型问题;

都串了一遍。

在这之上,下一步再看 Spring MVC 的 DispatcherServlet、Filter、拦截器,就会非常好理解:它们其实就是在 Servlet 这套机制上“玩花活”。


五、Servlet 家族:Filter、Listener 和它们的职责

前面我们主要讲的是“主角” Servlet 本身,其实在 Servlet 规范里,还有两个同样重要的角色:

  • Filter(过滤器)

  • Listener(监听器)

它们和 Servlet 一起,构成了 Java Web 请求处理的“组合技”。

5.1 Filter:横切关注点的第一站

先看最常用的 Filter。你可以这样理解它:

Filter 是挂在 Servlet 前后的一道“关卡”,
可以对请求/响应做统一的横切处理。

典型实现长这样:

@WebFilter("/*")
public class LoggingFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) {
        System.out.println("LoggingFilter init");
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp,
                         FilterChain chain)
            throws IOException, ServletException {

        long start = System.currentTimeMillis();

        try {
            // 放行,让请求继续往下走(可能到下一个 Filter 或目标 Servlet)
            chain.doFilter(req, resp);
        } finally {
            long cost = System.currentTimeMillis() - start;
            System.out.println("Request cost: " + cost + " ms");
        }
    }

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

关键点:

  • @WebFilter("/*"):表示匹配所有请求;

  • doFilter()

    • chain.doFilter() 之前的逻辑:请求前置处理;

    • chain.doFilter() 之后的逻辑:响应后置处理。

Filter 链是什么样子?

如果你注册了多个 Filter,它们会形成一个

Request
  └──> Filter A.doFilter()
          └──> Filter B.doFilter()
                  └──> Filter C.doFilter()
                          └──> 目标 Servlet.service()/doGet()
                  └──< Filter C 返回
          └──< Filter B 返回
  └──< Filter A 返回
Response

常见的 Filter 使用场景:

  • 统一设置请求编码、响应编码;

  • 记录访问日志、耗时统计;

  • 统一做权限校验、登录检测;

  • 给请求加上 TraceId / SpanId 做链路追踪;

  • 统一处理跨域(CORS)。

你以后看到 Spring Security 的过滤链、某些三方框架的“全局拦截”,本质都在玩这套思路,只不过换了 API 和名字。

5.2 Listener:对“事件”的监听

如果说 Servlet 是“处理请求的人”,Filter 是“守在门口的人”,那 Listener 就是:

“哪儿有动静我就记一笔”的人。

Servlet 规范里定义了一堆 Listener,常见的有:

  • ServletContextListener

    • 监听整个 Web 应用的启动和销毁;

  • HttpSessionListener

    • 监听 Session 创建和销毁;

  • ServletRequestListener

    • 监听每次请求的创建和结束;

  • 还有带 Attribute 的版本:

    • ServletContextAttributeListener

    • HttpSessionAttributeListener

    • ServletRequestAttributeListener

    • 用来监听属性的添加、修改、删除。

举个简单例子:统计在线用户数(粗糙版):

@WebListener
public class OnlineUserListener implements HttpSessionListener {

    private static final AtomicInteger ONLINE = new AtomicInteger(0);

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        int count = ONLINE.incrementAndGet();
        System.out.println("Session created, online = " + count);
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        int count = ONLINE.decrementAndGet();
        System.out.println("Session destroyed, online = " + count);
    }
}

类似的,你可以用 Listener 做:

  • 应用启动时加载配置、预热缓存;

  • 应用关闭时做收尾清理;

  • 统计请求次数、平均耗时;

  • 监控 Session 增长、内存占用情况。

区别于 Filter:

  • Filter 是每个请求链路的一部分

  • Listener 是针对特定“事件”的观察者,不一定每个请求都在链上跑逻辑。

5.3 一张小图:Filter、Servlet、Listener 的位置

可以在脑子里记一个“全家福”示意图:

      【容器层:Tomcat / Jetty ...】

  ┌─────────────────────────────────┐
  │   监听应用级、Session 级事件     │
  │   ┌─────────────────────────┐  │
  │   │      Listener 们        │  │
  │   └─────────────────────────┘  │
  │                                 │
  │   Request 进来                  │
  │      │                          │
  │      ▼                          │
  │   ┌─────────────────────────┐  │
  │   │   Filter 链 (A → B → C)  │  │
  │   └─────────────────────────┘  │
  │              │                  │
  │              ▼                  │
  │       ┌───────────────────┐     │
  │       │   Servlet 实例    │     │
  │       │  doGet/doPost...  │     │
  │       └───────────────────┘     │
  │              │                  │
  │              ▼                  │
  │            Response             │
  └─────────────────────────────────┘
  • Listener:在更“外围”,随时监听各种生命周期事件;

  • Filter:在请求链路上“包裹”着 Servlet;

  • Servlet:是最终处理业务逻辑的“窗口”。

理解这一层结构,对你以后理解 Spring MVC 的拦截器、Spring Security 的过滤链、各类 APM/链路追踪的埋点方式,都非常有帮助。


六、Servlet 与 JSP / Spring MVC / Spring Boot / WebFlux 的关系

讲完 Servlet 自己这一套,我们再把它放到更大的“Java Web 技术地图”里,看一看它和几个关键词之间的关系:

  • JSP

  • Spring MVC

  • Spring Boot

  • WebFlux

你会发现很多你平时用的东西,其实都是在 Servlet 上“盖楼”。

6.1 Servlet + JSP:最早的 Java Web MVC

在 Spring MVC 之前,最经典的一套组合是:

Servlet + JSP + JavaBean

简单理解就是:

  • Servlet 做 Controller

    • 解析请求参数;

    • 调用业务逻辑(JavaBean / Service);

    • 把结果塞到 request.setAttribute() 里;

    • forward() 到 JSP。

  • JSP 做 View

    • 负责页面展示;

    • 用 EL 表达式 ${user.name}、JSTL 标签 <c:forEach> 等读取 request 里的数据。

一个典型示例:

@WebServlet("/user")
public class UserServlet extends HttpServlet {

    private UserService userService = new UserService();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException, ServletException {
        String id = req.getParameter("id");
        User user = userService.findById(id);

        req.setAttribute("user", user);
        req.getRequestDispatcher("/WEB-INF/views/user.jsp")
           .forward(req, resp); // 转发给 JSP 展示
    }
}

user.jsp 可能这么写:

<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<body>
    <h1>用户信息</h1>
    <p>姓名:${user.name}</p>
    <p>邮箱:${user.email}</p>
</body>
</html>

这就是最早的一种 MVC:Servlet(C)+ JSP(V)+ JavaBean/Service(M)。

Spring MVC 其实就是在这套模式上,做了一个更干净、可扩展、可配置的封装。

6.2 Spring MVC:在 Servlet 之上再加一层“前端控制器”

Spring MVC 的核心是一个名叫 DispatcherServlet 的类——注意名字,它本身也是一个 Servlet:

浏览器
  ↓
Servlet 容器 (Tomcat)
  ↓           (URL 匹配)
DispatcherServlet(前端控制器,本质是 Servlet)
  ↓
各种 HandlerMapping / HandlerAdapter / Controller / ViewResolver

和“裸写 Servlet”相比,Spring MVC 做了这些事情:

  1. 统一入口:Front Controller 模式

    • 所有请求统一交给 DispatcherServlet

    • 再由它内部根据 URL 和注解找到对应 Controller 方法。

  2. 注解驱动的 Controller

    • 不用再继承 HttpServlet,改用注解:

      @RestController
      @RequestMapping("/users")
      public class UserController {
          @GetMapping("/{id}")
          public UserDto getUser(@PathVariable Long id) { ... }
      }
      
    • 参数解析、返回值处理、异常处理都交给 Spring。

  3. 灵活的视图解析

    • 返回字符串视图名 → ViewResolver 决定用 JSP/Thymeleaf 等;

    • 返回对象 + @ResponseBody / @RestController → 自动转成 JSON。

  4. 更丰富的扩展点

    • HandlerInterceptor:相当于更靠近 Controller 的“拦截器”;

    • @ControllerAdvice + @ExceptionHandler 用于全局异常处理;

    • HandlerMethodArgumentResolver/ReturnValueHandler 自定义参数和返回值处理。

可以这样总结:

Servlet 定义了“HTTP 请求处理”的底层接口,
Spring MVC 则在这之上实现了一整套高级抽象和约定。

你现在写的几乎所有 @Controller / @RestController,最终都会被 DispatcherServlet 调用,而 DispatcherServlet 最终又是由 Servlet 容器调用。

6.3 Spring Boot:把 Servlet 容器内嵌、自动配置

再往上一层,是现在占绝对主流的 Spring Boot。

Spring Boot 做的事情主要有两类:

  1. 把 Servlet 容器内嵌进来

    • 不再需要单独装一个 Tomcat,再把 war 部署进去;

    • 而是直接内嵌 Tomcat/Jetty/Undertow,打成一个可执行的 fat jar。
      启动方式变成了:

    java -jar app.jar
    

    内部仍然是:

    main() → Spring Boot 启动 → 内嵌 Servlet 容器启动
          → 注册 DispatcherServlet、Filter、Listener
    
  2. 自动配置 Servlet 层和 Spring MVC

    • 引入 spring-boot-starter-web 时:

      • 自动创建 DispatcherServlet

      • 自动注册默认的 HandlerMappingHandlerAdapterMessageConverter 等;

      • 自动暴露 /error 页面等。

    • 你几乎不需要也不应该自己去写 web.xml

所以现在你写 Spring Boot 应用时,虽然完全感受不到 Servlet、Filter、Listener 的存在,但底层仍然是:

浏览器 → 内嵌 Tomcat(Servlet 容器) → DispatcherServlet(Servlet) → Controller。

你写的东西越高级,越容易忘掉底下一层。但一旦出故障,调试、排查资源问题的时候,迟早要回到这一层来。

6.4 WebFlux:当你“离开 Servlet 栈”时会发生什么

最后再放一个对照:WebFlux

Spring 5 起,Spring 有两套 Web 栈:

  • 基于 Servlet 的 Spring MVC(我们前面讲的这一大套);

  • 基于 Reactive Streams 的 Spring WebFlux。

WebFlux 和 Servlet 的关系大致可以这么理解:

  1. 可以跑在 Servlet 容器上,也可以不用 Servlet

    • WebFlux 可以运行在:

      • Netty(最常见);

      • 也可以跑在 Servlet 3.1 容器之上,使用非阻塞 API。

    • 但它本身并不依赖 Servlet 规范,而是有自己的一套接口:

      • ServerRequest / ServerResponse

      • WebFilter(不是 javax.servlet.Filter 那个)

      • HandlerFunction / RouterFunction 等。

  2. 编程模型彻底变成响应式

    • 控制器方法返回 Mono<T> / Flux<T>

      @RestController
      @RequestMapping("/users")
      class ReactiveUserController {
      
          @GetMapping("/{id}")
          public Mono<UserDto> getUser(@PathVariable String id) {
              return userService.findById(id); // 返回 Mono
          }
      }
      
    • 底层用 Reactor + 非阻塞 I/O(比如 Reactor Netty),无需 Servlet 阻塞线程模型。

  3. “思想”上仍然继承了 Servlet 家族

    • 依然有“前端控制器”的思想;

    • 依然有 Filter(只不过换成 WebFlux 自己的 WebFilter);

    • 依然有路由 → Handler → 业务层的分层结构。

简单说:

  • Servlet 栈:DispatcherServlet + Filter + HttpServletRequest/Response

  • WebFlux 栈:HttpWebHandlerAdapter + WebFilter + ServerWebExchangeServerRequest/Response

只是:

一个是“阻塞 Servlet 模型 + 多线程池”,
一个是“非阻塞 Reactive 模型 + 少量事件线程”。

当你搞清楚 Servlet 这条链路,再去看 WebFlux 文档,会更容易识别:

  • 哪些是“完全一样的思想,只是换了一套接口名字”;

  • 哪些是真正“因为响应式、非阻塞引入的新概念”。


七、还需不需要“手写 Servlet”?Servlet 的现实价值

说了这么多,一个很现实的问题是:

“那我以后还要不要自己写 Servlet?”

如果从“日常业务开发”的角度讲,一个比较诚实的答案是:

  • 绝大多数情况下,你不需要、也不应该再从零手写 Servlet 项目。

  • 理解 Servlet,并在合适的地方“露一小手”,仍然很有现实价值。

我们拆开来看。

7.1 日常开发中:不必手写,但必须“心里有数”

在现代 Java Web 项目里:

  • Web 层几乎都是 Spring Boot + Spring MVC / WebFlux;

  • 应用入口是 @SpringBootApplication,而不是 web.xml

  • 全局拦截通常用:

    • HandlerInterceptor(Spring MVC)

    • WebFilter(Spring Boot / WebFlux)

    • 或 Spring Security 的过滤链。

所以,在新项目里:

  • 你很少需要自己去 extends HttpServlet

  • 更不用大规模维护传统的“纯 Servlet + JSP”工程。

但这不等于你可以完全忽略 Servlet:

  • 你需要知道:

    • 为什么 Spring Boot 一定会自动注册一个 DispatcherServlet

    • 为什么有些事情必须用 Filter 做,而不是在 Controller 里做;

    • 为什么线程数、连接数调不好的时候,Tomcat 会直接“把机器搞死”。

简短一句话:

业务代码层面,你可以“不用写 Servlet”;
架构和排障层面,你最好“懂 Servlet”。

7.2 哪些场景下,“直接动 Servlet 家族”很有用?

虽然不常见,但有几类场景,直接用到 Servlet / Filter / Listener 仍然很现实:

场景一:做一个极简的 Web 原型或教学 Demo

比如你在给别人讲解:

  • HTTP 请求到底怎么变成 Java 对象;

  • Servlet 线程模型;

  • Filter 链怎么工作;

这时候,用一个小小的 Servlet 项目比上来就扔 Spring Boot 更直观——“地基”看得见,细节也更透明。

场景二:写一个“跟框架无关”的通用 Filter / Listener

有些功能本质上是 Servlet 层的横切能力,不依赖 Spring:

  • 全局日志 & 访问统计;

  • 统一 TraceId/SpanId 的注入;

  • 请求体的统一解压/解密;

  • 监控 Session 数量、Session 大小等。

如果你希望这个能力在“是否使用 Spring 都能玩”的情况下复用,直接写成 Servlet Filter / Listener 会更通用。

即使在 Spring Boot 里,你也可以通过注册 Servlet Filter Bean,把这类逻辑挂到整个 Servlet 链路的最外层。

场景三:排查奇怪的连接 / 线程 / 超时问题

比如你碰到这些情况:

  • Tomcat 的线程池打满、队列堆积;

  • 某些请求一直“挂着不返回”,但业务日志已经结束;

  • 明明 Controller 结束了,连接还在占用资源;

这时候,能看懂这些东西就非常关键:

  • Tomcat 的 Connector 配置(maxThreads、acceptCount、maxConnections 等);

  • Servlet 的同步/异步处理模型;

  • Filter / Listener 有没有在某些地方“吞掉异常”,导致请求没正常返回。

这些问题如果只停留在“Spring 层面”,非常难排查;但回到 Servlet / 容器这一层,就会清晰很多。

场景四:写“介于网关和业务服务之间”的组件

比如你要实现:

  • 自定义的灰度发布网关;

  • HTTP-level 的限流、IP 黑名单、频控;

  • 对特定 URL 做协议转换或特殊安全防护。

这类组件既可以用 Spring Cloud Gateway / Nginx 来做,也可以在 Servlet 层通过 Filter/Servlet 实现一个轻量的“边缘服务”。
越贴近 Servlet,你越能拿捏:

  • 请求何时被拒绝;

  • 何时释放连接;

  • 何时记录日志。

7.3 学习 Servlet 的建议路径(给你一个实操大纲)

如果你想系统补一补 Servlet,个人比较建议这样的路径,一两天内就能搞定

  1. 写一个极简 HelloServlet 项目

    • 用最传统的方式(不带 Spring),跑起来一个:

      • HelloServlet

      • 一个简单的 web.xml

    • 看清楚它和 Tomcat 的关系。

  2. 加一个 Filter 与 Listener

    • 写一个日志 Filter:

      • 打印 URL、耗时;

    • 写一个简单 Session 统计 Listener:

      • 打印当前 Session 数量;

    • 在浏览器刷新几次,看控制台输出变化。

  3. 刻意写一个“有线程问题”的 Servlet

    • 写一个带 private int counter 的 Servlet;

    • 用 JMeter / ab / wrk 压一下;

    • 看看输出混乱的样子;

    • 再改成线程安全的实现,对比一下。

  4. 对照 Spring Boot

    • 启一个 Spring Boot + Spring MVC 的项目;

    • 打开自动配置报告,看看:

      • 谁注册了 DispatcherServlet

      • 谁注册了默认的 Filter

    • 这时你会发现:

      原来 Spring Boot 做的“自动配置”,很多都是在 Servlet 这一层帮我代劳。

做完这几个小练习,Servlet 这一层在你脑子里就不是抽象名词,而是清晰的“地基结构图”了。


八、总结:Servlet 是“地基”,不是“过去式”

最后,我们用几句话把整篇文章收一下。

8.1 回顾这一路,我们看到了什么?

如果你从头读到这里,大概已经把这样一幅图拼在脑子里了:

  1. Servlet 是什么?

    • 它是 Java/Jakarta EE 规范中定义的 HTTP 请求处理机制

    • 提供了 ServletHttpServletRequest/Response、Filter、Listener 等一整套接口和约定。

  2. 容器扮演的角色

    • Tomcat / Jetty / Undertow 等容器:

      • 负责监听端口、管理 Socket;

      • 解析 HTTP 报文;

      • 构造 request/response 对象;

      • 根据 URL 映射找到 Servlet;

      • 通过线程池并发调用你的 Servlet。

  3. 一次请求的完整旅程

    • 浏览器发出请求;

    • 容器接收并解析;

    • 进入 Filter 链;

    • 调用目标 Servlet 的 service() / doGet() / doPost()

    • Servlet 写入 response;

    • 容器把响应报文发回客户端。

  4. 生命周期与线程模型

    • 生命周期:init() → 多次 service()destroy()

    • 线程模型:一个 Servlet 实例 + 多个线程并发访问

    • 线程安全问题往往出在:把请求级状态放到成员变量 / 静态变量里。

  5. Servlet 家族

    • Servlet:主角,负责处理请求;

    • Filter:围在外面的“横切关卡”,统一做日志、鉴权、CORS 等;

    • Listener:监听应用/Session/Request 的各种事件,适合做统计与资源管理。

  6. 与 JSP / Spring MVC / Spring Boot / WebFlux 的关系

    • Servlet + JSP:最早的 Java Web MVC 模式;

    • Spring MVC:在 Servlet 之上加了一层 DispatcherServlet 前端控制器 + 注解控制器;

    • Spring Boot:把容器内嵌,并自动配置 Servlet/MVC 这一整层;

    • WebFlux:思想上延续“前端控制器 + Filter + Handler”,但底层不依赖 Servlet、改用 Reactive + Netty。

8.2 为什么说 Servlet 是“地基”,而不是“过去式”?

现在的确已经不是“到处写 HttpServlet、手搓 web.xml”的时代了。

  • Spring Boot 帮你盖了高楼;

  • Spring MVC 帮你铺好了 Web 层;

  • 各种 Starter 和云原生框架,让你几乎感受不到 Servlet 的存在。

但只要你走的还是 Servlet 栈,就永远站在 Servlet 模型之上。

  • 当你在调 Tomcat 的线程池和连接数;

  • 当你在写全局日志 Filter、过滤链;

  • 当你在排查一个“莫名其妙挂住的请求”;

  • 当你在考虑“要不要换成 WebFlux 或者 Netty”;

你其实都在和 Servlet 这一层打交道,只是深浅不同而已。

所以,更好的心态是:

不必“回到过去”做人肉 Servlet 程序员,
但要把 Servlet 当作自己技术栈里的“地基知识”,
在需要的时候,敢于把视角从 Controller 往下拉一层。

8.3 下一步可以怎么继续深入?

如果你读完这篇文章觉得还不过瘾,可以尝试这样几个方向:

  1. 从 Servlet 再往下:

    • 研究一下 Tomcat 的 Connector、NIO 线程模型、线程池参数;

    • 看看一次请求从 Socket 进入到 Request 对象,中间走了哪些类。

  2. 从 Servlet 往上:

    • 反向阅读 Spring MVC 中 DispatcherServlet 的源码;

    • 自己写一个 mini 版的前端控制器,支撑一个极简注解 MVC 框架。

  3. 从 Servlet 栈横向对比 WebFlux 栈:

    • 对比两套栈的 Filter/Handler/上下文模型;

    • 理解“阻塞 Servlet + 线程池”和“响应式 Netty + 事件循环”在理念上的差异。

等你把这些都搞明白了,再回头看那句标题:

“Servlet = Java 时代最底层的 Web 请求处理机制”

它就不仅仅是一个响亮的标题,而是一张你可以随时调用的心智地图

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐