qinghuan
← Java Web / 全部笔记

单元 64 / Java Web

P1 · 重点提升L2 · 进阶

Servlet 生命周期:实例、请求和异步任务何时结束

分清 Servlet 实例和一次请求的存活时间,再检查并发访问、请求再次分派与异步完成的边界。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能解释容器生命周期日志,并为异步请求设计明确完成与超时出口。

学习理由、准备要求与知识点

为什么学它

初始化、请求和异步生命周期不同,处理失误会导致状态串用或资源长期占用。

先读这些内容

知识点清单

  • 加载与 init 时机
  • service/doGet/doPost
  • 实例并发复用
  • 响应编码与局部参数
  • DispatcherType
  • asyncSupported 与 startAsync
  • complete/超时/错误
  • destroy 正常关闭边界

运行前先确认Java 17、Jakarta Servlet 6.0、Tomcat 10.1.44。源码定位 GenericServlet 的初始化回调;算法是保守的请求生命周期与优雅关闭模型,省略容器超时强制策略、实际线程和网络,只验证状态转换。

01先弄懂它是什么

Servlet 什么时候创建,和每次请求什么时候开始,是两回事。容器创建并初始化一个 Servlet 后,可能让多个请求同时调用它,停止使用时再执行销毁回调。init 通常不是每个请求都跑,destroy 也不是用户关闭浏览器时就跑。初始化可能在启动时或首次访问时发生,应用进程启动了,不等于所有 Servlet 都已创建。

每次请求有自己的参数、属性和响应,Servlet 实例却可以被复用。适合共享且已有正确并发保护的资源可以放在实例中;当前用户、查询条件和本次临时结果,应放在请求或方法范围。处理完把成员字段清零,仍挡不住另一个请求同时覆盖它;要先确定数据属于谁,而不是事后清空。

异步 Servlet 允许最初处理请求的线程先返回,请求本身继续等待后面的工作。容器用 AsyncContext 协调完成、错误、超时,以及把请求再次交给处理链。相关 Servlet 和 Filter 必须满足异步支持条件,不能随意把任意过滤器加入这条链。也要分清三个时刻:最初线程返回、后台任务做完、整个响应结束。它们不一定相同,请求对象更不能因此无限期保留使用。

文中这几个词,先认一认

实例生命周期
一个 Servlet 从创建、初始化、处理许多请求,到最终销毁的整个过程。
分派类型
请求这次是普通访问、转发、错误处理还是异步再次分派,会影响哪些 Filter 被执行。
异步周期
从 startAsync 开始,由容器管理完成、超时或再次分派的过程,不是无限存活的后台任务。
优雅关闭
停止接收新任务,并给已开始任务有限时间完成;并不保证突然故障时也一定能清理。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

初始化先建立实例的容器配置,再进入应用扩展回调。之后 service 根据请求类型进入 doGet、doPost 等处理。正常同步请求返回时,容器可以结束这一请求;异步已启动时,初始调用返回只是释放当前处理栈,请求仍等待 complete、超时、错误或后续分派。

初始 service 还没有返回时,异步任务就可能调用 complete,要求结束请求。但提出完成要求,不等于容器马上结束响应,仍要遵守请求的生命周期规则。因此模型把“已要求完成”和“已经完成”分开记录:当前处理还在运行时先记下标志,等它返回后再结束,避免把还在使用的对象提前当成可以回收。

再分派会让请求重新进入相关处理链,过滤器是否再运行取决于配置的分派类型。同步 Filter 的 finally 可能已经执行,不能继续把它当作异步请求最终清理时刻。业务关键数据应在正常事务和可靠流程中保存,而不是只寄希望于 destroy;强制终止进程时该回调并无必然执行保证。

正在加载流程图…

图中的请求状态与实例状态分开理解。教学算法只接受在途请求全部结束后的销毁,真实容器还要按规范和配置处理超时。
查看流程图源码(Mermaid)
stateDiagram-v2
 [*] --> Ready: "初始化完成"
 Ready --> Service: "接受请求"
 Service --> Done: "同步返回"
 Service --> AsyncWait: "启动异步并返回"
 AsyncWait --> Service: "异步再分派"
 AsyncWait --> Done: "完成或终止"
 Done --> [*]
 state "实例停止接收新请求后,等待在途请求或执行超时策略" as Note

