读完这篇,你能做什么
能解释容器生命周期日志,并为异步请求设计明确完成与超时出口。
学习理由、准备要求与知识点
为什么学它
初始化、请求和异步生命周期不同,处理失误会导致状态串用或资源长期占用。
先读这些内容
知识点清单
- 加载与 init 时机
- service
/doGet/doPost - 实例并发复用
- 响应编码与局部参数
DispatcherTypeasyncSupported与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按顺序看,每一步做了什么
- 区分实例 init
/destroy与每次请求 service。 - 同步请求状态只属于本次请求,不能泄漏到共享成员。
- 启动异步后显式管理完成、错误、超时和再分派。
- 停止服务先控制新流量,再处理在途请求与资源释放。
03对照实现代码
Apache Tomcat 10.1.44,Jakarta Servlet 6.0 API 实现 · jakarta.servlet.GenericServlet#init(ServletConfig)、init()、destroy()
已核验真实类和回调关系。下面是原创初始化顺序模型,使用普通 Map 代替 ServletConfig,并额外加入可观察状态;它不实现容器线程管理、完整 Servlet 接口或生命周期强制规则。
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 的默认扩展点不意味着任何进程退出方式都会调用它。正常卸载、超时、系统崩溃和强制终止不同;清理适合释放可重建资源,关键业务持久性应由事务、日志或消息协议承担。源码中方法存在,只证明提供了回调,不证明所有故障都能到达它。
代码先调用谁,再调用谁
- 容器创建 Servlet 实例并准备
ServletConfig。 GenericServlet.init(ServletConfig)保存配置。- 无参 init 进入应用初始化逻辑。
- 容器随后调用 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 验证同步、异步、提前请求完成、再分派、关闭等待和初始化前访问。这是确定性状态验证,不测试真实容器线程调度或超时精度。
为什么这样写是对的在途表中的每个请求尚未完成;销毁只发生在停止接收且在途表为空时;同一请求的处理栈不能被重复进入或重复退出。
哈希表保存 r 个在途请求,每个事件进行常数次检查和变更。真实回调、网络写入和业务计算不包含在模型内。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
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/超时/错误
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
为 Servlet 的加载、初始化、处理请求和正常销毁画生命周期图;标出哪些事件按实例发生、哪些按请求发生,并说明启动时尚不能知道的请求信息。
- 提交内容
- 生命周期图和事件作用域表。
- 怎样算完成
- 实例与请求阶段明确
- 加载时机有容器配置前提
练到的知识点加载与 init 时机 · service/doGet/doPost
- 作业 02排错实验
本地 Servlet 返回中文及请求参数,用两个客户端并发请求不同名字;记录实例标识、线程和响应编码设置时机,检查是否出现交叉内容。
- 提交内容
- 练习代码、自写并发/空参数/中文测试与运行证据。
- 怎样算完成
- 请求数据不共享到实例字段
- 实际响应字节可验证
练到的知识点实例并发复用 · 响应编码与局部参数
综合应用
- 作业 03代码推演
同一请求先经过 REQUEST 分发,再启动异步并重新分发。画出 Filter 与 Servlet 的参与条件,针对
asyncSupported的不同配置预测并验证可用性。- 提交内容
- 配置矩阵、事件轨迹与错误记录。
- 怎样算完成
- 异步支持沿整条链核验
- 重复进入与重复业务执行分开
练到的知识点DispatcherType · asyncSupported 与 startAsync
- 作业 04面试场景
后台任务完成、异步超时和应用正常关闭几乎同时发生。规定最终响应、资源释放和任务取消的责任,列出必须防止的重复终结行为。
- 提交内容
- 竞态时间线与终结状态合同。
- 怎样算完成
- 每种终结入口都有责任人
- 正常关闭承诺不延伸到强制终止
练到的知识点complete/超时/错误 · destroy 正常关闭边界
- 作业 05编程实践
实现一个最多延迟 200ms 的本地异步端点,支持成功和注入失败两种任务;设置有界执行器和超时,测试结果完成后活跃任务计数。
- 提交内容
- Servlet 工程、自写成功/失败/超时测试和运行证据。
- 怎样算完成
- 响应只按合同完成一次
- 后台任务和资源可回收
- 测试无需无限等待
练到的知识点DispatcherType · asyncSupported 与 startAsync · complete/超时/错误
- 作业 06编程实践
实现本地事件记录器跟踪异步请求的分发类型和最终完成事件,支持任务成功、任务失败与超时;使用唯一请求标识串联全部事件。
- 提交内容
- Servlet 实验、自写事件顺序/终结次数测试和运行证据。
- 怎样算完成
- 每次分发均能关联同一请求
- 完成状态与入口返回分开
练到的知识点DispatcherType · complete/超时/错误
- 作业 07代码推演
一次请求发生 forward 后再进入错误分发,过滤器只配置 REQUEST。列出哪些事件需要由容器配置确认,再根据实际日志检查参与范围。
- 提交内容
- 分发图、配置与观测矩阵。
- 怎样算完成
- 配置条件完整
- 不按方法调用直觉猜分发行为
练到的知识点DispatcherType
- 作业 08设计评审
异步任务超时后仍可能完成计算并尝试写响应。为任务取消、结果丢弃和资源收尾设定责任,不依赖每次取消都立即生效。
- 提交内容
- 终结状态合同与竞态事件表。
- 怎样算完成
- 迟到结果有明确政策
- 资源释放可独立验证
练到的知识点complete/超时/错误
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
完成服务发布基础后:停止接收新请求后需给在途请求最多 5 秒收尾。设计正常结束、强制预算到期和再次关闭的行为。
- 提交内容
- 关闭状态机、资源清单和故障演练计划。
- 怎样算完成
- 关闭预算有限
- 业务提交与响应完成分别核验
练到的知识点实例并发复用 · complete/超时/错误 · destroy 正常关闭边界
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 条件变化
请求增加一次异步重新分发,旧实现依靠进入 Filter 的次数统计访问量。分析指标含义如何改变,设计既能统计请求又能统计分发的记录模型。
- 提交内容
- 指标定义、事件样本和计数验收。
- 怎样算完成
- 统计单位明确
- 同请求多事件可去歧义
练到的知识点DispatcherType · complete/超时/错误
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读规范英文
Jakarta Servlet 6.0 规范 (新标签页)↗
阅读生命周期、并发请求、分派类型和异步处理;用于 Jakarta 命名空间课程。
- 实践参考文档英文
Spring MVC:DispatcherServlet (新标签页)↗
阅读前端控制器与处理器查找流程,连接容器、Filter 和 Controller。
- 深入参考文档英文
Spring Framework:Bean 作用域 (新标签页)↗
比较 singleton、prototype 与请求作用域,检查对象状态和销毁责任。
- 深入参考文档英文
Jakarta Servlet 6.0 Specification (新标签页)↗
阅读初始化、并发 service、异步处理与关闭边界。
- 深入参考文档英文
Tomcat 10.1.44 GenericServlet (新标签页)↗
核对配置保存与无参 init 的调用顺序。
- 深入参考文档英文
Tomcat 10.1.44 HttpServlet (新标签页)↗
阅读 HTTP 方法到具体处理方法的分派。
- 深入参考文档英文
Tomcat ApplicationFilterChain (新标签页)↗
查看过滤链对
asyncSupported条件的处理。