2245 字
约 7 分钟
20
S07-X12 · Bean 生命周期与后置处理器

@PostConstruct 里拿到的对象是不是代理?BeanPostProcessor 为什么会连带把半个容器提前初始化?原型 Bean 的 @PreDestroy 为什么从不执行?答案在同一张时间轴上:Bean 从一行 BeanDefinition 变成可用对象,要穿过十几个回调点,每个点的"当下状态"都不同。

把这条轴画准,很多时灵时不灵的注入问题会自己消失。以下按 Boot 3.3.x / Framework 6.1.x 核对。


1. 容器启动的十二步

AbstractApplicationContext#refresh() 顺序固定,与 Bean 生命周期相关的五步:

阶段 方法 做了什么
1 invokeBeanFactoryPostProcessors 解析配置类、改 BeanDefinition
2 registerBeanPostProcessors 实例化并注册所有 BPP
3 onRefresh 子类扩展(如内嵌 Tomcat)
4 finishBeanFactoryInitialization 实例化所有非懒加载单例
5 finishRefresh 发 ContextRefreshedEvent

单 Bean 的创建在 doCreateBean:createBeanInstance → populateBean → initializeBean → registerDisposableBean。initializeBean 内四段:

invokeAwareMethods(bean, name);                    // BeanNameAware 等
applyBeanPostProcessorsBeforeInitialization(...);  // @PostConstruct 在这里
invokeInitMethods(bean, name);                     // afterPropertiesSet + init-method
applyBeanPostProcessorsAfterInitialization(...);   // AOP 代理在这里生成

完整顺序:

# 回调 此刻的 bean 是
1 构造器 原始对象,无依赖
2 字段/Setter 注入 依赖已就位
3 Aware 回调 原始对象
4 postProcessBeforeInitialization → @PostConstruct 原始对象
5 afterPropertiesSet → init-method 原始对象
6 postProcessAfterInitialization 之后返回的可能是代理
7 使用期 容器里拿到的是代理
8 @PreDestroy → destroy → DisposableBean 代理

2. @PostConstruct 与 afterPropertiesSet

维度 @PostConstruct afterPropertiesSet() init-method
处理者 CommonAnnotationBeanPostProcessor invokeInitMethods 直接调用 同左,反射调用
时机 postProcessBeforeInitialization 其后 再后
依赖 JSR-250 Spring 接口,与框架耦合 @Bean(initMethod)
推荐 通用初始化 需要框架能力时 第三方类不能改源码时

第 4、5、6 步"还是原始对象"是本节核心:所有初始化回调都在代理生成之前。想在初始化阶段调用本类的事务方法,等于调 this,注解不生效——与第 X11 篇的自调用同根。


3. BeanFactoryPostProcessor 与 BeanPostProcessor

维度 BeanFactoryPostProcessor BeanPostProcessor
作用对象 BeanDefinition(图纸) Bean 实例(房子)
时机 所有实例化之前 初始化前后各一次
典型实现 ConfigurationClassPostProcessor AutowiredAnnotationBeanPostProcessor
数量 通常个位数 几十个
能否依赖业务 Bean 不能,会提前实例化 同样不能

前者改定义,所以能"给所有 @Service 加个属性";后者改实例,所以它才能做代理与 @Autowired 填充。混用这两个接口是常见的架构级误用。


4. AOP 代理在第 6 步生成

public Object postProcessAfterInitialization(Object bean, String beanName) {
    return wrapIfNecessary(bean, beanName, cacheKey);   // 有切面则返回代理
}

三条推论:初始化回调里 this 是原始对象;别的 Bean 注入拿到的是代理,跨 Bean 调用有事务;代理的 getClass() 是 XxxService$$SpringCGLIB$$0,用 getClass() == XxxService.class 判断会失败,应用接口或 AopUtils.getTargetClass。


5. 循环依赖与三级缓存

缓存 字段 存什么
一级 singletonObjects 完整成品 Bean
二级 earlySingletonObjects 提前暴露的早期引用
三级 singletonFactories ObjectFactory,可产出早期引用

为什么需要第三级?"早期引用要不要被 AOP 包装"取决于是否真有循环依赖。用工厂延迟到被引用时才决定,才能做到:无循环依赖时不必提前生成代理,有循环依赖时注入给对方的也是同一个代理。少了这级,代理与原始对象的身份会不一致。

getBean(A) → 建 A 实例 → A 的 ObjectFactory 进三级缓存
  → populateBean(A) 需要 B → getBean(B) → populateBean(B) 需要 A
    → 取 A 的工厂 → 早期引用(必要时包成代理)→ 进二级缓存
  → B 完成后入一级 → A 注入 B → A 完成后入一级

6. 构造器注入为什么解不了循环依赖

@Service
public class A { public A(B b) { } }
@Service
public class B { public B(A a) { } }

