一、原理剖析:像树一样生长的 "生命周期"

2001 年第一次在科研所调试 Servlet 时,我盯着 Tomcat 日志看了一下午:OrderServlet init→service invoked→destroyed,突然意识到这像极了院子里那棵梧桐树 —— 春天发芽(初始化),夏天舒展枝叶承接阳光雨露(处理请求),秋天落叶前把养分收回根部(资源清理),整个过程有严格的时序逻辑。

  • 加载与实例化:服务器启动时,Tomcat 的类加载器会像 "园丁栽树" 一样,把 Servlet 类从 WAR 包中加载到 JVM,再 new 出唯一实例(单例模式的核心)。这个过程就像种子破土,一个 Servlet 类只会长出 "一棵树",不会有两棵一模一样的。
  • init ():扎根生长

实例化后,init () 会被调用且仅调用一次。它像树木扎根,负责初始化资源:比如电商 Servlet 初始化数据库连接池,OA 系统 Servlet 加载权限配置文件。你可以通过@WebServlet(loadOnStartup=1)指定初始化时机 —— 数值越小启动越早,就像园丁会先栽果树再栽花草。

  • service ():枝叶承雨

每次收到 HTTP 请求,服务器会分配一个线程调用 service (),就像每滴雨水落到叶片上都会被处理。它的核心逻辑是 "请求分诊":若请求是 GET 就转发到 doGet (),POST 就转发到 doPost (),堪称 Web 世界的 "急诊分诊台"。

  • destroy ():落叶归根

服务器关闭前,destroy () 会被调用,负责释放资源:关闭数据库连接、保存缓存数据到磁盘。就像深秋的树会把叶片养分收回树干,避免寒冬里的资源浪费。

二、代码示例:电商订单 Servlet 的 "一生"

@WebServlet(

urlPatterns = "/order",

loadOnStartup = 2 // 启动顺序:数值越小越先初始化

)

public class OrderServlet extends HttpServlet {

private ConnectionPool pool; // 共享连接池(单例资源)

private Logger logger;

// 1. 初始化:服务器启动后执行(仅一次)

@Override

public void init(ServletConfig config) throws ServletException {

super.init(config);

// 初始化连接池(类似树木扎根吸收养分)

pool = new ConnectionPool(config.getInitParameter("dbUrl"));

logger = Logger.getLogger(OrderServlet.class);

logger.info("订单Servlet初始化完成,连接池容量:" + pool.size());

}

// 2. 处理请求:每次请求都会触发(多线程)

@Override

protected void service(HttpServletRequest req, HttpServletResponse resp)

throws ServletException, IOException {

// 通用逻辑:记录请求日志(所有请求都要走的"分诊台")

logger.info("收到" + req.getRemoteAddr() + "的" + req.getMethod() + "请求");

// 自动分发到doGet/doPost(根据请求类型"分流")

super.service(req, resp);

}

// 处理订单查询(GET请求)

@Override

protected void doGet(HttpServletRequest req, HttpServletResponse resp) {

String orderId = req.getParameter("id");

// 用共享连接池查询数据(多线程共享同一资源)

try (Connection conn = pool.getConnection()) {

Order order = new OrderDao(conn).queryById(orderId);

// 返回订单信息...

} catch (SQLException e) {

logger.error("查询订单失败", e);

}

}

// 处理创建订单(POST请求)

@Override

protected void doPost(HttpServletRequest req, HttpServletResponse resp) {

// 解析表单数据并创建订单...

}

// 3. 销毁:服务器关闭前执行(仅一次)

@Override

public void destroy() {

// 关闭连接池(类似树木落叶前回收资源)

pool.close();

logger.info("订单Servlet销毁,释放连接池资源");

}

}
三、实战场景:OA 系统的生命周期踩坑记

2009 年给某银行做 OA 系统时,遇到一个经典问题:公文审批 Servlet 在服务器启动时总是初始化失败。排查后发现,init () 方法里调用了还未初始化的权限 Servlet—— 就像一棵树苗想从还没扎根的另一棵树上吸收养分,自然会失败。