按顺序看,每一步做了什么

  1. 区分实例 init/destroy 与每次请求 service。
  2. 同步请求状态只属于本次请求,不能泄漏到共享成员。
  3. 启动异步后显式管理完成、错误、超时和再分派。
  4. 停止服务先控制新流量,再处理在途请求与资源释放。

03对照实现代码

GenericServlet 先保存 ServletConfig 再调用无参 init (新标签页)↗

Apache Tomcat 10.1.44,Jakarta Servlet 6.0 API 实现 · jakarta.servlet.GenericServlet#init(ServletConfig)、init()、destroy()

已核验真实类和回调关系。下面是原创初始化顺序模型,使用普通 Map 代替 ServletConfig,并额外加入可观察状态;它不实现容器线程管理、完整 Servlet 接口或生命周期强制规则。

核心机制代码 · java
import java.util.Map;
class SourceModel {
  private Map<String,String> config;
  final void initialize(Map<String,String> incoming) {
    config=Map.copyOf(incoming); // 配置先可见,扩展回调才能读取
    onInit(); // 对照 GenericServlet 的无参初始化扩展点
  }
  protected void onInit() { /* 子类只负责自身初始化工作 */ }
  protected String parameter(String key) {
    if(config==null) throw new IllegalStateException("尚未初始化");
    return config.get(key);
  }
  void destroy() { /* 此处不是关键数据的唯一保存入口 */ }
}

GenericServlet 的带配置 init 保存 ServletConfig,随后调用无参 init。对子类而言,覆盖无参方法可以在容器配置就绪后完成初始化;如果自行覆盖带配置方法却遗漏必要的父类初始化,后续读取上下文或初始化参数可能失去预期基础。

对象先经过构造,之后容器才提供配置并调用 init,所以不能认为构造器里已经能拿到 init 阶段的配置。依赖注入框架也可能把创建对象和准备配置分成不同步骤,但具体顺序与回调由各自的规则决定,不能看见某个框架注解,就推断 Servlet 也会按同样方式执行。

GenericServlet 提供接口实现基础和可重写位置,真正何时创建、并发调用 service、卸载,由容器安排。教学模型没有用一个布尔值就实现完整线程安全生命周期。共享资源怎样安全交给其他线程,以及何时关闭,仍需同时遵守容器与应用的同步规则。

destroy 的默认扩展点不意味着任何进程退出方式都会调用它。正常卸载、超时、系统崩溃和强制终止不同;清理适合释放可重建资源,关键业务持久性应由事务、日志或消息协议承担。源码中方法存在,只证明提供了回调,不证明所有故障都能到达它。

代码先调用谁,再调用谁

  1. 容器创建 Servlet 实例并准备 ServletConfig。
  2. GenericServlet.init(ServletConfig) 保存配置。
  3. 无参 init 进入应用初始化逻辑。
  4. 容器随后调用 service,并在受控卸载过程中处理 destroy。

04动手写一个例子

请求异步周期与优雅关闭的合法状态转换

离线模拟一个实例:初始化后才能接受请求;请求可以同步返回、启动异步后返回、请求完成或异步再分派。开始关闭后拒绝新请求,只有所有在途请求结束才能销毁。complete 在 service 尚未返回时只记标志,leave 后才释放该请求。

输入 1
init,enter,leave
输出 1
在途请求为0
输入 2
init,enter,async,leave
输出 2
仍有1个在途请求
输入 3
enter,async,complete,leave
输出 3
complete 时仍在途,leave 后结束
输入 4
存在在途请求时 destroy
输出 4
IllegalStateException

实例保存 READY 等状态,请求另存 inService、async 和 completionRequested。把这两组状态分开,可以表达实例正在停止但旧请求仍允许完成,避免只用一个 running 布尔值混淆全部情况。

请求 leave 时,如果没有异步周期或已经请求完成,就从在途表移除;否则保留。异步 complete 在处理栈外可以立即结束,在栈内则推迟到 leave。再分派要求旧异步处理栈已返回,并重新进入服务状态。

