Bean 已经创建,工作线程是否已经可用

订单服务依赖一个后台通知 Worker。Worker 对象构造成功,并不意味着线程池已经启动;close 日志出现,也不意味着任务线程已经结束。对象存在、组件运行、资源终止是三个状态,需要各自的入口和证据。

Spring 的初始化回调处理对象准备,Lifecycle 提供显式 start/stop,SmartLifecycle 再增加自动启动、phase 和带完成回调的停止契约。把业务启动放在哪一层,会改变失败时点和依赖约束。本文用真实 ExecutorService,而不是只打印“启动/停止”的空方法,检查容器启动与关闭对资源的影响。生命周期官方契约

实验固定 Spring Framework 6.2.11、JDK 21,源码提交 4c134254642d88e058aa004bdaf44168e1be7bb2。完整代码位于 examples/spring-framework-lab/src/main/java/blog/spring/Chapter13.java。每个 Worker 持有独立线程池,start 提交一次任务并限时等待,stop 关闭并等待终止。没有数据库连接,资源释放结论限于这些实际线程池。

状态 观察依据
对象已创建 已登记并实例化的 Worker 引用
组件已启动 start 任务完成后 running=true
停止协议完成 stop 完成资源关闭后调用 callback
对象已销毁 DisposableBean.destroy 标记
线程池已结束 ExecutorService.isTerminated 为 true

phase 顺序、失败清理和父子容器查找方向

SmartLifecycle 在 refresh 的哪个位置启动

AbstractApplicationContext.finishRefresh() 初始化 LifecycleProcessor,并调用其 onRefresh。默认处理器的 onRefresh 调用 startBeans(true),只选择自动启动候选;成功后处理器才标记 running。此时常规非 lazy singleton 已创建,但业务生命周期的启动仍可能失败。Context 完成刷新入口

DefaultLifecycleProcessor.startBeans() 收集 Lifecycle Bean,按 phase 放入升序分组,依次启动。SmartLifecycle 的自动启动语义使它能在 refresh 中运行;普通 Lifecycle 不能直接套用相同的自动启动结论。实现中还会检查 isRunning,防止把已运行组件当成尚未启动组件重复启动。默认生命周期处理器

实验给 early 设置 phase=-10,late 设置 phase=10。refresh 后 trace 精确为 start:early、start:late,两个 Worker 的 running 都为 true。start 中的 Future.get 带五秒截止,这使“可用”具有本地判据:至少成功执行过一次提交到自身池中的任务。这个判据没有证明远端服务健康,也没有用一个字段代替所有业务就绪检查。

SmartLifecycle.start 没有返回业务 Future 的统一接口。如果实现只是提交异步初始化并马上返回,容器看到的启动结束也会早于真正业务准备完成。需要保留哪一级就绪保证,必须由 start 实现或额外 readiness 协议明确。实验选择有界等待,避免把这种差异隐藏在日志里。

停止 phase 反向执行,销毁发生在后面

正常关闭调用 LifecycleProcessor.onClose,停止运行组件,再进入 singleton 销毁。停止阶段按 phase 从高到低分组,所以 late 先停、early 后停。实验检查 trace 中 stop:late 在 stop:early 前面,也检查最后一个 stop 先于销毁回调。

对于 SmartLifecycle,stop(Runnable callback) 的 callback 表示这次组件停止已经完成。异步 stop 可以先发停止请求,完成后再调用 callback。若在资源尚未释放时提前调用 callback,容器等待的是一个错误的完成声明;若永不调用,则处理器只能依赖超时策略继续关闭。

实验的 Worker 用同步实现 stop:shutdownNow、awaitTermination 成功后设置 running=false,再调用 callback。destroy 记录销毁状态,并对仍处于 running 的组件补做停止。最终还直接断言两个 executor.isTerminated。这些断言分别覆盖停止协议、销毁回调和真实线程池终态,不能合并成一条“close 执行过”。

1
2
3
refresh:创建 Worker → early.start → late.start → 返回
close:late.stop → early.stop → destroy 回调 → 返回
资源验收:early.executor.isTerminated && late.executor.isTerminated

destroy 作为兜底并不意味着任意失败都能自动恢复。对象构造过程中打开资源后直接抛错,容器可能从未获得可供销毁的完整实例;start 在把资源保存到字段前失败,也需要方法自身清理。实验的 start 捕获提交或等待异常后立即 shutdownNow,成功路径才标记 running,责任在获取资源的同一方法中闭合。

模式提炼:用终态验证停止协议

资源管理可以写成 requestStop → awaitStopped → destroyOwner。线程池看终止状态,连接看关闭或池归还,网络服务器看接受连接是否停止及活动请求是否排空。callback 是协议中的信号,最终资源状态是信号是否真实的证据。两个检查同时存在,才能发现“回调很快,但后台线程仍活着”的实现错误。

dependsOn 可以覆盖 phase 的表面顺序

第二组实验故意设置 dependent 的 phase=-10,dependency 的 phase=10,再给 dependent 的定义设置 dependsOn(“dependency”)。若只按数字排序,dependent 应先启动;实际 trace 为 start:dependency、start:dependent。