解决办法是调整loadOnStartup值:把权限 Servlet 设为 1(先启动),公文 Servlet 设为 2(后启动),确保 "养分源" 先就绪。

另一个场景是销毁阶段:有个同事开发的日志 Servlet,在 destroy () 里忘了关闭日志文件流,导致服务器重启时提示 "文件被占用"。后来我们在团队里定下规矩:所有资源释放必须写在finally块里,就像离开办公室前一定要锁门。

四、常见陷阱:单例多线程的 "暗礁"

Servlet 的单例多线程特性,是新手最容易翻船的地方,我见过三个典型案例:

成员变量污染

有个电商项目在 Servlet 里定义private Order currentOrder;,结果并发下单时,A 用户的订单信息跑到了 B 用户的页面上。这就像多个顾客共用一个购物车,东西肯定会混在一起。

解法:用局部变量存储请求相关数据,每个线程单独持有。

初始化逻辑过重

某 OA 系统的公告 Servlet 在 init () 里加载了近万条历史公告,导致服务器启动时间从 30 秒变成 5 分钟。这就像给树苗一次性浇 100 升水,反而会烂根。

解法:非紧急数据采用懒加载(第一次请求时再加载),或用异步线程初始化。

destroy () 执行时机问题

服务器突然断电时,destroy () 不会被调用,导致未保存的临时数据丢失。就像暴风雨突然来临,树叶来不及回收养分就被打落。

解法:关键数据定时持久化,不依赖 destroy () 做最终保存。

五、最佳实践:让生命周期 "优雅运转"

初始化策略

  • 核心服务(支付、登录)用loadOnStartup=1~10,确保优先启动;
  • 非核心服务(统计、日志)用loadOnStartup=-1(默认),第一次请求时再初始化,减轻启动压力。

线程安全保障

  • 禁止在 Servlet 中定义可变成员变量(如private int count);
  • 若需共享资源(如连接池),使用线程安全的实现(如ConcurrentHashMap);
  • 复杂业务逻辑抽离到 Service 层,Servlet 只做请求分发,避免 "业务代码堆在生命周期方法里"。

销毁机制

  • 资源释放按 "先创建后销毁" 的顺序(如先关连接池,再关日志),避免依赖错误;
  • 在 Tomcat 的server.xml中配置unloadDelay="30000",给 destroy () 留足 30 秒执行时间。

异步处理优化

对耗时操作(如下载大报表),用 Servlet 3.0 的异步特性:

@Override

protected void doGet(HttpServletRequest req, HttpServletResponse resp) {

AsyncContext async = req.startAsync(); // 开启异步

async.start(() -> { // 用新线程处理,不阻塞原请求线程

generateLargeReport(async); // 生成报表

async.complete(); // 完成异步处理

});

}

这就像餐厅接到大单时,前台先开单,后厨慢慢做,前台继续接待新客人。

六、设计哲学:单例与线程安全的平衡术

Servlet 的生命周期设计,藏着早期 Java 工程师的智慧:单例模式减少内存占用(一个类一个实例),多线程处理提升并发效率,但必须由开发者自己保证线程安全。这种 "框架定规则,开发者填细节" 的模式,后来被 Spring 等框架继承 —— 比如 Spring Bean 默认单例,也面临同样的线程安全问题。

就像老北京四合院的设计:一个院子(Servlet 实例)住多户人家(线程),院子公共区域(成员变量)需要大家共同维护,各自房间(局部变量)则可随意使用。理解这种设计,不仅能写好 Servlet,更能明白 Java Web 框架的底层逻辑。

最后小结:

如今的Spring Boot 了,在DispatcherServlet源码,里面赫然有initStrateges()和destroy()方法 —— 原来再先进的框架,也逃不开 "初始化 - 服务 - 销毁" 的生命周期逻辑。技术在迭代,但核心规律从未变过。就像我们老程序员,从手写 Servlet 到用云原生框架,变的是工具,不变的是对 "系统如何运转" 的追问。理解生命周期,其实是理解软件从诞生到消亡的本质。

Logo

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

更多推荐