Spring 是如何解决循环依赖问题的(三级缓存详解)
Spring 是如何解决循环依赖问题的(三级缓存详解)
面试指北 · 蔚来
题意
面试官问「Spring 怎么解决循环依赖」,表面在问一个机制,实际在考三件事:
- 你是否知道循环依赖发生在哪个阶段(实例化?属性填充?初始化?);
- 你是否说得清三级缓存各自存什么、按什么顺序被访问;
- 你是否知道这套方案什么时候失效——构造器注入、prototype 作用域、Spring Boot 2.6 之后的默认策略,这些才是区分「背过」和「理解过」的地方。
所谓循环依赖,就是 A 依赖 B、B 又依赖 A(也可能是 A→B→C→A 这种更长的环)。如果没有额外手段,创建 A 需要先创建 B,创建 B 又需要先创建 A,形成互相等待。
结论
- Spring 只能解决单例作用域 + setter/字段注入这一类循环依赖,靠的是三级缓存 + 提前暴露半成品对象引用。
- 三级缓存分别是:一级
singletonObjects(成品 Bean)、二级earlySingletonObjects(半成品早期引用)、三级singletonFactories(对象工厂ObjectFactory)。 - 三级缓存的关键价值在于延迟决定要不要生成代理,而不是「多存一层」。只靠两级缓存,AOP 场景下的注入对象就会出错。
- 构造器注入的循环依赖 Spring 解决不了,因为对象还没实例化出来,没有任何东西可以提前暴露,只能抛
BeanCurrentlyInCreationException。 - 工程上的正解通常是重构解耦,
@Lazy、ObjectProvider只是让代码能跑起来的补救手段。
要点展开
1. 三级缓存到底存了什么
| 缓存 | 字段 | 内容 | 生命周期位置 |
|---|---|---|---|
| 一级 | singletonObjects |
完整初始化好的单例 Bean | 初始化完成、放入容器后 |
| 二级 | earlySingletonObjects |
已实例化但还没填充属性/初始化的「半成品」,也可能是提前生成的代理对象 | 发生循环依赖、被提前引用时 |
| 三级 | singletonFactories |
ObjectFactory 工厂(本质是个 lambda) |
实例化之后立刻放入 |
注意二级缓存里存的不一定是原始对象,如果这个 Bean 需要 AOP 代理,存进去的就是代理对象——这一点是理解三级缓存存在意义的前提。
2. 一次完整的 A ↔ B 循环依赖流程
以 A 依赖 B、B 依赖 A(字段注入)为例:
getBean(A)→ 一级缓存没有 → 走创建流程,createBeanInstance通过反射实例化出原始 A(此时属性全是 null)。- 实例化后立刻执行
addSingletonFactory,把一个ObjectFactory放进三级缓存,工厂内容是() -> getEarlyBeanReference(beanName, mbd, bean)。 populateBean(A)填充属性,发现依赖 B → 转去getBean(B)。- B 同理:实例化 → 三级缓存放入工厂 → 填充属性时发现依赖 A → 再次
getBean(A)。 - 这次
getSingleton("a", true)命中流程:一级没有 → 二级没有 → 三级有工厂 → 调用getObject(),内部走getEarlyBeanReference:需要 AOP 就返回代理对象,否则返回原始对象;结果放进二级缓存,同时从三级缓存移除。 - B 拿到 A 的早期引用,顺利完成属性填充和初始化,进入一级缓存。
- 回到 A,注入已经就绪的 B,完成自己的初始化,放入一级缓存,清理二级缓存。
简化成伪代码就是:
// AbstractAutowireCapableBeanFactory#doCreateBean 核心片段(示意)
Object bean = createBeanInstance(beanName, mbd, args); // 1. 实例化
if (earlySingletonExposure) { // 2. 提前暴露
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
populateBean(beanName, mbd, bean); // 3. 填充属性,可能触发循环依赖
initializeBean(beanName, bean, mbd); // 4. 初始化(AOP 代理正常在这里生成)
3. 为什么非要三级?两级不行吗
这是这道题最容易被追问的点。
如果只有两级缓存,那么「半成品」必须在实例化之后马上放进二级缓存——问题就在于,此时你无法确定这个对象将来会不会被 AOP 代理(代理通常发生在初始化之后的 postProcessAfterInitialization)。一旦提前就生成代理,等于所有 Bean 都得提前走一遍代理逻辑,既浪费,也破坏了 BeanPostProcessor 的正常执行顺序和语义。
三级缓存的做法是「先登记一个工厂,用到了再算」:没有循环依赖的 Bean,工厂永远不会被调用,代理依旧在初始化后正常生成;真的发生循环依赖时,才通过 getEarlyBeanReference 提前把代理对象做出来交给对方。
配套还有一个细节:AbstractAutoProxyCreator 会把提前生成过代理的 Bean 记录在 earlyProxyReferences 里,后续正常初始化完成时发现已经代理过,就不再重复包装,避免出现「代理套代理」。
所以可以这样总结:二级缓存解决「能不能提前拿到引用」,三级缓存解决「拿到的是不是最终应该拿到的那个对象」。
4. 为什么构造器注入解决不了
因为提前暴露的前提是「先有对象,再填属性」。构造器注入反过来:new A(b) 必须先把 B 造出来,而造 B 又需要 A,双方都卡在实例化阶段,容器里根本没有半成品可以暴露,只能检测到「当前 Bean 正在创建中」并抛出:
BeanCurrentlyInCreationException:
Error creating bean with name 'a': Is there an unresolvable circular reference?
这也是 Spring 官方文档长期推荐「构造器注入为主」却不担心循环依赖的原因——它逼着你在设计阶段就把环拆掉,而不是靠容器兜底。
5. @Lazy 为什么能救场
@Lazy 注入的不是真实对象,而是一个代理,真正调用方法时才去容器里取目标 Bean。这样一来,A 在实例化 B 时不需要 B 已就绪,环被打断了。
@Service
public class A {
@Lazy
@Autowired
private B b;
}
同类思路还有 ObjectProvider<B>、ObjectFactory<B>、直接注入 ApplicationContext 延迟获取。它们都是「延迟到使用时再解析」,而不是真正修好了循环依赖。
需要提醒的是,@Lazy 解决的是「能启动」,但如果 A、B 在初始化阶段(比如 @PostConstruct 里)就互相调用对方的方法,仍可能拿到状态未完成的半成品,出现 NPE 或字段为空。能启动不等于语义正确。
易错点与对比
① Spring 能解决的范围要划清楚
- ✅ 单例 + setter 注入 /
@Autowired字段注入 - ❌ 构造器注入(除非配合
@Lazy) - ❌ prototype 作用域:不缓存、不提前暴露,直接抛异常
- ❌
@Bean方法之间互相调用构成的环,同样可能无解
② 三级缓存的常见误答
- 说「三级缓存存对象工厂,用于实例化 Bean」是不准确的:工厂的作用是在需要早期引用时决定返回原始对象还是代理对象,不是用来实例化。
- 说「二级缓存没必要,三级就够了」也不对:二级缓存避免了同一个 Bean 的工厂被反复调用、代理被反复生成,同时让「早期引用」这个状态有明确归属。
③ 与 AOP / 异步注解的冲突
如果某个 Bean 在初始化阶段会被 BeanPostProcessor 包装成代理(典型如 @Async、自定义 AOP),而发生循环依赖时提前暴露的是原始对象,注入方拿到的可能不是最终代理,导致注解「失效」。这属于循环依赖带来的隐性 bug,比启动报错更麻烦。
④ Spring Boot 2.6 之后的变化
从 Spring Boot 2.6 起,spring.main.allow-circular-references 默认是 false,循环依赖会直接启动失败。想打开可以配置为 true,但更推荐的做法是趁这个机会重构依赖关系,而不是把开关拨回去。
⑤ 循环依赖 ≠ 循环调用
前者是容器创建 Bean 阶段的依赖闭环(可解或不可解),后者是运行期方法互相调用导致的死循环/栈溢出,两者不是一回事,别在面试里混着讲。
⑥ 真正推荐的解法
优先抽第三层:把 A、B 共同依赖的逻辑下沉到 C,让依赖变成 A→C、B→C,环自然消失;其次用事件机制、回调接口解耦;最后才考虑 @Lazy 这类容器层面的补丁。
小结
一句话概括:Spring 通过三级缓存在「实例化完成、属性填充之前」把对象的早期引用暴露出去,让依赖方先拿到引用继续走流程,从而打破创建期的等待环;三级缓存里的 ObjectFactory 把「是否需要生成代理」这件事推迟到真正发生循环依赖时才决策,兼顾了正确性和性能。
面试时按「三级缓存结构 → 完整流程 → 为什么是三级 → 什么情况下失效 → 怎么重构」这条线展开,基本就覆盖了面试官所有可能的追问方向。