每个操作都先检查合法前置状态,错误序列不能悄悄推进。main 验证同步、异步、提前请求完成、再分派、关闭等待和初始化前访问。这是确定性状态验证,不测试真实容器线程调度或超时精度。

为什么这样写是对的在途表中的每个请求尚未完成;销毁只发生在停止接收且在途表为空时;同一请求的处理栈不能被重复进入或重复退出。

时间 每个事件期望 O(1)额外空间 O(r)

哈希表保存 r 个在途请求,每个事件进行常数次检查和变更。真实回调、网络写入和业务计算不包含在模型内。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 47 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
  enum Life { NEW,READY,STOPPING,DESTROYED }
  static class Request { boolean inService=true,async=false,completionRequested=false; }
  static class Server {
    Life life=Life.NEW; final Map<Integer,Request> live=new HashMap<>();
    void need(boolean ok) { if(!ok) throw new IllegalStateException(); }
    Request request(int id) { Request r=live.get(id); need(r!=null); return r; }
    void init() { need(life==Life.NEW); life=Life.READY; }
    void enter(int id) { need(life==Life.READY && !live.containsKey(id)); live.put(id,new Request()); }
    void async(int id) { Request r=request(id); need(r.inService && !r.async); r.async=true; }
    void leave(int id) {
      Request r=request(id); need(r.inService); r.inService=false;
      if(!r.async || r.completionRequested) live.remove(id); // 同步或已请求完成才结束
    }
    void complete(int id) {
      Request r=request(id); need(r.async && !r.completionRequested);
      r.completionRequested=true;
      if(!r.inService) live.remove(id); // 栈内完成先记标志,不能提前释放
    }
    void dispatch(int id) {
      Request r=request(id); need(r.async && !r.inService && !r.completionRequested);
      r.inService=true; r.async=false; // 新的服务调用可再次启动异步周期
    }
    void stop() { need(life==Life.READY); life=Life.STOPPING; }
    void destroy() { need(life==Life.STOPPING && live.isEmpty()); life=Life.DESTROYED; }
  }
  static void check(boolean ok) { if(!ok) throw new AssertionError(); }
  static void fails(Runnable action) {
    try { action.run(); throw new AssertionError("非法状态应被拒绝"); }
    catch(IllegalStateException expected) { /* 期望状态错误 */ }
  }
  static Server ready() { Server s=new Server(); s.init(); return s; }
  public static void main(String[] args) {
    Server sync=ready(); sync.enter(1); sync.leave(1); check(sync.live.isEmpty());
    Server async=ready(); async.enter(1); async.async(1); async.leave(1); check(async.live.size()==1);
    async.complete(1); check(async.live.isEmpty());
    Server early=ready(); early.enter(1); early.async(1); early.complete(1); check(early.live.size()==1);
    early.leave(1); check(early.live.isEmpty());
    Server dispatch=ready(); dispatch.enter(1); dispatch.async(1); dispatch.leave(1);
    dispatch.dispatch(1); dispatch.leave(1); check(dispatch.live.isEmpty());
    Server stop=ready(); stop.enter(1); stop.stop(); fails(stop::destroy); fails(()->stop.enter(2));
    stop.leave(1); stop.destroy(); check(stop.life==Life.DESTROYED);
    fails(()->new Server().enter(1)); fails(sync::init);
    System.out.println("PASS 11 lifecycle assertions");
  }
}

这些特殊情况也要试一试

  • 初始 service 未返回时请求 complete,模型延迟结束到 leave。
  • 停止接收不等于立刻删除在途请求。
  • 真实容器可能在配置超时后执行终止策略,不能从本模型推出无限等待。
进阶选读 · 生产拓展:超时、完成与再分派竞争

场景正常任务完成、超时回调和连接错误几乎同时发生,多个路径都想写响应或释放资源。

做法定义单一终结状态与原子竞争,胜出路径负责一次性资源回收;其他路径只记录或取消剩余工作。处理器与外部任务的取消协议分别管理。

复杂度每次终结竞争可用常数次原子状态转换,但被取消任务的清理成本取决于资源与任务数量。

好处与代价严格一次清理减少重复释放,却增加状态与回调组织复杂度;简单同步栈模型无法直接覆盖这些竞争。

05项目里怎么用

