Spring 循环依赖是面试里非常常见的问题。很多文章一上来就背“一级缓存、二级缓存、三级缓存”,但真正理解它,其实只需要抓住一个核心:
Spring 解决循环依赖的关键,是提前暴露一个还没有完全初始化完成的 Bean 引用。
一、什么是循环依赖?
假设有两个 Bean,A 依赖 B,B 又依赖 A:
@ServicepublicclassA{@AutowiredprivateBb;}@ServicepublicclassB{@AutowiredprivateAa;}正常创建过程就会变成:
创建 A ↓ A 需要 B ↓ 创建 B ↓ B 又需要 A如果 Spring 再重新创建 A,就会一直循环下去。
那 Spring 是怎么打破这个循环的呢?
关键在于:对象实例化完成之后,就已经存在了,只是属性还没有注入完成。
Spring 创建 Bean 大致可以分为:
实例化 → 属性填充 → 初始化例如:
Aa=newA();执行完这一句,A 对象其实已经存在,只不过此时:
A └── b = null所以 Spring 可以先把这个“半成品 A”的引用暴露出去,让 B 先使用。
二、Spring 的三级缓存
Spring 为此使用了三级缓存:
| 缓存 | 作用 |
|---|---|
singletonObjects | 一级缓存,保存已经初始化完成的单例 Bean |
earlySingletonObjects | 二级缓存,保存已经提前暴露的 Bean 引用 |
singletonFactories | 三级缓存,保存用于生成提前引用的对象工厂ObjectFactory |
可以简单记成:
一级:Bean 已经创建完成 二级:Bean 还没创建完成,但提前引用已经生成 三级:提前引用还没生成,但我知道怎么生成三、A 和 B 的循环依赖到底怎么解决?
假设还是:
A → B B → ASpring 首先创建 A:
Aa=newA();A 实例化完成以后,并不会马上进入一级缓存,因为它还没有完成属性填充和初始化。
Spring 会先向三级缓存中放入一个对象工厂:
三级缓存: "A" → ObjectFactory这个工厂以后可以用来获取 A 的提前引用。
接下来 Spring 给 A 注入 B,发现 B 还不存在,于是开始创建 B。
创建 B 的时候,又发现:
@AutowiredprivateAa;B 需要 A。
Spring 此时去查找 A:
一级缓存 → 没有 二级缓存 → 没有 三级缓存 → 找到了 A 的 ObjectFactory于是 Spring 调用这个对象工厂,得到 A 的提前引用:
ObjectFactory ↓ A 的提前引用然后把这个提前引用放入二级缓存:
二级缓存: "A" → A 的提前引用B 就可以拿着这个 A 完成自己的依赖注入和初始化。
B 创建完成以后进入一级缓存。
然后回到 A 的创建流程,此时 B 已经有了:
A └── b → BA 也就可以继续完成初始化,最后进入一级缓存。
整个过程可以简化成:
创建 A ↓ 实例化 A ↓ 把 A 的对象工厂放入三级缓存 ↓ A 需要 B ↓ 创建 B ↓ B 需要 A ↓ 从三级缓存获取 A 的提前引用 ↓ 提前引用进入二级缓存 ↓ A 注入 B ↓ B 创建完成 ↓ A 创建完成 ↓ 进入一级缓存四、为什么还需要第三级缓存?
看到这里可能会有一个疑问:
既然最终还是要把 A 提前暴露出去,为什么不直接把 A 放进二级缓存?
普通情况下确实可以。
问题主要出现在AOP场景。
例如:
@ServicepublicclassA{@Transactionalpublicvoidtest(){}}因为存在@Transactional,最终 Spring 暴露出去的 A 很可能不是原始对象,而是一个代理对象:
AProxy ↓ 原始 A如果 Spring 在 A 刚实例化时,就直接把原始 A 放到二级缓存:
B.a → 原始 A但是 A 最终创建完成以后,Spring 又产生了代理对象:
Spring 容器 → AProxy这时候就会变成:
B 持有:原始 A 容器持有:AProxy这样 B 调用 A 时,就可能绕过 Spring 的 AOP 代理。
因此 Spring 没有直接缓存原始 A,而是在三级缓存中保存:
ObjectFactory当真的发生循环依赖,需要提前获取 A 时,再通过:
getEarlyBeanReference()获取 A 的提前引用。
如果不需要代理,可以返回原始 A;如果需要 AOP,则有机会提前返回代理对象。
所以三级缓存的重点并不是“多存了一层 Bean”,而是:
通过
ObjectFactory延迟生成 Bean 的提前引用,从而让 Spring 有机会决定提前暴露原始对象还是代理对象。
第一次生成提前引用以后,再把它放到二级缓存,后面直接复用即可。
五、为什么构造器循环依赖解决不了?
如果使用构造器注入:
classA{publicA(Bb){}}classB{publicB(Aa){}}创建 A 时:
创建 A ↓ 必须先有 B ↓ 创建 B ↓ 必须先有 A这时候最大的问题是:
Aa=newA(...);这一句都还没有执行完成。
也就是说,A 对象本身还不存在,自然就没有所谓的“提前引用”可以暴露。
因此 Spring 的三级缓存机制主要能处理的是:
单例 Bean 的 setter / 字段注入循环依赖。
而典型的构造器循环依赖无法通过提前暴露引用来解决。
六、总结
Spring 解决循环依赖可以归纳成一句话:
利用 Bean 已经实例化、但尚未完成初始化的时间窗口,提前暴露 Bean 的引用。
三级缓存则可以理解成 Bean 创建过程中的三个状态:
三级缓存 ObjectFactory “我知道怎么生成提前引用” ↓ 二级缓存 Early Reference “提前引用已经生成” ↓ 一级缓存 Singleton Bean “Bean 已经完整创建”所以面试时,与其死记三级缓存的名字,不如先记住:
循环依赖靠“提前暴露引用”解决,三级缓存则负责管理这个提前引用从“准备生成”到“已经生成”,再到“完整 Bean”的整个过程。
另外需要注意:Spring 底层具备这套解决部分循环依赖的机制,并不代表实际项目应该依赖循环依赖。如果 A、B 长期互相依赖,很多时候也说明类之间的职责划分值得重新设计。