如果你打开了这篇,说明你已经知道------Spring MVC 的底层,站着一个叫 Servlet 的东西。但你可能没想过:为什么 Spring 不自己从头写一套 Web 处理逻辑,而非要"寄生"在 Servlet 规范之上?
答案很简单:因为 Servlet 是 Java Web 世界的"通用语言"。Tomcat、Jetty、Undertow 这些 Web 容器都"说" Servlet 这套语言。Spring 只要"说"同样的语言,就能跑在任何容器上------这就是规范的力量。
这一篇,我们把 Servlet 彻底搞清楚。
学习目标
- 理解 Servlet 的本质:它是一个处理网络请求的 Java 接口规范
- 掌握 Servlet 的完整生命周期(加载→实例化→初始化→服务→销毁)
- 理解 Servlet 容器(Tomcat) 如何管理 Servlet 实例
- 理解为什么说 Spring MVC 的 DispatcherServlet 本质上也是一个 Servlet
正文
一、从 HTTP 到 Servlet:一个请求的"旅行"
想象一下这个场景:你在浏览器输入 https://example.com/hello,回车。
背后发生了什么?我们一步步拆解:
第一步:浏览器 → Web 服务器
浏览器根据域名找到对应的服务器 IP,发起一个 HTTP 请求。Web 服务器(比如 Nginx 或 Apache)收到请求后,发现这是一个需要动态处理的请求(而不是静态的 HTML 或图片),于是将请求转发给 Servlet 容器。
第二步:Web 服务器 → Servlet 容器
Servlet 容器(比如 Tomcat)是真正"干活"的地方。它收到请求后,需要做三件事:
- 解析 HTTP 请求 :把原始的 HTTP 报文解析成 Java 对象(
HttpServletRequest) - 找到对应的 Servlet :根据 URL 路径(
/hello)找到匹配的 Servlet - 调用 Servlet :把请求交给 Servlet 处理,然后把响应(
HttpServletResponse)写回浏览器
第三步:Servlet 容器 → Servlet 实例
容器找到对应的 Servlet 后,调用它的 service() 方法。你的业务逻辑就在这个方法(或其衍生方法 doGet/doPost)中执行。
整个流程可以简化为:
浏览器 → Web服务器 → Servlet容器 → Servlet实例 → 你的业务代码
关键认知 :Servlet 不是一个"东西",而是一个"接口" 。它定义了一个 Java 类应该如何与容器协作来处理网络请求。任何实现了 jakarta.servlet.Servlet 接口的类,都可以被 Servlet 容器管理并处理请求。
二、Servlet 接口剖析:五个方法,三个核心
我们来看 Servlet 接口的定义(Jakarta Servlet 6.0):
java
package jakarta.servlet;
public interface Servlet {
// 初始化方法------容器在创建 Servlet 实例后调用
void init(ServletConfig config) throws ServletException;
// 服务方法------容器每次收到请求都会调用
void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
// 销毁方法------容器在卸载 Servlet 前调用
void destroy();
// 获取 Servlet 配置信息
ServletConfig getServletConfig();
// 获取 Servlet 的基本信息(作者、版本等)
String getServletInfo();
}
这五个方法中,init()、service()、destroy() 是生命周期方法,由容器在特定时机调用。
实际开发中,我们几乎不会直接实现 Servlet 接口,而是继承 HttpServlet 抽象类:
Servlet(接口)
↑
GenericServlet(抽象类,实现了 Servlet 接口)
↑
HttpServlet(抽象类,继承了 GenericServlet,增加了 HTTP 协议支持)
↑
你的 Servlet(继承 HttpServlet,重写 doGet/doPost)
HttpServlet 已经帮我们实现了 service() 方法------它会自动判断请求方法是 GET 还是 POST,然后分别调用 doGet() 或 doPost()。所以我们只需要重写这两个方法即可。
三、生命周期详解:一个 Servlet 的"生老病死"
Servlet 的生命周期由 Servlet 容器全权管理。我们来看看每个阶段容器到底做了什么。
阶段一:加载与实例化
容器通过反射 加载 Servlet 类,并调用无参构造方法创建实例。
触发时机(二选一):
- 懒加载(默认) :第一次收到请求时创建
- 预加载 :容器启动时创建(通过
@WebServlet(loadOnStartup = 1)配置)
关键点 :一个 Servlet 类在容器中只有一个实例------这就是所谓的"单例模式"。
阶段二:初始化
实例创建后,容器立刻调用 init(ServletConfig) 方法。
init() 在整个生命周期中只执行一次。你可以在这里做一次性的初始化工作,比如加载配置文件、建立数据库连接池等。
阶段三:服务
每次请求到来,容器都会创建一个新的线程 ,在该线程中调用 service() 方法。
service() 会被调用多次------每收到一次请求就调用一次。
HttpServlet 的 service() 会自动路由到 doGet()/doPost() 等方法。
阶段四:销毁
容器关闭或应用卸载时,容器调用 destroy() 方法。
destroy() 在整个生命周期中只执行一次。你可以在这里释放资源(关闭连接池、保存状态等)。
完整顺序:
容器启动(或首次请求)
↓
【加载】反射创建 Servlet 实例(1次)
↓
【初始化】调用 init()(1次)
↓
【服务】每次请求调用 service()(多次)← 多线程并发执行
↓
容器关闭
↓
【销毁】调用 destroy()(1次)
四、单例多线程模型:一个实例,多个线程
这是 Servlet 最容易被误解的地方,我们单独拿出来说。
核心事实 :一个 Servlet 类只有一个实例,但可以同时服务多个请求。
容器是这样做的:
- 创建一个 Servlet 实例(单例)
- 每个请求到来时,创建一个新的线程
- 所有线程共享同一个 Servlet 实例
- 每个线程独立调用
service()方法
这个设计的好处是:不需要频繁创建销毁对象,性能高。
这个设计的风险 是:如果 Servlet 有成员变量,多个线程同时修改会导致数据错乱。
举个例子:
java
// ❌ 错误:在 Servlet 中定义可变的成员变量
@WebServlet("/counter")
public class CounterServlet extends HttpServlet {
private int count = 0; // 所有线程共享!
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
count++; // 多个线程同时执行,结果不可预测!
resp.getWriter().write("Count: " + count);
}
}
两个用户同时访问,count 的值可能从 1 跳到 3(丢失了一次自增),也可能两个线程都读到同一个值然后各自加 1------你永远不知道结果是什么。
正确做法:
java
// ✅ 正确:使用局部变量
@WebServlet("/counter")
public class CounterServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
int count = 0; // 每个线程独立,互不干扰!
count++;
resp.getWriter().write("Count: " + count);
}
}
或者使用 AtomicInteger 等线程安全的类。
五、从 Servlet 到 Spring MVC:DispatcherServlet 的"继承链"
现在我们来回答一个关键问题:Spring MVC 和 Servlet 到底是什么关系?
答案:DispatcherServlet 就是一个 Servlet。
我们来看 DispatcherServlet 的继承关系:
Servlet(接口)
↑
GenericServlet(抽象类)
↑
HttpServlet(抽象类) ← 这就是我们熟悉的 HttpServlet
↑
HttpServletBean(抽象类) ← Spring 对 HttpServlet 的扩展
↑
FrameworkServlet(抽象类) ← Spring Web 的基类
↑
DispatcherServlet(类) ← Spring MVC 的核心!
DispatcherServlet 直接或间接继承了 HttpServlet,所以它本质上就是一个 Servlet。
那么问题来了:既然 DispatcherServlet 只是一个普通的 Servlet,它凭什么能驱动整个 Spring MVC?
答案是 "委派模式" (Delegation Pattern)。
DispatcherServlet 的 service() 方法(实际是 doDispatch())并不自己处理业务逻辑,而是把工作"委派"给一系列组件:
- HandlerMapping :根据 URL 找到对应的
@Controller方法和拦截器 - HandlerAdapter :适配不同的 Handler 类型(
@Controller、HttpRequestHandler等) - ViewResolver:根据逻辑视图名找到真正的视图
- HandlerExceptionResolver:统一处理异常
这就是"前端控制器模式"(Front Controller Pattern)------一个 Servlet 接收所有请求,然后分发给不同的处理器。
所以你可以这样理解:Servlet 是"地基",Spring MVC 是在地基上盖的"大楼"。没有 Servlet,Spring MVC 连请求都收不到。
代码示例
示例一:手写一个最简单的 Servlet(观察生命周期)
功能 :创建一个 Servlet,在 init()、service()、destroy() 中打印日志,观察生命周期的执行顺序。
环境:Spring Boot 3.x(内置 Tomcat 10.x,支持 Servlet 6.0)
java
package com.example.servletdemo.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
/**
* 最简单的 Servlet------用于演示生命周期方法的调用顺序
*
* @WebServlet 注解替代了传统的 web.xml 配置(Servlet 3.0+ 支持)
* urlPatterns 指定了访问路径:http://localhost:8080/lifecycle
*/
@WebServlet(
name = "LifecycleServlet",
urlPatterns = "/lifecycle",
loadOnStartup = 1 // 设置为1表示容器启动时立即加载和初始化
)
public class LifecycleServlet extends HttpServlet {
/**
* 构造方法:容器通过反射调用
* 注意:每次容器启动只调用一次
*/
public LifecycleServlet() {
System.out.println("[1] 构造方法被调用 ------ Servlet 实例被创建");
}
/**
* init 方法:实例创建后立即调用
* 注意:整个生命周期只调用一次
*/
@Override
public void init() throws ServletException {
System.out.println("[2] init() 被调用 ------ Servlet 初始化");
// 可以在这里做一次性的初始化工作
// 比如:加载配置文件、建立数据库连接池等
}
/**
* service 方法:每次请求都会调用
* HttpServlet 的 service 会自动根据请求方法路由到 doGet/doPost
* 注意:会被多次调用,且在不同线程中并发执行
*/
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
System.out.println("[3] service() 被调用 ------ 处理请求,线程ID: "
+ Thread.currentThread().threadId());
// 调用父类的 service,让它自动路由到 doGet 或 doPost
super.service(req, resp);
}
/**
* doGet 方法:处理 GET 请求
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
System.out.println("[4] doGet() 被调用 ------ 执行业务逻辑");
// 设置响应内容类型
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.println("<html><body>");
out.println("<h1>Hello, Servlet!</h1>");
out.println("<p>访问时间: " + LocalDateTime.now() + "</p>");
out.println("<p>当前线程ID: " + Thread.currentThread().threadId() + "</p>");
out.println("</body></html>");
}
/**
* destroy 方法:容器关闭或应用卸载时调用
* 注意:整个生命周期只调用一次
*/
@Override
public void destroy() {
System.out.println("[5] destroy() 被调用 ------ Servlet 销毁,释放资源");
// 可以在这里释放资源
// 比如:关闭数据库连接池、保存状态等
}
}
如何运行验证:
-
在 Spring Boot 项目中创建这个类(确保项目使用
jakarta.servlet包) -
启动应用,观察控制台输出:
[1] 构造方法被调用 ------ Servlet 实例被创建 [2] init() 被调用 ------ Servlet 初始化 -
在浏览器访问
http://localhost:8080/lifecycle,观察控制台:[3] service() 被调用 ------ 处理请求,线程ID: 28 [4] doGet() 被调用 ------ 执行业务逻辑 -
多次刷新页面,观察
service()被多次调用,且线程 ID 可能不同 -
停止应用,观察控制台:
[5] destroy() 被调用 ------ Servlet 销毁,释放资源
关键观察:
- 构造方法和
init()只执行一次 service()和doGet()每次请求都执行- 如果快速发起多个请求,你会看到
service()被不同线程同时调用
示例二:演示 Servlet 的线程安全问题
功能:创建一个有成员变量的 Servlet,模拟多线程并发访问时的数据错乱。
java
package com.example.servletdemo.servlet;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.concurrent.atomic.AtomicInteger;
@WebServlet("/threadsafe")
public class ThreadSafetyServlet extends HttpServlet {
// ❌ 不安全的成员变量------所有线程共享
private int unsafeCounter = 0;
// ✅ 线程安全的计数器------使用 AtomicInteger
private AtomicInteger safeCounter = new AtomicInteger(0);
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
// ❌ 错误做法:直接修改成员变量
unsafeCounter++; // 这不是原子操作!读取→加1→写入,三步可能被中断
// ✅ 正确做法:使用线程安全的类
int safeValue = safeCounter.incrementAndGet();
// ✅ 正确做法:使用局部变量(每个线程独立)
int localVar = 0;
localVar++;
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.println("<html><body>");
out.println("<h2>Servlet 线程安全演示</h2>");
out.println("<p>❌ 不安全计数器 (成员变量): " + unsafeCounter + "</p>");
out.println("<p>✅ 安全计数器 (AtomicInteger): " + safeValue + "</p>");
out.println("<p>✅ 局部变量 (每次请求独立): " + localVar + "</p>");
out.println("<p>当前线程ID: " + Thread.currentThread().threadId() + "</p>");
out.println("</body></html>");
}
}
如何验证:
- 使用 JMeter 或 Postman 并发发送多个请求
- 观察
unsafeCounter的值是否出现了"跳跃"(比如从 3 直接跳到 5,丢失了 4) - 而
safeCounter始终保持连续
新手错误 vs 正确姿势
| 错误表象 | 根本原因 | 正确姿势 |
|---|---|---|
在 Servlet 中定义了 private int count 成员变量来计数,结果数值总是乱跳 |
未理解 Servlet 是单例多线程,成员变量会被所有线程共享 | 使用局部变量 或 AtomicInteger 等线程安全类 |
| 认为每次请求都会创建新的 Servlet 实例 | 混淆了 Servlet 实例 和 HttpServletRequest 对象的生命周期 | 理解 Servlet 实例只有一个,request 对象每次请求新建 |
在 Servlet 构造方法中做初始化操作,发现某些依赖为 null |
构造方法执行时容器尚未完成 ServletConfig 的注入 | 在 init() 方法中做初始化,此时 ServletConfig 已就绪 |
疑难深度追问
Q1:Servlet 是单例的,那么每次请求的用户数据如何隔离?
通过 HttpServletRequest 对象 。每个请求独立创建一个 HttpServletRequest 实例,它包含了该次请求的所有数据(参数、头信息、Session 等)。这个对象只在当前请求的线程中存活,请求结束后就被回收------所以不存在线程安全问题。
Q2:如果 Servlet 容器(Tomcat)重启,正在处理的请求会怎样?
容器会先调用所有 Servlet 的 destroy() 方法,等待正在执行的 service() 方法完成(有超时机制),然后再关闭容器。这就是所谓的"优雅关闭"(Graceful Shutdown)。如果请求在超时时间内未完成,容器会强制中断。
Q3:@WebServlet 注解中的 loadOnStartup 值越大越好还是越小越好?
值越小,优先级越高 (启动时越先加载)。loadOnStartup = 1 最先加载,loadOnStartup = 2 次之。如果多个 Servlet 之间有依赖关系(比如 A 初始化时需要 B 已经就绪),可以通过设置不同的值来控制加载顺序。
思考与延伸
-
动手验证 :用 JMeter 对
ThreadSafetyServlet发起 100 个并发请求,观察unsafeCounter的最终值是不是 100。如果不是,思考为什么。 -
思考题 :如果在 Servlet 中定义了一个
private static的成员变量,线程安全问题会更严重还是更轻微?为什么? -
延伸思考 :Spring MVC 的
@Controller默认是单例还是多例?如果是单例,为什么我们可以在@Controller中定义成员变量(比如注入的 Service)而不担心线程安全问题?
参考与延伸阅读
- Eclipse Foundation. Jakarta Servlet Specification, Version 6.0. Jakarta EE Specifications, 2022-05-12
- Apache Tomcat. Servlet 6.0 API Documentation. Tomcat 10.1.x
- Spring Framework. DispatcherServlet Documentation. Spring.io
- Baeldung. Introduction to Java Servlets. Baeldung, 2024
- Oracle. Java Servlet Technology. Java EE 8 Tutorial