场景:Bean 从生到死,框架在背后做了什么

注入一个 Bean 看似只是 @Autowired 一行,但从"一个 class"到"一个可用对象",Spring 在背后跑了一套完整的生命周期流程。理解它,你才能解释:为什么 @PostConstruct 里能拿到注入的依赖、为什么 AOP 代理会在某个时机替换掉原对象、为什么两个 Bean 互相依赖大多数时候能正常启动、又为什么构造器循环依赖必然报错。

Bean 生命周期:四大阶段

单例 Bean 的创建可以拆成四个核心阶段,每个阶段都有可插拔的扩展点:

1
2
3
4
5
实例化 (Instantiation)   → 调用构造器,得到"半成品"对象(属性还是 null)
属性填充 (Population) → 依赖注入,@Autowired 的字段在此被赋值
初始化 (Initialization) → Aware 回调 → BeanPostProcessor 前置
→ @PostConstruct / InitializingBean → 后置(AOP 在此)
销毁 (Destruction) → @PreDestroy / DisposableBean

一次 doCreateBean 的内部流转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// AbstractAutowireCapableBeanFactory#doCreateBean 的核心骨架(简化)
protected Object doCreateBean(...) {
// 1. 实例化:反射调用构造器
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
Object bean = instanceWrapper.getWrappedInstance();

// 2. 提前暴露:把"半成品"工厂放进三级缓存(为循环依赖准备)
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

// 3. 属性填充:依赖注入,可能触发其他 Bean 的创建
populateBean(beanName, mbd, instanceWrapper);

// 4. 初始化:Aware → BeanPostProcessor 前置 → init 方法 → 后置(AOP)
exposedObject = initializeBean(beanName, bean, mbd);

return exposedObject;
}

初始化阶段的执行顺序(高频面试点)

initializeBean 内部的执行顺序是有严格先后的:

  1. Aware 回调:BeanNameAwareBeanFactoryAwareApplicationContextAware,让 Bean 拿到容器基础设施。
  2. BeanPostProcessor 的 postProcessBeforeInitialization:@PostConstruct 实际由 InitDestroyAnnotationBeanPostProcessor(在此阶段)触发。
  3. InitializingBean#afterPropertiesSet → 自定义 init-method
  4. BeanPostProcessor 的 postProcessAfterInitialization:AOP 代理通常在这一步生成(AbstractAutoProxyCreator 在此返回代理对象替换原 Bean)。

所以"为什么 @PostConstruct 能拿到注入的依赖"答案很清楚:属性填充(阶段 3)在初始化(阶段 4)之前完成,到 @PostConstruct 执行时,依赖早已注入。

循环依赖:三级缓存如何破局

什么是循环依赖

A 依赖 B,B 又依赖 A。创建 A 时要先填充 B,创建 B 时又要填充 A,看似死循环。Spring 通过三级缓存 + 提前暴露化解了 setter/字段注入下的循环依赖。

三级缓存的角色

1
2
3
一级缓存 singletonObjects        : 成品 Bean(完全初始化好的)
二级缓存 earlySingletonObjects : 半成品 Bean(已实例化,未初始化完)
三级缓存 singletonFactories : 生成"早期引用"的工厂(ObjectFactory)

解决流程拆解(A 依赖 B,B 依赖 A)

1
2
3
4
5
6
7
8
1. 创建 A:实例化 A(半成品)→ 把"A 的工厂"放进三级缓存
2. 填充 A 的属性,发现需要 B → 去创建 B
3. 创建 B:实例化 B(半成品)→ 把"B 的工厂"放进三级缓存
4. 填充 B 的属性,发现需要 A → 查缓存:
一级没有 → 二级没有 → 三级有!调用 A 的工厂拿到"A 的早期引用"
把这个早期引用放进二级缓存(升级),B 拿到 A 引用完成填充
5. B 初始化完成 → 进一级缓存。回到第 2 步,A 拿到成品 B,完成填充与初始化
6. A 初始化完成 → 进一级缓存

为什么需要"第三级",二级不够吗

关键就在三级缓存存的是工厂(ObjectFactory)而不是对象。这个工厂里封装了 getEarlyBeanReference 逻辑:如果 A 需要被 AOP 代理,这里返回的就是代理对象;否则返回原始对象。

如果只有二级缓存、直接存半成品原始对象,那么当 A 需要 AOP 时,B 提前拿到的是原始 A,而最终容器里的 A 是代理 A,两者不一致,B 持有的就成了一个"该被代理却没被代理"的对象,破坏一致性。三级缓存用"延迟到被引用时才决定生成原始对象还是代理"的方式,保证了即使在循环依赖中,被注入的也是正确的(代理后的)引用。这是三级缓存设计的精髓。

工程权衡与边界

构造器循环依赖无解

三级缓存的"提前暴露"发生在实例化之后、属性填充之前。但构造器注入要求在实例化时就拿到依赖——此时 Bean 还没被 new 出来,根本没机会放进三级缓存。于是:

1
2
3
4
5
6
7
8
9
10
11
@Service
public class A {
private final B b;
public A(B b) { this.b = b; } // 构造器注入
}
@Service
public class B {
private final A a;
public B(A a) { this.a = a; } // 构造器注入
}
// 启动直接报错:BeanCurrentlyInCreationException

构造器循环依赖会抛 BeanCurrentlyInCreationException,这是设计使然,不是 bug。要解,要么改成字段/setter 注入,要么用 @Lazy 让其中一方注入代理延迟初始化,但更推荐重新审视设计——循环依赖往往是模块职责划分不清的信号。

默认禁止循环依赖的趋势

较新版本的 Spring Boot 默认关闭循环依赖支持,启动直接失败,强制开发者正视问题。需要时可用 spring.main.allow-circular-references=true 临时打开,但这应被视为技术债而非常规手段。

性能与内存

三级缓存只在 Bean 创建期短暂使用,半成品一旦升级为成品,二、三级缓存中对应条目会被清理,不会长期占用内存。整套机制的开销集中在启动期,运行期无额外成本。

常见误区与踩坑

  • 误以为所有循环依赖都能解:只有 单例 + 非全构造器注入 才行。prototype 作用域的循环依赖不被缓存机制支持(prototype 不放缓存),会直接报错。
  • @Async + 循环依赖踩坑:@Async 的代理在初始化后置阶段才生成,而早期暴露的引用可能不是这个最终代理,某些版本下会抛"注入的 Bean 与最终 Bean 不一致"的异常。遇到时优先解开循环依赖。
  • 在构造器里使用注入的依赖做复杂初始化:构造器执行时属性还没填充(字段注入场景),此时访问字段依赖必为 null。需要依赖参与的初始化逻辑应放到 @PostConstruct
  • @PreDestroy 不被调用:prototype Bean 的销毁回调不由容器管理(容器创建后即"撒手"),需手动销毁;另外 System.exit 或 kill -9 也会跳过优雅销毁。

小结

Bean 生命周期是"实例化 → 属性填充 → 初始化(Aware → 前置 → init → 后置/AOP)→ 销毁"的有序流水线,每个阶段都开放扩展点。循环依赖的破解靠三级缓存 + 提前暴露,而第三级存工厂而非对象的设计,正是为了在循环引用中也能正确返回 AOP 代理。记住两条边界:构造器循环依赖无解(实例化阶段拿不到依赖),prototype 循环依赖无解(不入缓存)。当你需要打开 allow-circular-references 时,先停下来想想是不是设计该重构了。