启动直接失败,报 BeanCurrentlyInCreationException。三级缓存暴露的是"已 new 出来、尚未填充属性的半成品";构造器注入连 new 都完成不了,没有半成品可暴露。这不是缓存层级不够,是对象还不存在。

解法三种:改 Setter/字段注入(能解但藏起了依赖关系);把共同依赖抽成第三个 Bean;在注入点加 @Lazy:

@Service
public class A {
    public A(@Lazy B b) { }   // 注入 B 的延迟代理
}

@Lazy 只在首次调用方法时才真正 getBean(B),打破创建顺序的强约束,代价是把启动期的错误推迟到运行期。


7. BeanPostProcessor 自身的限制

BPP 在 registerBeanPostProcessors 阶段就实例化,早于任何业务 Bean,由此两个硬约束:

  • BPP 不被其它 BPP 处理,不会被 AOP 代理。给 BPP 加 @Transactional 无效,日志里会出现 is not eligible for getting processed by all BeanPostProcessors。
  • 在 BPP 里注入业务 Bean,会把这 Bean 及整条依赖链提前拉到该阶段初始化,于是它同样错过后续 BPP。典型症状:某 Bean 配了事务/缓存,行为却像没配。

需要访问其它 Bean 时用 ObjectProvider 延迟取,或挪到 SmartInitializingSingleton、ApplicationRunner 里做。


8. 事件与监听器时机

事件 触发点 能拿到全部单例
ApplicationEnvironmentPreparedEvent Environment 就绪 否
ContextRefreshedEvent finishRefresh 是
ApplicationStartedEvent refresh 后、runner 前 是
ApplicationReadyEvent runner 之后 是
ContextClosedEvent 关闭时,destroy 之前 是

@EventListener 默认同步,跑在发布线程;加 @Async 才异步,需先有 @EnableAsync 与可用 Executor。监听器抛异常会中断发布者流程,这一点与消息队列的语义完全不同。


9. 作用域与原型注入单例

作用域 实例数 销毁回调
singleton 每容器一个 调用
prototype 每次 getBean 一个 不调用
request 每请求一个 请求结束时
session 每会话一个 会话失效时
@Service
public class ReportService {
    private final ReportContext ctx;            // 只注入一次,等于单例
    public ReportService(ReportContext ctx) { this.ctx = ctx; }

    @Autowired private ObjectProvider<ReportContext> provider;  // 正确:每次取新的
    public void run() { ReportContext c = provider.getObject(); }
}

@Lookup 或 @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) 是另两条路。原型 Bean 的 @PreDestroy 不执行不是 bug:容器不持有原型实例,无从知道何时销毁。


10. 常见坑

现象 根因 处理
@PostConstruct 里调事务方法无效 代理尚未生成 初始化只做纯内存准备
加了 BPP 后某 Bean 行为异常 该 Bean 被提前初始化 用 ObjectProvider 延迟取
BeanCurrentlyInCreationException 构造器循环依赖 @Lazy 或抽第三 Bean
原型 Bean 销毁方法不执行 容器不管原型生命周期 手动 destroy()
多个 BPP 顺序不定 未实现 Ordered 显式给顺序,PriorityOrdered 优先
getClass() == Xxx.class 失败 拿到 CGLIB 代理 用接口或 getTargetClass

BPP 顺序常被忽略:AutowiredAnnotationBeanPostProcessor 与 CommonAnnotationBeanPostProcessor 都实现 PriorityOrdered,自定义 BPP 不声明顺序就排在它们之后,那时 @Autowired 与 @PostConstruct 都已处理完。


11. 边界

场景 更合适的做法 理由
启动后一次性预热 ApplicationRunner 时机明确,单例已齐
改所有 Bean 的公共属性 BeanFactoryPostProcessor 改定义 改实例会漏掉后续创建的
销毁前统一处理 DestructionAwareBeanPostProcessor 统一入口,含原型
延迟到首次使用才初始化 @Lazy + ObjectProvider 缩短启动,错误后移
按请求创建对象 作用域代理或工厂方法 直接注入单例必错

生命周期的坑本质都是"在错误的时间点做了正确的事"。先确认当前处在第几步,再决定这段代码能否写在这里,比背下回调名字更有用。


12. 自测

  1. @PostConstruct 与 afterPropertiesSet 谁先执行,为什么?
  2. BeanFactoryPostProcessor 和 BeanPostProcessor 分别改什么?
  3. AOP 代理在第几步生成,在此之前 this 是什么?
  4. 三级缓存各存什么,为什么不能只有两级?
  5. 构造器注入的循环依赖为什么用缓存也解不了?
  6. 把 prototype 注入单例,会拿到几个实例,销毁回调执行吗?
S07-X12 · Bean 生命周期与后置处理器
https://www.clxhxhhr.top/posts/5953/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。