Servlet = Java 时代最底层的 Web 请求处理机制
一、为什么还要回头聊 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 吗?”
我自己的答案是:需要,而且非常值得。
原因有几条:
-
出问题时,你绕不过它
当 JVM 里线程数突然暴涨、Tomcat 连接数被打满、Filter 链执行顺序不对、请求“莫名其妙卡住”时,你其实是在和 Servlet 这层打交道。
如果你只会在 Controller 里打日志,而对下面这层一无所知,排查问题只能靠“猜”。 -
大部分框架都“踩在它头上”
Spring MVC 的DispatcherServlet本身就是一个 Servlet;
Spring Boot 默认的 Web 模式,本质就是:帮你自动起了一个 Servlet 容器,然后注册了好几个 Servlet/Filter/Listener。
你现在写的绝大多数业务代码,其实都在 Servlet 模型之上工作——只是你没直接看到。 -
理解 Servlet = 掌握 Java Web 的“汇编层”
就像学操作系统,不是为了天天写内核,而是为了搞懂进程、线程、系统调用到底在干嘛。
Servlet 也是类似的角色:-
它定义了 Java 世界里 HTTP 请求是怎么表示的、
-
生命周期怎么管理、
-
线程是怎么调度、
-
过滤器、监听器这些“横切能力”从哪来的。
理解这层之后,你会发现:Spring MVC、Spring Security、各种网关和过滤链条,逻辑一下就通了。
-
-
对“框架替换”和“栈切换”的心里更有底
比如,从 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 这俩字至少包含 三层角色:
-
Servlet 规范(Specification)
-
Servlet 容器(Container)
-
你的 Servlet 应用代码(Application)
这三层如果混在一起理解,会特别乱。我们一个个拆开。
2.1 Servlet 规范:定义规则的人
首先,Servlet 最初是 Java EE(现在是 Jakarta EE)规范里的一部分。
它做几件事:
-
定义一组接口:
Servlet、HttpServlet、ServletRequest、ServletResponse、Filter、Listener…… -
规定生命周期:
-
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 容器。
容器要做的事情就多了,比如:
-
打开端口,接收 Socket 连接
-
在 8080、8081…… 上
accept()HTTP 请求; -
负责底层 I/O 模型(BIO、NIO、APR、Netty 等)。
-
-
解析 HTTP 报文,封装成对象
-
把原始字节解析成请求行、请求头、请求体;
-
封装到
HttpServletRequest/HttpServletResponse实现类里。
-
-
根据 URL 映射找到对应的 Servlet
-
启动时读取
web.xml或注解@WebServlet; -
建立 “URL pattern → Servlet 实例” 的映射表。
-
-
管理 Servlet 的生命周期
-
创建实例、调用
init(); -
每个请求时调用
service(); -
容器关闭时调用
destroy()。
-
-
调度线程、管理并发
-
维护线程池;
-
多线程调用同一个 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");
}
}
这一小段代码背后,其实发生了这些事:
-
容器启动时扫描到
@WebServlet("/hello"),记住:-
URL
/hello→ 这个HelloServlet类。
-
-
第一次有请求打到
/hello:-
容器创建一个
HelloServlet实例,调用init()。
-
-
每次请求
/hello:-
容器从线程池拿一个线程;
-
构造
HttpServletRequest和HttpServletResponse实现类; -
调用
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)时,大概会做这些事情:
-
打开端口,监听 Socket
-
比如监听 8080 端口;
-
根据配置使用 BIO / NIO / APR 等 I/O 模型;
-
启动 Acceptor 线程,不停地
accept()新连接。
-
-
加载并解析 Web 应用配置
-
读取
web.xml或扫描@WebServlet、@WebFilter、@WebListener; -
建立 URL 映射表:
/hello→HelloServlet、/api/*→ApiServlet等。
-
-
初始化一些“急需” Servlet
-
对于配置了
load-on-startup的 Servlet:-
容器会在启动时就创建实例并调用
init();
-
-
没配的,一般会在第一次请求到来时再懒加载。
-
到这里为止,容器相当于“门店开张,员工到位,业务窗口挂好牌子”,只是还没有顾客进来。
3.2 阶段二:请求进来,从字节流到 Request/Response 对象
当浏览器发起一个 HTTP 请求,例如:
GET /hello HTTP/1.1
Host: localhost:8080
User-Agent: ...
...
Tomcat 这类容器大致会这么干:
-
Acceptor 线程接住连接
-
ServerSocket收到一个新的 TCP 连接; -
把这个连接封装为一个
Socket/SocketChannel,交给 I/O 线程池或 Selector 维护。
-
-
读取并解析 HTTP 报文
-
从这个 Socket 里读数据;
-
解析出:
-
请求行(方法、URL、协议版本);
-
请求头(Header);
-
请求体(Body)。
-
-
-
封装为
HttpServletRequest和HttpServletResponse实现类-
比如
org.apache.catalina.connector.Request/Response; -
再对外暴露为接口
HttpServletRequest/HttpServletResponse; -
里面已经封装好了:
-
URL、QueryString、Header、Cookie、Session、输入流、输出流等。
-
-
这一切你都看不到,但每次你在 doGet() 里拿到的那个 request/response,都是容器帮你先把“原始字节流”解析并包装好的结果。
3.3 阶段三:根据 URL 找到目标 Servlet
解析完 HTTP 请求后,容器会做一个“路由”步骤:
-
拿到 URL,比如
/hello; -
去启动时建立的映射表里查找:
-
精确匹配:
/hello→HelloServlet -
前缀匹配:
/api/*→ApiServlet -
后缀匹配:
*.do→DispatcherServlet等
-
-
找到对应的 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 代码被调用了:
-
容器从线程池取出一个工作线程(假设是 T1);
-
调用
yourServlet.service(request, response); -
HttpServlet的service()默认根据 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(...) 写完数据:
-
数据先写入 Servlet 容器内部的缓冲区;
-
当缓冲区满了、或你调用了
flush()/close()/ 请求处理结束:-
容器把缓冲区内容编码成 HTTP 响应报文;
-
加上状态码、响应头等;
-
通过 Socket 写回给客户端。
-
-
写完后,容器可能:
-
关闭连接;
-
或根据
Connection: keep-alive和 HTTP 版本决定是否复用连接。
-
整个请求-响应流程就此结束。
你只需要记住一个心智模型:
你只负责“写入 response 对象”,
真正推回浏览器的是容器。
四、Servlet 生命周期与线程模型:最容易被忽视的“坑点”
说完“请求怎么走到 Servlet”,接下来讲两个最容易踩坑的点:
-
生命周期:什么时候创建?什么时候销毁?
-
线程模型:多少个实例?多少个线程?共享状态在哪里?
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作为成员变量复用; -
把
ArrayList、HashMap当作共享缓存,多线程读写; -
把“当前用户”的信息放成成员变量。
这些都是典型的并发踩坑点。
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());
}
}
为什么这是安全的?
-
id、user都是局部变量,存在线程栈里,不共享; -
userService作为成员变量,只要自身是线程安全的(要么只读,要么内部做了同步),就没有问题。
一般的最佳实践是:
-
请求相关数据 → 局部变量 /
request属性; -
系统配置、DAO、Service 等 → 成员变量,但要么只读,要么内部线程安全;
-
尽量不要在 Servlet 里放“可变的全局状态”。
4.2.3 关于 SingleThreadModel:为什么被废弃
有些老文章会提到 SingleThreadModel 接口,说可以让每次请求都用不同 Servlet 实例,从而避免线程安全问题。这个接口现在已经被标记为废弃,原因很简单:
-
每个请求一个实例,或者实例池,本身就会导致:
-
创建销毁成本高;
-
不可预测的性能;
-
-
并且根本性问题没解决:你还是要搞清楚哪些数据应该共享,哪些不该。
规范最终的结论是:
与其给你一个“看上去线程安全”但非常昂贵的模式,
不如让你正视“Servlet 默认就是多线程访问”的事实,
再用正常的并发模式和无状态设计去解决问题。
4.3 生命周期 + 线程模型综合起来看:几个实战建议
把上面两点合在一起,可以得到一些简单但很有用的建议:
-
初始化逻辑放
init(),而不是构造器或doGet()-
避免每次请求都重复初始化;
-
避免在构造器里做太多事,影响容器管理。
-
-
清理逻辑放
destroy()-
数据库、线程池等资源如果是你自己创建的,就在这里收尾;
-
不要完全指望 JVM 的 GC,尤其是有本地资源时。
-
-
不要在 Servlet 里保存请求级别的成员变量
-
请求参数、当前用户、临时计算结果,都应该是局部变量;
-
真的要把数据带到别的地方,用
request.setAttribute()或封装成对象传给 Service。
-
-
把 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 做了这些事情:
-
统一入口:Front Controller 模式
-
所有请求统一交给
DispatcherServlet; -
再由它内部根据 URL 和注解找到对应 Controller 方法。
-
-
注解驱动的 Controller
-
不用再继承
HttpServlet,改用注解:@RestController @RequestMapping("/users") public class UserController { @GetMapping("/{id}") public UserDto getUser(@PathVariable Long id) { ... } } -
参数解析、返回值处理、异常处理都交给 Spring。
-
-
灵活的视图解析
-
返回字符串视图名 → ViewResolver 决定用 JSP/Thymeleaf 等;
-
返回对象 +
@ResponseBody/@RestController→ 自动转成 JSON。
-
-
更丰富的扩展点
-
HandlerInterceptor:相当于更靠近 Controller 的“拦截器”; -
@ControllerAdvice+@ExceptionHandler用于全局异常处理; -
HandlerMethodArgumentResolver/ReturnValueHandler自定义参数和返回值处理。
-
可以这样总结:
Servlet 定义了“HTTP 请求处理”的底层接口,
Spring MVC 则在这之上实现了一整套高级抽象和约定。
你现在写的几乎所有 @Controller / @RestController,最终都会被 DispatcherServlet 调用,而 DispatcherServlet 最终又是由 Servlet 容器调用。
6.3 Spring Boot:把 Servlet 容器内嵌、自动配置
再往上一层,是现在占绝对主流的 Spring Boot。
Spring Boot 做的事情主要有两类:
-
把 Servlet 容器内嵌进来
-
不再需要单独装一个 Tomcat,再把 war 部署进去;
-
而是直接内嵌 Tomcat/Jetty/Undertow,打成一个可执行的 fat jar。
启动方式变成了:
java -jar app.jar内部仍然是:
main() → Spring Boot 启动 → 内嵌 Servlet 容器启动 → 注册 DispatcherServlet、Filter、Listener -
-
自动配置 Servlet 层和 Spring MVC
-
引入
spring-boot-starter-web时:-
自动创建
DispatcherServlet; -
自动注册默认的
HandlerMapping、HandlerAdapter、MessageConverter等; -
自动暴露
/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 的关系大致可以这么理解:
-
可以跑在 Servlet 容器上,也可以不用 Servlet
-
WebFlux 可以运行在:
-
Netty(最常见);
-
也可以跑在 Servlet 3.1 容器之上,使用非阻塞 API。
-
-
但它本身并不依赖 Servlet 规范,而是有自己的一套接口:
-
ServerRequest/ServerResponse -
WebFilter(不是javax.servlet.Filter那个) -
HandlerFunction/RouterFunction等。
-
-
-
编程模型彻底变成响应式
-
控制器方法返回
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 阻塞线程模型。
-
-
“思想”上仍然继承了 Servlet 家族
-
依然有“前端控制器”的思想;
-
依然有 Filter(只不过换成 WebFlux 自己的
WebFilter); -
依然有路由 → Handler → 业务层的分层结构。
-
简单说:
-
Servlet 栈:
DispatcherServlet+Filter+HttpServletRequest/Response -
WebFlux 栈:
HttpWebHandlerAdapter+WebFilter+ServerWebExchange、ServerRequest/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,个人比较建议这样的路径,一两天内就能搞定:
-
写一个极简 HelloServlet 项目
-
用最传统的方式(不带 Spring),跑起来一个:
-
HelloServlet; -
一个简单的
web.xml;
-
-
看清楚它和 Tomcat 的关系。
-
-
加一个 Filter 与 Listener
-
写一个日志 Filter:
-
打印 URL、耗时;
-
-
写一个简单 Session 统计 Listener:
-
打印当前 Session 数量;
-
-
在浏览器刷新几次,看控制台输出变化。
-
-
刻意写一个“有线程问题”的 Servlet
-
写一个带
private int counter的 Servlet; -
用 JMeter / ab / wrk 压一下;
-
看看输出混乱的样子;
-
再改成线程安全的实现,对比一下。
-
-
对照 Spring Boot
-
启一个 Spring Boot + Spring MVC 的项目;
-
打开自动配置报告,看看:
-
谁注册了
DispatcherServlet; -
谁注册了默认的
Filter;
-
-
这时你会发现:
原来 Spring Boot 做的“自动配置”,很多都是在 Servlet 这一层帮我代劳。
-
做完这几个小练习,Servlet 这一层在你脑子里就不是抽象名词,而是清晰的“地基结构图”了。
八、总结:Servlet 是“地基”,不是“过去式”
最后,我们用几句话把整篇文章收一下。
8.1 回顾这一路,我们看到了什么?
如果你从头读到这里,大概已经把这样一幅图拼在脑子里了:
-
Servlet 是什么?
-
它是 Java/Jakarta EE 规范中定义的 HTTP 请求处理机制;
-
提供了
Servlet、HttpServletRequest/Response、Filter、Listener 等一整套接口和约定。
-
-
容器扮演的角色
-
Tomcat / Jetty / Undertow 等容器:
-
负责监听端口、管理 Socket;
-
解析 HTTP 报文;
-
构造
request/response对象; -
根据 URL 映射找到 Servlet;
-
通过线程池并发调用你的 Servlet。
-
-
-
一次请求的完整旅程
-
浏览器发出请求;
-
容器接收并解析;
-
进入 Filter 链;
-
调用目标 Servlet 的
service()/doGet()/doPost(); -
Servlet 写入 response;
-
容器把响应报文发回客户端。
-
-
生命周期与线程模型
-
生命周期:
init()→ 多次service()→destroy(); -
线程模型:一个 Servlet 实例 + 多个线程并发访问;
-
线程安全问题往往出在:把请求级状态放到成员变量 / 静态变量里。
-
-
Servlet 家族
-
Servlet:主角,负责处理请求; -
Filter:围在外面的“横切关卡”,统一做日志、鉴权、CORS 等; -
Listener:监听应用/Session/Request 的各种事件,适合做统计与资源管理。
-
-
与 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 下一步可以怎么继续深入?
如果你读完这篇文章觉得还不过瘾,可以尝试这样几个方向:
-
从 Servlet 再往下:
-
研究一下 Tomcat 的 Connector、NIO 线程模型、线程池参数;
-
看看一次请求从 Socket 进入到 Request 对象,中间走了哪些类。
-
-
从 Servlet 往上:
-
反向阅读 Spring MVC 中
DispatcherServlet的源码; -
自己写一个 mini 版的前端控制器,支撑一个极简注解 MVC 框架。
-
-
从 Servlet 栈横向对比 WebFlux 栈:
-
对比两套栈的 Filter/Handler/上下文模型;
-
理解“阻塞 Servlet + 线程池”和“响应式 Netty + 事件循环”在理念上的差异。
-
等你把这些都搞明白了,再回头看那句标题:
“Servlet = Java 时代最底层的 Web 请求处理机制”
它就不仅仅是一个响亮的标题,而是一张你可以随时调用的心智地图。
更多推荐
所有评论(0)