Bean 生命周期与处理器:对象在哪些阶段被改变
@PostConstruct 中调用事务方法没有事务,BeanPostProcessor 打印的对象类在 before/after 两侧不同,停机时 @PreDestroy 执行了但连接仍未释放。这些都来自同一事实:Spring 管理的 Bean 不是 new 完就进入单例池,而要经过实例化、依赖填充、Aware、初始化回调与多轮后处理,最终暴露对象甚至可能是代理。
官方 容器扩展点参考 定义了后处理器的基本契约;真正的工程风险来自处理器介入阶段、顺序、对象身份和失败清理之间的组合。
doCreateBean 组织一条可插入的流水线
AbstractAutowireCapableBeanFactory#doCreateBean 协调实例包装、merged definition 后处理、早期引用、属性填充、初始化和注册销毁。构造器选择可能由 SmartInstantiationAwareBeanPostProcessor 参与;AutowiredAnnotationBeanPostProcessor 在属性阶段解析注入点;自动代理创建器通常在初始化后返回代理。
构造方法内只有构造参数可依赖,字段/setter 尚未填充;BeanNameAware 等 Aware 回调发生在属性填充后、初始化前;初始化回调运行在目标对象上,此时外部代理通常尚未成为最终暴露引用。因此在 init 内 this.transactionalMethod() 仍是自调用。
examples/backend-development/spring-framework/bean-lifecycle-processors/BeanLifecycleDemo.java 输出关键状态:
javac --release 17 -Xlint:all -Werror examples/backend-development/spring-framework/bean-lifecycle-processors/BeanLifecycleDemo.java examples/backend-development/spring-framework/bean-lifecycle-processors/ProcessorOrderingDemo.java
java -cp examples/backend-development/spring-framework/bean-lifecycle-processors BeanLifecycleDemoevents=[instantiate, populate, aware, beforeInit, init, afterInit:proxy, destroy]模型省略了 merged definition、实例化前短路、早期引用和多个 init/destroy 适配器,但准确表达“代理通常晚于目标初始化”。正文代码若需要代理能力,应由另一个 Bean 在容器刷新完成后调用,或重新拆分职责,而不是在初始化中从容器找自己。
BeanPostProcessor 的顺序决定谁看到什么对象
PriorityOrdered、Ordered 与普通处理器分组注册,组内再按 order;手工通过 BeanFactory API 注册的处理器可能按注册顺序工作。处理器链不是业务 Filter:每个 Bean 都可能经过,重逻辑和宽泛类型判断会乘以 Bean 数,直接拖慢启动。
实际 before/after 都按处理器列表正序调用,并把上一个返回值传给下一个;图用嵌套表达对象逐层包装,不应理解成 Filter 栈的反向回调。任一处理器返回替代对象,后续看到的类型与 identity 都会改变。处理器必须只命中明确注解/接口,缓存反射元数据,并保留 bean name 与原始 target class 诊断。
ProcessorOrderingDemo.java 给出分组排序:
java -cp examples/backend-development/spring-framework/bean-lifecycle-processors ProcessorOrderingDemoorder=[priority, ordered, ordinary]不要假设相同 order 的跨模块处理器有稳定顺序。若两个处理器存在先后依赖,要通过更明确的 order 与集成测试固定;更好的做法是减少隐式耦合,让每个处理器对输入是否已代理都有兼容策略。
InstantiationAware 处理器能改变更早阶段
实例化前处理器可以直接返回代理并短路普通创建,实例化后可阻止属性填充,属性处理器可修改 PropertyValues。能力越早,影响面越大。自定义框架若在此创建对象,必须自己承担注入、初始化与销毁契约,否则会产生“能从容器拿到,却没完成标准生命周期”的对象。
MergedBeanDefinitionPostProcessor 常缓存注入元数据,必须在 definition 变化或类重载时正确清理。缓存键若只用类名而忽略 ClassLoader,会在插件/重部署场景串用反射成员。
初始化失败需要回滚已经取得的资源
init 抛异常时 Bean 不会进入可用单例池,但构造器、属性 setter 或更早 BPP 已经可能分配线程、文件和连接。把资源获取集中在可关闭组件,并在初始化失败 catch/finally 主动释放;不能期待正常 destroy 一定被调用。
SmartInitializingSingleton 在所有常规单例完成后执行,适合验证跨 Bean 图,不适合无界远程预热。Lifecycle/SmartLifecycle 负责 context 刷新后的 start/stop 和 phase 顺序,可表达“消费者先停、生产者后停”。所有外部动作都要有 timeout、失败策略和可跳过开关。
销毁路径可能依次适配 @PreDestroy、DisposableBean 和自定义 destroyMethod;依赖对象通常在依赖者之后销毁。进程被强杀时没有保证,因此持久一致性不能依赖销毁回调。关闭验收要检查 executor terminated、连接池 active 归零、注册中心摘除和后台线程 context class loader。
处理器治理看命中与代价
为每个自定义 BPP 记录扫描 Bean 数、命中数、包装数、耗时与失败 bean name;启动基线比较处理器列表与顺序。日志出现“not eligible for getting processed by all BeanPostProcessors”通常表示某 Bean 在处理器完整注册前被提前实例化,应追查处理器自身依赖与非 static 工厂方法。
测试不能只断言 Context 启动。要断言最终暴露对象类型、target class、处理器命中次数、init/destroy 次数、异常初始化后的资源回收和多轮 Context 关闭后线程/类加载器回到基线。生命周期的价值在于让对象从配方到关闭每一步都能解释,而不是背诵回调名单。
单例创建锁保护身份,不替业务对象提供线程安全
多个线程同时请求同一个尚未创建的 singleton 时,注册表必须协调“谁创建、谁等待、失败后谁清理”,保证最终只发布一个规范引用。创建线程在属性填充期间又递归请求同一 Bean,才会进入提前引用路径。外部并发请求和内部递归创建是两类问题,不能把所有 BeanCurrentlyInCreationException 都解释为业务循环。
singleton 的“单”是每个 BeanFactory 一个实例,并不表示实例字段自动安全。处理器完成后,该对象可能被所有请求线程并发调用;在 init 中创建可变缓存而没有安全发布,或在方法中修改非线程安全集合,容器不会替它加锁。生命周期测试应与并发测试分开:前者证明只初始化一次和正确发布,后者证明公开方法的共享状态语义。
创建失败必须清理正在创建标记、提前缓存和已经登记的依赖关系,后续获取才能重试或稳定失败。自定义 scope 若自行缓存“正在创建”的占位值,也要定义异常清理,否则一次失败会永久留下半对象。错误日志应保留第一次失败 cause,避免第二次请求只看到缓存状态异常。
初始化回调不是应用启动编排器
@PostConstruct、afterPropertiesSet 与自定义 init method 适合校验本对象配置、构建本地不可变状态和获取受控资源。把数据库迁移、全量缓存预热、注册中心长轮询或消息消费主循环放进去,会把单 Bean 初始化变成无界启动任务,并持有整个依赖创建链。外部动作必须有 deadline、幂等、取消和失败策略。
需要所有常规 singleton 就绪后再检查依赖图,可使用 SmartInitializingSingleton 一类完成回调;需要随 context 启停的运行组件,可使用 Lifecycle/SmartLifecycle 和 phase。phase 表达启动先后与停止逆序,但相同 phase 的隐式顺序不应承担业务依赖。消费者必须等生产者就绪时,应通过健康状态或明确依赖协议证明,而不只调一个整数。
应用“Context 已 refresh”也不等于“实例可以接流量”。连接池、订阅者和预热可能仍在启动,应该把内部生命周期状态汇总到 readiness;关闭时先把 readiness 置为不可用、停止接收新任务、等待在途工作,再销毁资源。若直接 close 后才摘流量,请求会进入已销毁 Bean。
初始化期间启动后台线程还需设置名称、daemon 策略、异常处理器与 context class loader,并在 destroy 中确定性终止。线程捕获 Bean 或 ClassLoader 会阻止重部署卸载。回归测试可多次创建/关闭 context,比较线程集合、连接池 active、文件描述符和弱引用回收,而不是只跑一次启动。
销毁适配器需要覆盖正常关闭之外的失败路径
正常关闭时,容器根据依赖关系让依赖者先销毁,再销毁其依赖,并适配 @PreDestroy、DisposableBean 和推断出的 close/shutdown 方法。多种回调同时存在时要确保资源只关闭一次;关闭方法应幂等,部分初始化对象也能安全调用。
进程崩溃、强制终止和宿主超时不会保证 destroy 完成,因此持久一致性不能依赖关闭回调。关闭负责释放本地资源和停止工作,可靠业务状态要在执行过程中持续落盘。优雅停机必须有总预算:停止接流量、等待在途、停止消费者、刷新缓冲、关闭连接,各阶段超时后进入明确强制策略。
prototype 的完整销毁通常不由容器跟踪,获得它的组件要承担 close;request/session scope 则由相应上下文触发回调。把 prototype 注入 singleton 后长期持有,会同时失去“每次新建”和自动销毁预期。生命周期文档必须把创建者、持有者、关闭者写成同一个责任闭环。
处理器回归要验证对象身份与调用能力
自定义处理器的测试矩阵至少包含:不命中 Bean、正常命中、Bean 已被其他代理包装、循环依赖提前引用、初始化异常、父子 context、重复 refresh/close。对每个样本断言 before/after 次数、最终对象 identity、可赋值接口、Advisor 顺序和销毁是否到达 target。
启动剖析可按处理器聚合总耗时与最大单 Bean 耗时。一个处理器总计只占 50 ms,却在某个 Bean 上阻塞 5 秒,平均数会掩盖故障;同时记录命中率可以发现“扫描全部 3000 个,实际只处理 2 个”的低效设计。反射元数据缓存应包含 Class 与 ClassLoader 生命周期,避免静态 Map 让重部署类无法回收。
生命周期扩展的成熟标志不是回调数量丰富,而是对象在任一阶段失败都不会以半成熟身份逃逸,最终代理与提前引用保持一致,正常与异常关闭都能收敛资源,并且每个处理器的影响范围与成本可以量化。
