场景:Bean 从生到死,框架在背后做了什么
注入一个 Bean 看似只是 @Autowired 一行,但从"一个 class"到"一个可用对象",Spring 在背后跑了一套完整的生命周期流程。理解它,你才能解释:为什么 @PostConstruct 里能拿到注入的依赖、为什么 AOP 代理会在某个时机替换掉原对象、为什么两个 Bean 互相依赖大多数时候能正常启动、又为什么构造器循环依赖必然报错。
Bean 生命周期:四大阶段
单例 Bean 的创建可以拆成四个核心阶段,每个阶段都有可插拔的扩展点:
1 | 实例化 (Instantiation) → 调用构造器,得到"半成品"对象(属性还是 null) |
一次 doCreateBean 的内部流转
1 | // AbstractAutowireCapableBeanFactory#doCreateBean 的核心骨架(简化) |
初始化阶段的执行顺序(高频面试点)
initializeBean 内部的执行顺序是有严格先后的:
- Aware 回调:
BeanNameAware、BeanFactoryAware、ApplicationContextAware,让 Bean 拿到容器基础设施。 - BeanPostProcessor 的
postProcessBeforeInitialization:@PostConstruct实际由InitDestroyAnnotationBeanPostProcessor(在此阶段)触发。 InitializingBean#afterPropertiesSet→ 自定义init-method。- BeanPostProcessor 的
postProcessAfterInitialization:AOP 代理通常在这一步生成(AbstractAutoProxyCreator在此返回代理对象替换原 Bean)。
所以"为什么 @PostConstruct 能拿到注入的依赖"答案很清楚:属性填充(阶段 3)在初始化(阶段 4)之前完成,到 @PostConstruct 执行时,依赖早已注入。
循环依赖:三级缓存如何破局
什么是循环依赖
A 依赖 B,B 又依赖 A。创建 A 时要先填充 B,创建 B 时又要填充 A,看似死循环。Spring 通过三级缓存 + 提前暴露化解了 setter/字段注入下的循环依赖。
三级缓存的角色
1 | 一级缓存 singletonObjects : 成品 Bean(完全初始化好的) |
解决流程拆解(A 依赖 B,B 依赖 A)
1 | 1. 创建 A:实例化 A(半成品)→ 把"A 的工厂"放进三级缓存 |
为什么需要"第三级",二级不够吗
关键就在三级缓存存的是工厂(ObjectFactory)而不是对象。这个工厂里封装了 getEarlyBeanReference 逻辑:如果 A 需要被 AOP 代理,这里返回的就是代理对象;否则返回原始对象。
如果只有二级缓存、直接存半成品原始对象,那么当 A 需要 AOP 时,B 提前拿到的是原始 A,而最终容器里的 A 是代理 A,两者不一致,B 持有的就成了一个"该被代理却没被代理"的对象,破坏一致性。三级缓存用"延迟到被引用时才决定生成原始对象还是代理"的方式,保证了即使在循环依赖中,被注入的也是正确的(代理后的)引用。这是三级缓存设计的精髓。
工程权衡与边界
构造器循环依赖无解
三级缓存的"提前暴露"发生在实例化之后、属性填充之前。但构造器注入要求在实例化时就拿到依赖——此时 Bean 还没被 new 出来,根本没机会放进三级缓存。于是:
1 |
|
构造器循环依赖会抛 BeanCurrentlyInCreationException,这是设计使然,不是 bug。要解,要么改成字段/setter 注入,要么用 @Lazy 让其中一方注入代理延迟初始化,但更推荐重新审视设计——循环依赖往往是模块职责划分不清的信号。
默认禁止循环依赖的趋势
较新版本的 Spring Boot 默认关闭循环依赖支持,启动直接失败,强制开发者正视问题。需要时可用 spring.main.allow-circular-references=true 临时打开,但这应被视为技术债而非常规手段。
性能与内存
三级缓存只在 Bean 创建期短暂使用,半成品一旦升级为成品,二、三级缓存中对应条目会被清理,不会长期占用内存。整套机制的开销集中在启动期,运行期无额外成本。
常见误区与踩坑
- 误以为所有循环依赖都能解:只有 单例 + 非全构造器注入 才行。
prototype作用域的循环依赖不被缓存机制支持(prototype 不放缓存),会直接报错。 @Async+ 循环依赖踩坑:@Async的代理在初始化后置阶段才生成,而早期暴露的引用可能不是这个最终代理,某些版本下会抛"注入的 Bean 与最终 Bean 不一致"的异常。遇到时优先解开循环依赖。- 在构造器里使用注入的依赖做复杂初始化:构造器执行时属性还没填充(字段注入场景),此时访问字段依赖必为 null。需要依赖参与的初始化逻辑应放到
@PostConstruct。 @PreDestroy不被调用:prototypeBean 的销毁回调不由容器管理(容器创建后即"撒手"),需手动销毁;另外System.exit或 kill -9 也会跳过优雅销毁。
小结
Bean 生命周期是"实例化 → 属性填充 → 初始化(Aware → 前置 → init → 后置/AOP)→ 销毁"的有序流水线,每个阶段都开放扩展点。循环依赖的破解靠三级缓存 + 提前暴露,而第三级存工厂而非对象的设计,正是为了在循环引用中也能正确返回 AOP 代理。记住两条边界:构造器循环依赖无解(实例化阶段拿不到依赖),prototype 循环依赖无解(不入缓存)。当你需要打开 allow-circular-references 时,先停下来想想是不是设计该重构了。