异步机制的收益通常来自等待期间释放处理线程,而不是凭空减少业务工作量。若仍把阻塞任务无限提交给无界线程池,压力只从容器线程转移到另一个队列。需要把在途请求数、队列容量和依赖连接容量一起估算。

分派配置影响过滤器运行次数,尤其是错误与异步再分派。关联日志应标明请求和分派阶段,避免把一次请求的多个阶段误算成多个独立业务。线程上下文在跨线程后需要受控传递与清理,不能保留整份请求到无限期任务。

关闭时先停止新请求,再给正在处理的工作有限时间完成或取消,时间要与部署平台允许的期限一致。数据库提交和消息确认平时就应按可靠流程处理,不能等 destroy 时集中补写。强制终止可能根本不会运行清理,因此故障恢复不能只依赖关闭回调。

容易踩的坑

  • 把 init 当作每次请求的初始化位置。
  • 在共享实例字段存储当前请求数据。
  • 启动异步后没有完成、错误或超时出口。
  • 把 destroy 回调当作任何故障下必然执行的持久化保障。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
同步请求处理调用栈与资源范围直观。等待期间占用处理线程。耗时短且并发规模可控的接口。
Servlet 异步等待期间可释放初始处理线程。完成、分派、超时和上下文管理更复杂。大量等待型请求并有明确容量规划。
后台任务加状态查询请求可以快速确认受理,任务生命周期更独立。需要持久任务状态、幂等与结果查询。执行时间远长于正常 HTTP 请求预算的工作。

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01init 为什么不能保存当前请求用户?

展开参考答案与推导

它属于实例初始化阶段,通常没有某次用户请求,而且实例会被许多请求复用。当前用户必须属于请求或受控上下文。

02service 返回为什么不一定代表响应已完成?

展开参考答案与推导

启动 Servlet 异步后,请求可在初始调用返回后继续存在,由异步完成、错误、超时或再分派机制管理。

03异步是否自动使所有过滤器都能参与?

展开参考答案与推导

不是。Servlet 与相关过滤器链需要支持异步,并按分派类型配置相应行为;具体调用次数和清理时机由契约决定。

换个情况,还会处理吗

01发布后长请求被截断,如何排查?

展开参考答案与推导

对齐负载均衡摘流量、平台终止宽限期、容器关闭与业务超时,观察在途请求及任务取消。不能仅凭 destroy 日志出现就认为所有工作完成。

02异步请求日志显示两个入口,是否重复下单?

展开参考答案与推导

先区分初始请求与异步再分派,核对同一业务 ID 是否执行了重复副作用。入口次数与业务执行次数不能直接等同。

03任务完成与超时同时发生,应如何设计?

展开参考答案与推导

由明确的终结状态竞争决定谁可写最终结果和释放资源,其他路径不得重复提交;外部副作用还需单独幂等。

写代码试一试

01验证 complete 在 service 内调用的延迟终结。

输入 / 条件
enter,async,complete
输出 / 预期
leave 前仍在途

提示 完成请求与真正终结分开。

展开参考答案与推导

记录 completionRequested,处理栈仍活跃时不移除对象;leave 再检查标志并结束。

02为关闭模型增加强制超时状态。

输入 / 条件
stop 后仍有请求,宽限期到期
输出 / 预期
进入明确强制终止或失败状态

提示 不能假装请求已经成功。

展开参考答案与推导

增加超时事件与终结原因,逐个标记未完成请求并触发受控取消,记录哪些业务结果未知,而非直接清空表后报告成功。

03如何拒绝重复 leave?

输入 / 条件
enter,leave,leave
输出 / 预期
第二次抛状态错误

提示 只有活跃处理栈可退出。

展开参考答案与推导

首次 leave 已完成请求或清除 inService,第二次无法满足前置条件。对异步保留请求也同样检查 inService,不能仅看 ID 是否存在。

读完之后,独立完成

课后作业:自己做一遍

本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能解释容器生命周期日志,并为异步请求设计明确完成与超时出口。

本篇重点加练

