Servlet 的生命周期由 Servlet 容器(Tomcat、Jetty)管理,主要分为三个阶段:

- 初始化
- 处理请求
- 销毁
下面我们通过一个标准的流程图和详细的代码示例来解释这个过程。
Servlet 生命周期流程图
graph TD
A[客户端请求] --> B{Servlet 是否已加载并初始化?};
B -- 否 --> C[Servlet 容器加载 Servlet 类];
C --> D[调用 init() 方法];
D --> E[Servlet 准备就绪];
B -- 是 --> E;
E --> F[调用 service() 方法];
F --> G[根据请求类型 (GET/POST) 调用 doGet() / doPost()];
G --> H[生成响应];
H --> I[响应返回给客户端];
I --> F;
J[Servlet 容器关闭/应用卸载] --> K[调用 destroy() 方法];
K --> L[Servlet 被垃圾回收];
生命周期详解
初始化
这是 Servlet 生命周期的第一个阶段,并且只执行一次。
-
触发时机:
- 当 Servlet 容器(如 Tomcat)启动时,
<load-on-startup>配置为正数,或者在web.xml中配置了该 Servlet。 - 当客户端第一次请求该 Servlet 时。
- 当 Servlet 容器决定需要预加载该 Servlet 时。
- 当 Servlet 容器(如 Tomcat)启动时,
-
执行方法:
(图片来源网络,侵删)- 容器会创建一个
Servlet实例。 - 然后调用该实例的
init(ServletConfig config)方法。
- 容器会创建一个
-
init()方法的特点:- 只执行一次:在整个 Servlet 生命周期中,
init()方法只被调用一次,这是执行一次性初始化任务(如建立数据库连接、加载配置文件等)的理想位置。 - 线程安全:在
init()方法执行期间,Servlet 容器不会处理对该 Servlet 的任何请求,你不需要担心线程安全问题。 - 参数:该方法接收一个
ServletConfig对象,其中包含了 Servlet 的配置信息,例如初始化参数(<init-param>)。
- 只执行一次:在整个 Servlet 生命周期中,
处理请求
这是 Servlet 生命周期中最重要的阶段,也是最频繁的阶段。
-
触发时机:
当客户端发送一个请求,且该请求的 URL 映射到了这个 Servlet 时。
(图片来源网络,侵删) -
执行方法:
- 容器会调用 Servlet 实例的
service(ServletRequest request, ServletResponse response)方法。 - 注意:我们通常不重写
service()方法,容器会根据 HTTP 请求的 Method(GET, POST, PUT, DELETE 等)来调用相应的doGet(),doPost(),doPut(),doDelete()等方法。
- 容器会调用 Servlet 实例的
-
service()和doGet()/doPost()的特点:- 多线程:对于每一个新的请求,容器通常会在同一个 Servlet 实例上创建一个新的线程来处理,这意味着
doGet(),doPost()等方法必须是线程安全的。 - 如何保证线程安全:
- 避免使用实例变量:不要在 Servlet 类中定义可变的成员变量(除非它们是只读的),如果必须共享数据,可以使用同步代码块(
synchronized),但会降低性能。 - 使用局部变量:在方法内部声明的变量是线程安全的,因为每个线程都有自己的栈空间,局部变量位于栈中,不会互相干扰,这是最推荐的做法。
- 使用线程安全类:如果必须共享数据,可以使用
ConcurrentHashMap,CopyOnWriteArrayList等线程安全的集合类。
- 避免使用实例变量:不要在 Servlet 类中定义可变的成员变量(除非它们是只读的),如果必须共享数据,可以使用同步代码块(
- 高性能:由于只有一个实例,避免了重复创建和销毁对象的开销,使得 Servlet 非常高效。
- 多线程:对于每一个新的请求,容器通常会在同一个 Servlet 实例上创建一个新的线程来处理,这意味着
销毁
这是 Servlet 生命周期的最后一个阶段,同样只执行一次。
-
触发时机:
当 Servlet 容器决定卸载该 Servlet 时(Web 应用被停止或容器关闭时)。
-
执行方法:
- 容器会调用 Servlet 实例的
destroy()方法。
- 容器会调用 Servlet 实例的
-
destroy()方法的特点:- 只执行一次:在
destroy()方法执行后,Servlet 实例将不再被使用,并且会被垃圾回收器回收。 - 清理资源:这是进行资源清理工作的最佳位置,例如关闭数据库连接、停止后台线程、释放文件句柄等。
- 最后的请求:在调用
destroy()之前,容器会确保所有正在处理的请求都已结束或放弃,之后,容器将不再将新的请求传递给该 Servlet。
- 只执行一次:在
代码示例
下面是一个完整的 Servlet 生命周期示例,通过打印日志来清晰地展示各个阶段的执行顺序。
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
// 1. 实现 Servlet 接口
public class LifeCycleServlet implements Servlet {
// ServletConfig 对象,由容器在 init() 时传入
private ServletConfig servletConfig;
// 2. init() 方法 - 初始化阶段
@Override
public void init(ServletConfig config) throws ServletException {
System.out.println("---------- 1. init() 被调用 (只调用一次) ----------");
this.servletConfig = config;
// 在这里进行初始化工作,比如加载配置文件、建立数据库连接等
String initParam = config.getInitParameter("databaseUrl");
System.out.println(" 初始化参数 databaseUrl: " + initParam);
}
// 3. getServletInfo() 方法 - 返回 Servlet 信息
@Override
public String getServletInfo() {
return "这是一个演示 Servlet 生命周期的 Servlet";
}
// 4. service() 方法 - 处理请求 (通常不重写)
@Override
public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
System.out.println("---------- 2. service() 被调用 (每次请求都调用) ----------");
// 获取请求和响应对象
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
// 根据请求方法调用相应的 doXXX 方法
String method = httpServletRequest.getMethod();
if ("GET".equalsIgnoreCase(method)) {
this.doGet(httpServletRequest, httpServletResponse);
} else if ("POST".equalsIgnoreCase(method)) {
this.doPost(httpServletRequest, httpServletResponse);
}
}
// 5. doGet() 方法 - 处理 GET 请求
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println(" --- doGet() 被调用 ---");
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("<h1>这是一个 GET 请求的响应</h1>");
response.getWriter().println("<p>Servlet 实例地址: " + this + "</p>");
}
// 6. doPost() 方法 - 处理 POST 请求
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println(" --- doPost() 被调用 ---");
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("<h1>这是一个 POST 请求的响应</h1>");
response.getWriter().println("<p>Servlet 实例地址: " + this + "</p>");
}
// 7. destroy() 方法 - 销毁阶段
@Override
public void destroy() {
System.out.println("---------- 3. destroy() 被调用 (只调用一次) ----------");
// 在这里进行资源清理工作,比如关闭数据库连接
System.out.println(" 正在清理资源...");
}
// 8. getServletConfig() 方法
@Override
public ServletConfig getServletConfig() {
return this.servletConfig;
}
}
对应的 web.xml 配置 (Servlet 3.0+ 注解方式更常用):
<web-app>
<servlet>
<servlet-name>LifeCycleServlet</servlet-name>
<servlet-class>com.example.LifeCycleServlet</servlet-class>
<!-- 初始化参数 -->
<init-param>
<param-name>databaseUrl</param-name>
<param-value>jdbc:mysql://localhost:3306/mydb</param-value>
</init-param>
<!-- 可选:在容器启动时加载该 Servlet -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>LifeCycleServlet</servlet-name>
<url-pattern>/lifecycle</url-pattern>
</servlet-mapping>
</web-app>
执行顺序分析:
-
启动 Tomcat:如果配置了
<load-on-startup>,你会看到控制台输出:---------- 1. init() 被调用 (只调用一次) ---------- 初始化参数 databaseUrl: jdbc:mysql://localhost:3306/mydb -
第一次访问
http://localhost:8080/your-app/lifecycle:- 如果没预加载,此时会先看到
init()的输出。 - 然后看到:
---------- 2. service() 被调用 (每次请求都调用) ---------- --- doGet() 被调用 ---
- 如果没预加载,此时会先看到
-
第二次访问
http://localhost:8080/your-app/lifecycle:- Servlet 实例已经存在,所以不会再次调用
init()。 - 只会看到:
---------- 2. service() 被调用 (每次请求都调用) ---------- --- doGet() 被调用 --- - 注意:两次请求输出的 Servlet 实例地址是相同的,证明了单例模式。
- Servlet 实例已经存在,所以不会再次调用
-
关闭 Tomcat:你会看到控制台输出:
---------- 3. destroy() 被调用 (只调用一次) ---------- 正在清理资源...
| 阶段 | 方法 | 调用次数 | 作用 | 线程安全 |
|---|---|---|---|---|
| 初始化 | init() |
一次 | 执行一次性初始化任务(如加载资源、建立连接)。 | 是 |
| 处理请求 | service() / doGet()/doPost() |
每次请求 | 处理客户端请求并生成响应。 | 否 (需自行保证) |
| 销毁 | destroy() |
一次 | 释放资源,进行清理工作。 | 是 |
理解这个单例、多线程的生命周期模型是掌握 Servlet 编程的核心。不要在 Servlet 中使用实例变量来存储请求相关的状态,这是最常见的并发错误来源。
