@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. 自测
@PostConstruct与afterPropertiesSet谁先执行,为什么?BeanFactoryPostProcessor和BeanPostProcessor分别改什么?- AOP 代理在第几步生成,在此之前
this是什么? - 三级缓存各存什么,为什么不能只有两级?
- 构造器注入的循环依赖为什么用缓存也解不了?
- 把
prototype注入单例,会拿到几个实例,销毁回调执行吗?