Spring Bean 生命周期 & 循环依赖:90% 开发者只知结论不懂原理
"Spring 能解决 setter 循环依赖,解决不了构造器循环依赖。"
这句话,几乎所有 Java 后端都能背出来。
但问一句:为什么?底层是怎么做到的?三级缓存到底存了什么?
绝大多数人就开始支支吾吾了。
这篇文章,我们不背结论,一起把 Spring Bean 生命周期 + 循环依赖的底层机制 拆清楚。读完你会明白:
Spring 不是"神奇地"解决了循环依赖,而是"精心设计"了一套流程。
一、先跳出细节:Bean 生命周期全景图(简化版)
为了讲清楚循环依赖,必须先有"地图"。
一个 Bean 从定义到可用,核心经历以下几个阶段:
csharp
实例化(new)
↓
填充属性(populate)
↓
初始化(init)
↓
就绪(getBean)
↓
销毁(destroy)
对应到 Spring 源码中,最核心的方法是:
bash
AbstractAutowireCapableBeanFactory#doCreateBean
里面三件大事,务必记住:
- createBeanInstance:创建实例(反射 new)
- populateBean:依赖注入(@Autowired / setter)
- initializeBean:执行 aware、init-method、AOP 代理
👉 循环依赖的问题,就发生在「实例化之后、属性注入期间」。
二、什么是循环依赖?为什么是个问题?
1️⃣ 什么是循环依赖
less
@Service
public class A {
@Autowired
private B b;
}
@Service
public class B {
@Autowired
private A a;
}
创建 A → 需要 B → 创建 B → 又需要 A → 无限套娃。
2️⃣ 问题的本质
对象还没创建完,就被别人引用了。
- 如果等 A 完全创建好再给 B,B 拿不到
- 如果先给 B 一个"半成品 A",又怕出问题
Spring 的解法很巧妙:
👉 提前暴露一个"尚未初始化完成的 Bean 引用"
三、三级缓存:Spring 解决循环依赖的核心
这是面试必考点,也是理解原理的关键。
1️⃣ 三级缓存分别是什么?
| 缓存名称 | 作用 |
|---|---|
| singletonObjects | 一级缓存:存放完全初始化好的 Bean |
| earlySingletonObjects | 二级缓存:存放早期暴露的 Bean(未初始化完) |
| singletonFactories | 三级缓存:存放 ObjectFactory(Bean 的工厂) |
2️⃣ 它们的关系(重点)
- 一级缓存:最终对外暴露的 Bean
- 二级缓存:防止重复创建早期引用
- 三级缓存 :真正解决循环依赖的关键
注意:三级缓存里存的不是 Bean 本身,而是一个 工厂(ObjectFactory) 。
四、源码级流程:Spring 是如何"破局"的?
我们用 A → B → A 这个经典循环依赖,走一遍流程。
Step 1:创建 A
scss
getBean(A)
singletonObjects中没有 A- 标记 A 正在创建中
- 调用
createBeanInstance(A)→ 实例化 A(此时 a 是半成品)
Step 2:将 A 的 ObjectFactory 放入三级缓存
scss
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean))
这一步非常关键:
- A 虽然没初始化完
- 但 Spring 已经允许别人拿到它的早期引用
Step 3:填充 A 的属性(需要 B)
scss
populateBean(A)
→ getBean(B)
Step 4:创建 B
singletonObjects中没有 B- 实例化 B
- 同样把 B 的 ObjectFactory 放入三级缓存
Step 5:填充 B 的属性(需要 A)
scss
populateBean(B)
→ getBean(A)
这时候重点来了 👇
Step 6:B 再次 getBean(A) 时发生了什么?
Spring 发现:
- A 正在创建中 ✅
- 三级缓存中存在 A 的 ObjectFactory ✅
于是:
- 调用
ObjectFactory.getObject() - 拿到 A 的 早期引用
- 放入 二级缓存(earlySingletonObjects)
- 删除三级缓存中的工厂
👉 B 成功拿到了"半成品 A"
Step 7:B 初始化完成
- B 初始化完成
- 放入 一级缓存 singletonObjects
Step 8:回到 A 的创建流程
- A 继续填充属性(此时 B 已就绪)
- A 初始化完成
- 放入一级缓存
- 清理二、三级缓存
✅ 循环依赖解决完成
五、为什么 setter / field 注入可以,构造器注入不行?
这是很多人"背结论"的地方,现在我们从原理上解释。
setter / field 注入为什么可以?
因为流程是这样的:
css
实例化 A(new A)✅
→ 暴露早期引用 ✅
→ 再注入 B ✅
实例化 和 依赖注入 是分开的
Spring 有机会在"注入前"先把 A 曝光出去。
构造器注入为什么不行?
kotlin
@Service
public class A {
private final B b;
public A(B b) { this.b = b; }
}
构造器注入的流程是:
css
创建 A
→ 必须先有 B
→ 才能 new A
问题来了:
- A 还没实例化完
- 根本来不及放进三级缓存
- B 再来要 A → 找不到 → 抛异常
📌 结论一句话版:
构造器注入把"实例化"和"依赖注入"绑死了,而 Spring 的循环依赖机制依赖"先实例化、后暴露"。
六、那 AOP 呢?代理对象会不会出问题?
这是一个进阶但非常重要的问题。
如果 A 需要被 AOP 代理:
- 早期暴露的是 原始对象
- 但最终放入容器的是 代理对象
Spring 怎么保证一致性?
答案就在这一句:
scss
getEarlyBeanReference()
- 如果 Bean 需要被代理
- 三级缓存中的
ObjectFactory - 会提前生成代理对象
这样:
- B 拿到的就是 代理后的 A
- 最终容器中也是代理后的 A
- ✅ 完全一致
七、面试标准答案(可直接背)
Spring 通过三级缓存解决 singleton 作用域下的 setter / field 循环依赖。
在 Bean 实例化完成后、属性注入前,Spring 会将一个 ObjectFactory 放入三级缓存,从而在发生循环依赖时,能让对方拿到当前 Bean 的早期引用。
构造器循环依赖无法解决,因为构造器调用发生在实例化阶段,此时 Bean 尚未放入三级缓存,无法满足提前暴露的条件。
八、实际工作中的三点建议
-
不要依赖循环依赖
- 循环依赖往往是设计坏味道
- 优先考虑:拆分职责、引入中间层、使用
@Lazy
-
构造器注入优先
- 虽然不支持循环依赖
- 但能更早暴露设计问题
- 更符合"不可变对象"思想
-
理解 > 背诵
- 面试不是为了复述结论
- 而是展示你对 Spring 容器运作机制的掌控力
九、总结一句话
Spring 不是"黑魔法",它只是在 Bean 生命周期的关键节点,提前暴露了一个工厂,让半成品 Bean 有了被引用的机会。
当你能把这句话讲清楚,你也就超越了那 90% 只知结论的开发者。