杰瑞科技汇

Servlet生命周期包含哪些关键阶段?

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

Servlet生命周期包含哪些关键阶段?-图1
(图片来源网络,侵删)
  1. 初始化
  2. 处理请求
  3. 销毁

下面我们通过一个标准的流程图和详细的代码示例来解释这个过程。


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 生命周期的第一个阶段,并且只执行一次

  1. 触发时机

    • 当 Servlet 容器(如 Tomcat)启动时,<load-on-startup> 配置为正数,或者在 web.xml 中配置了该 Servlet。
    • 当客户端第一次请求该 Servlet 时。
    • 当 Servlet 容器决定需要预加载该 Servlet 时。
  2. 执行方法

    Servlet生命周期包含哪些关键阶段?-图2
    (图片来源网络,侵删)
    • 容器会创建一个 Servlet 实例。
    • 然后调用该实例的 init(ServletConfig config) 方法。
  3. init() 方法的特点

    • 只执行一次:在整个 Servlet 生命周期中,init() 方法只被调用一次,这是执行一次性初始化任务(如建立数据库连接、加载配置文件等)的理想位置。
    • 线程安全:在 init() 方法执行期间,Servlet 容器不会处理对该 Servlet 的任何请求,你不需要担心线程安全问题。
    • 参数:该方法接收一个 ServletConfig 对象,其中包含了 Servlet 的配置信息,例如初始化参数(<init-param>)。

处理请求

这是 Servlet 生命周期中最重要的阶段,也是最频繁的阶段。

  1. 触发时机

    当客户端发送一个请求,且该请求的 URL 映射到了这个 Servlet 时。

    Servlet生命周期包含哪些关键阶段?-图3
    (图片来源网络,侵删)
  2. 执行方法

    • 容器会调用 Servlet 实例的 service(ServletRequest request, ServletResponse response) 方法。
    • 注意:我们通常不重写 service() 方法,容器会根据 HTTP 请求的 Method(GET, POST, PUT, DELETE 等)来调用相应的 doGet(), doPost(), doPut(), doDelete() 等方法。
  3. service()doGet()/doPost() 的特点

    • 多线程:对于每一个新的请求,容器通常会在同一个 Servlet 实例上创建一个新的线程来处理,这意味着 doGet(), doPost() 等方法必须是线程安全的。
    • 如何保证线程安全
      • 避免使用实例变量:不要在 Servlet 类中定义可变的成员变量(除非它们是只读的),如果必须共享数据,可以使用同步代码块(synchronized),但会降低性能。
      • 使用局部变量:在方法内部声明的变量是线程安全的,因为每个线程都有自己的栈空间,局部变量位于栈中,不会互相干扰,这是最推荐的做法。
      • 使用线程安全类:如果必须共享数据,可以使用 ConcurrentHashMap, CopyOnWriteArrayList 等线程安全的集合类。
    • 高性能:由于只有一个实例,避免了重复创建和销毁对象的开销,使得 Servlet 非常高效。

销毁

这是 Servlet 生命周期的最后一个阶段,同样只执行一次

  1. 触发时机

    当 Servlet 容器决定卸载该 Servlet 时(Web 应用被停止或容器关闭时)。

  2. 执行方法

    • 容器会调用 Servlet 实例的 destroy() 方法。
  3. 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>

执行顺序分析:

  1. 启动 Tomcat:如果配置了 <load-on-startup>,你会看到控制台输出:

    ---------- 1. init() 被调用 (只调用一次) ----------
        初始化参数 databaseUrl: jdbc:mysql://localhost:3306/mydb
  2. 第一次访问 http://localhost:8080/your-app/lifecycle

    • 如果没预加载,此时会先看到 init() 的输出。
    • 然后看到:
      ---------- 2. service() 被调用 (每次请求都调用) ----------
      --- doGet() 被调用 ---
  3. 第二次访问 http://localhost:8080/your-app/lifecycle

    • Servlet 实例已经存在,所以不会再次调用 init()
    • 只会看到:
      ---------- 2. service() 被调用 (每次请求都调用) ----------
      --- doGet() 被调用 ---
    • 注意:两次请求输出的 Servlet 实例地址是相同的,证明了单例模式。
  4. 关闭 Tomcat:你会看到控制台输出:

    ---------- 3. destroy() 被调用 (只调用一次) ----------
        正在清理资源...

阶段 方法 调用次数 作用 线程安全
初始化 init() 一次 执行一次性初始化任务(如加载资源、建立连接)。
处理请求 service() / doGet()/doPost() 每次请求 处理客户端请求并生成响应。 (需自行保证)
销毁 destroy() 一次 释放资源,进行清理工作。

理解这个单例、多线程的生命周期模型是掌握 Servlet 编程的核心。不要在 Servlet 中使用实例变量来存储请求相关的状态,这是最常见的并发错误来源。

分享:
扫描分享到社交APP
上一篇
下一篇