重点加练多次分发与异步终结,避免把一次入口调用等同完整请求生命周期。

  • DispatcherType
  • complete/超时/错误

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    为 Servlet 的加载、初始化、处理请求和正常销毁画生命周期图;标出哪些事件按实例发生、哪些按请求发生,并说明启动时尚不能知道的请求信息。

    提交内容
    生命周期图和事件作用域表。
    怎样算完成
    • 实例与请求阶段明确
    • 加载时机有容器配置前提

    练到的知识点加载与 init 时机 · service/doGet/doPost

  2. 作业 02排错实验

    本地 Servlet 返回中文及请求参数,用两个客户端并发请求不同名字;记录实例标识、线程和响应编码设置时机,检查是否出现交叉内容。

    提交内容
    练习代码、自写并发/空参数/中文测试与运行证据。
    怎样算完成
    • 请求数据不共享到实例字段
    • 实际响应字节可验证

    练到的知识点实例并发复用 · 响应编码与局部参数

综合应用

  1. 作业 03代码推演

    同一请求先经过 REQUEST 分发,再启动异步并重新分发。画出 Filter 与 Servlet 的参与条件,针对 asyncSupported 的不同配置预测并验证可用性。

    提交内容
    配置矩阵、事件轨迹与错误记录。
    怎样算完成
    • 异步支持沿整条链核验
    • 重复进入与重复业务执行分开

    练到的知识点DispatcherType · asyncSupported 与 startAsync

  2. 作业 04面试场景

    后台任务完成、异步超时和应用正常关闭几乎同时发生。规定最终响应、资源释放和任务取消的责任,列出必须防止的重复终结行为。

    提交内容
    竞态时间线与终结状态合同。
    怎样算完成
    • 每种终结入口都有责任人
    • 正常关闭承诺不延伸到强制终止

    练到的知识点complete/超时/错误 · destroy 正常关闭边界

  3. 作业 05编程实践

    实现一个最多延迟 200ms 的本地异步端点,支持成功和注入失败两种任务;设置有界执行器和超时,测试结果完成后活跃任务计数。

    提交内容
    Servlet 工程、自写成功/失败/超时测试和运行证据。
    怎样算完成
    • 响应只按合同完成一次
    • 后台任务和资源可回收
    • 测试无需无限等待

    练到的知识点DispatcherType · asyncSupported 与 startAsync · complete/超时/错误

  4. 作业 06编程实践

    实现本地事件记录器跟踪异步请求的分发类型和最终完成事件,支持任务成功、任务失败与超时;使用唯一请求标识串联全部事件。

    提交内容
    Servlet 实验、自写事件顺序/终结次数测试和运行证据。
    怎样算完成
    • 每次分发均能关联同一请求
    • 完成状态与入口返回分开

    练到的知识点DispatcherType · complete/超时/错误

  5. 作业 07代码推演

    一次请求发生 forward 后再进入错误分发,过滤器只配置 REQUEST。列出哪些事件需要由容器配置确认,再根据实际日志检查参与范围。

    提交内容
    分发图、配置与观测矩阵。
    怎样算完成
    • 配置条件完整
    • 不按方法调用直觉猜分发行为

    练到的知识点DispatcherType

  6. 作业 08设计评审

    异步任务超时后仍可能完成计算并尝试写响应。为任务取消、结果丢弃和资源收尾设定责任,不依赖每次取消都立即生效。

    提交内容
    终结状态合同与竞态事件表。
    怎样算完成
    • 迟到结果有明确政策
    • 资源释放可独立验证

    练到的知识点complete/超时/错误

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    完成服务发布基础后:停止接收新请求后需给在途请求最多 5 秒收尾。设计正常结束、强制预算到期和再次关闭的行为。

    提交内容
    关闭状态机、资源清单和故障演练计划。
    怎样算完成
    • 关闭预算有限
    • 业务提交与响应完成分别核验

    练到的知识点实例并发复用 · complete/超时/错误 · destroy 正常关闭边界

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 条件变化

    请求增加一次异步重新分发,旧实现依靠进入 Filter 的次数统计访问量。分析指标含义如何改变,设计既能统计请求又能统计分发的记录模型。

    提交内容
    指标定义、事件样本和计数验收。
    怎样算完成
    • 统计单位明确
    • 同请求多事件可去歧义

    练到的知识点DispatcherType · complete/超时/错误

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