原因在 doStart():准备启动某个 Bean 时先读取它登记的依赖,递归启动依赖,再启动自身。停止路径相反,先停止依赖于当前对象的组件,再停止当前对象。phase 为分组提供顺序,显式依赖关系给出更强的约束。启动和停止的依赖递归

关闭后,实验确认 stop:dependent 先于 stop:dependency,且两个池都终止。两个方向必须一起检查;只验证启动顺序,仍可能在关闭时让上游组件访问已经结束的依赖。

上游 DefaultLifecycleProcessorTests 包含 dependencyStartedFirstEvenIfItsPhaseIsHigher 和 dependentShutdownFirstEvenIfItsPhaseIsLower,固定了这个优先关系。phase 的数值约定应表达组件分组,而不是要求业务在多个类中记住一套偶然的注册顺序。生命周期上游测试

父子容器按名称查找时的方向

父容器有 route=“parent” 和 parentOnly=42,子容器有 route=“child” 与 childOnly=7。子容器查 route 返回 child,父容器查 route 仍返回 parent。子定义遮蔽同名父定义,没有修改父容器保存的实例。

AbstractBeanFactory.doGetBean() 在本地无法找到相应定义时才考虑委托父工厂。查找沿 child 到 parent 方向前进;parent 不知道某个 child 的专属定义。因此 child 能取得 parentOnly,并与父查询结果保持引用相同,parent 查询 childOnly 却抛 NoSuchBeanDefinitionException。按名称查找与父工厂委托

containsBean 会考虑层级可见性,containsLocalBean 只检查本地。实验对 parentOnly 同时得到 true 与 false,两个 API 回答的问题不同。排查重复对象时应把所属容器一起记录,不能只看 Bean 名称相同就当成同一个定义被覆盖。

本文验证的是按名称获取。按类型列举、依赖候选合并、事件向父容器传播都有自己的实现入口,不能从这个例子直接推出所有层级 API 都采用完全相同遍历方式。尤其父 Bean 的内部依赖通常在父工厂创建时已经解析,子容器同名 Bean 不会反向改写父 Bean 的字段。

子容器先 close 后,父容器仍 active,route 仍可查询。父子关系提供查找路径,不意味着 child 拥有 parent 的生命周期。实际 Web 应用中需要明确根容器与子上下文分别由谁创建、谁关闭,避免把一个局部服务的关闭扩散到共享父容器。

启动失败后,必须确认已启动资源已结束

最后一组实验先启动 phase=0 的 survivor,再启动 phase=100 的 Failing。Failing.start 抛受控 IllegalStateException,DefaultLifecycleProcessor 将失败传播为 ApplicationContextException。onRefresh 捕获该异常时先 stopBeans,尝试停止已启动的组件,然后重新抛出。

外层 refresh 捕获运行时异常后 destroyBeans,取消 active 状态并继续抛出。实验因此能同时观察到:refresh 失败、Context 不再 active、survivor.running 为 false、线程池已终止、destroy 标记为 true。失败后的 trace 为 start:survivor、stop:survivor、destroy:survivor。

上游 singleSmartLifecycleAutoStartupWithFailingLifecycleBean 检查先前启动对象在 refresh 失败后不再运行。本文把这个契约扩展成真实线程池终止断言,避免实现仅把 running 字段改成 false 却遗留线程。上游源码与测试已阅读,本地运行的是独立实验,不等于执行过上游构建。

这条证据不覆盖强制杀进程、永不响应中断的任务或外部服务不可达。shutdownNow 发出中断请求,真正终止仍取决于任务合作;awaitTermination 的返回值必须检查。本文任务为空操作,适合观察框架调用顺序,不能用它证明任意生产任务都能在五秒内关闭。

运行、反例题与练习

在实验工程中使用 JDK 21:

1
./mvnw -q compile exec:java -Dexec.mainClass=blog.spring.Chapter13

输出在 evidence/13/local-20261002/run.txt,17 条 CHECK PASS,退出码为 0。正常 phase、dependsOn、父子容器和故障启动分别使用独立上下文,全部由 try-with-resources 管理。成功、依赖覆盖与失败三组线程池均检查终态。

反例题:一个 Worker.stop 把 running 改为 false 并立即调用 callback,但没有调用 executor.shutdown。LifecycleProcessor.isRunning 或 trace 能否证明线程池已释放?不能。框架只能观察组件声明,必须用 executor.isTerminated 等资源状态揭示错误。

可执行练习:在副本中令 Failing 的 phase 小于 survivor,重跑并记录 survivor 是否曾启动。相应调整断言时应允许 executor 尚未创建,而不能强行检查不存在的线程池。再给 dependent 增加一个依赖,画出启动拓扑与反向停止顺序,用 trace 验证依赖约束是否覆盖数字排序。

故障现象 证据入口 需要避免的误判
refresh 返回却不可服务 start 的真实就绪保证 构造成功代表业务就绪
phase 顺序与预期不同 dependsOn 与运行状态 数字排序是唯一规则
子容器对象重复 容器身份与本地定义 同名就表示同一实例
启动失败后线程仍在 stop、destroy 与资源终态 有关闭日志就无泄漏

参考资料