开篇引入

在Spring框架的IoC体系中,Bean循环依赖(Circular Dependency)是每一位开发者绕不开的高频知识点,也是面试中必问的核心考点。很多学习者面临这样的困境:
一、痛点切入:没有三级缓存时,循环依赖会出什么问题?

传统方式的困境
假设我们有两个Service相互依赖:
@Service public class ServiceA { @Autowired private ServiceB serviceB; // A依赖B public void doA() { serviceB.doB(); } } @Service public class ServiceB { @Autowired private ServiceA serviceA; // B依赖A,形成循环 public void doB() { serviceA.doSomething(); } }
如果不做任何特殊处理,Spring在启动时会在填充ServiceA的serviceB属性时发现需要ServiceB,于是去创建ServiceB;而在创建ServiceB时又发现需要ServiceA。此时ServiceA还没创建完成,两个Bean相互等待,最终抛出BeanCurrentlyInCreationException异常,应用启动失败-1。
这个设计困境的本质是什么?
Spring Bean的创建遵循三步曲:实例化 → 属性填充 → 初始化。循环依赖的本质是:Bean A在“属性填充”阶段需要Bean B,但Bean B此时正处于“实例化”阶段,尚未完成创建,导致相互等待-32。
想象一下这个场景:你要去办理身份证(Bean A),但办身份证需要先有户口本(Bean B);而你去办户口本时,工作人员说需要先有身份证(Bean A)才能办理——两个证件的办理相互依赖,最终陷入死循环。
如果没有三级缓存机制, 这种场景只能靠人工重构代码来解决(比如提取中间Bean、使用@Lazy等),但这恰恰说明:Spring的三级缓存正是为了解决这个“鸡蛋相生”问题而设计的精妙机制。
二、核心概念:什么是三级缓存?
概念A:三级缓存(Three-level Cache)
标准定义:三级缓存是Spring在DefaultSingletonBeanRegistry中维护的三个Map集合,分别存放不同状态的Bean实例,通过协同工作解决单例Bean的循环依赖问题-1。
Spring用三个Map分别存放不同阶段的Bean:
| 级别 | 缓存名称 | 作用 |
|---|---|---|
| 一级缓存 | singletonObjects | 存放完全初始化完成的单例Bean(成品对象),供业务直接使用 |
| 二级缓存 | earlySingletonObjects | 存放提前暴露的半成品Bean(已实例化但未完成属性填充和初始化) |
| 三级缓存 | singletonFactories | 存放ObjectFactory(对象工厂),仅在调用getObject()时才会创建Bean实例 |
一句话记住:一级存成品,二级存半成品,三级存“将来可能生成代理”的工厂。
概念B:ObjectFactory(对象工厂)
标准定义:ObjectFactory是Spring定义的一个函数式接口,核心方法为getObject(),用于在需要时才真正创建或获取Bean实例。三级缓存存储的正是ObjectFactory<?>对象-1-4。
ObjectFactory是一个lambda表达式或匿名内部类,例如:
() -> getEarlyBeanReference(beanName, mbd, bean)这个工厂的作用是:在真正需要暴露早期引用时,才动态决定——是否要加AOP代理。如果Bean没有切面,就返回原始Bean;如果有@Transactional或@Async,就在这里生成代理对象。这样既避免了提前代理带来的副作用,又保证了最终暴露出去的对象和最终单例一致-4。
三、概念关系与区别:三级缓存如何协同工作?
三者的逻辑关系
| 对比维度 | 一级缓存(成品) | 二级缓存(半成品) | 三级缓存(工厂) |
|---|---|---|---|
| 存储内容 | 完整Bean实例 | 提前暴露的Bean引用 | ObjectFactory工厂 |
| 何时存入 | Bean初始化完成后 | 发生循环依赖时 | 实例化完成后立即存入 |
| 何时取出 | 每次getBean优先查 | 循环依赖时取早期引用 | 第一次被引用时调用getObject |
| 作用定位 | 结果存储 | 缓存中转 | 按需创建 |
一句话高度概括
一级缓存存最终成品,二级缓存存早期引用,三级缓存存工厂用于按需生成代理对象——三级缓存的核心职责是:把“要不要代理”的判断延迟到真正被其他Bean引用时才执行-4。
为什么一定要三级?两级不行吗?
这是一个高频面试追问点。简单来说:
如果只有一级+二级,在实例化Bean后就要立即决定是否创建代理对象并放入二级缓存。
但此时Bean还没有走到初始化阶段,无法判断是否需要AOP增强(比如
@PostConstruct方法里才加标记)。三级缓存把“要不要代理”延迟到第一次被其他Bean引用时才计算,既按需代理,又不破坏Bean的生命周期-4。
对于普通Bean(无AOP),两级缓存确实够用;但对于有AOP代理的Bean,三级缓存才是精髓所在-21。
四、代码示例:完整流程演示
场景:ServiceA依赖ServiceB,ServiceB依赖ServiceA(字段注入)
假设ServiceA和ServiceB相互依赖,Spring的创建流程如下:
Step 1:创建ServiceA
// 1. 实例化ServiceA(调用构造器,生成原始对象) Object aInstance = new ServiceA(); // 2. 将ServiceA的ObjectFactory放入三级缓存 singletonFactories.put("serviceA", () -> getEarlyBeanReference("serviceA", mbd, aInstance));
Step 2:填充ServiceA属性,发现需要ServiceB
// 3. 开始创建ServiceB // 实例化ServiceB,将其ObjectFactory放入三级缓存 singletonFactories.put("serviceB", () -> getEarlyBeanReference("serviceB", mbd, bInstance));
Step 3:填充ServiceB属性,发现需要ServiceA
// 4. 此时发生循环依赖!Spring执行getSingleton("serviceA", true) // 一级缓存:没有 → 二级缓存:没有 → 三级缓存:有! ObjectFactory<?> factory = singletonFactories.get("serviceA"); Object earlyA = factory.getObject(); // 可能返回原始对象或代理对象 earlySingletonObjects.put("serviceA", earlyA); // 升级到二级缓存 singletonFactories.remove("serviceA"); // 移除三级缓存 // 5. 将earlyA注入到ServiceB中,ServiceB完成属性填充和初始化 // 6. ServiceB放入一级缓存,移除二级缓存中的ServiceB
Step 4:ServiceA继续完成初始化
// 7. 从一级缓存获取ServiceB,注入到ServiceA // 8. ServiceA完成属性填充和初始化,放入一级缓存
整个流程可以用一句话总结:先实例化后暴露工厂,遇循环依赖时从工厂拿引用,最后完成初始化存入成品池-1。
五、底层原理与依赖支撑
三级缓存机制能够生效,底层依赖两个关键知识点:
1. Java的引用传递特性
Java中对象的引用是指针传递。当Bean A实例化完成后,它已经在堆内存中占据了一个地址。虽然此时它的属性(如serviceB)还是null,但这个对象的“地址”已经存在了。Spring正是利用了这一点:在属性填充之前就把对象的引用暴露出去,让其他Bean先用上,等引用方完成后,再回过头来填充原对象的属性-20。
2. 动态代理(JDK Proxy / CGLIB)
AOP代理(如@Transactional、@Async)是三级缓存存在的核心原因。在三级缓存的ObjectFactory中,通过getEarlyBeanReference方法判断Bean是否需要代理,如果需要,就在这里生成代理对象返回。这背后依赖的是Spring AOP的后置处理器机制,在Bean创建的生命周期中插入代理创建逻辑-4。
这两个底层知识点支撑了三级缓存的核心功能,后续的进阶文章可以深入探讨。
六、高频面试题与参考答案
Q1:Spring是如何解决循环依赖的?
参考答案(分层回答,面试官最想听的完整答案)
Spring通过三级缓存机制解决单例Bean的Setter注入和字段注入场景下的循环依赖问题。三级缓存分别是:
一级缓存(singletonObjects) :存放完全初始化完成的成品Bean;
二级缓存(earlySingletonObjects) :存放提前暴露的半成品Bean;
三级缓存(singletonFactories) :存放ObjectFactory工厂,用于按需生成代理对象。
核心思路是提前暴露正在创建中的Bean的引用。当Bean A依赖Bean B、Bean B又依赖Bean A时,Spring在实例化Bean A后,将它的ObjectFactory放入三级缓存,然后开始填充属性。发现需要Bean B时,去创建Bean B,Bean B实例化后同样将工厂放入三级缓存。当Bean B需要Bean A时,从三级缓存拿到A的工厂,生成早期引用放入二级缓存,完成B的初始化。最后A拿到B的完整实例,完成自己的初始化-31。
但需要注意:构造器注入和原型Bean的循环依赖无法解决,Spring会直接抛出异常。另外,Spring Boot 2.6+版本默认已禁止循环依赖,需要通过spring.main.allow-circular-references=true显式开启-31-13。
Q2:为什么一定要三级缓存?两级不行吗?
参考答案
对于普通Bean(无AOP),两级缓存确实可以解决循环依赖。但Spring需要同时支持AOP代理场景。
如果只用两级缓存,就需要在Bean实例化后立即决定是否创建代理对象并放入二级缓存。但此时Bean还没有走到初始化阶段,无法判断是否需要AOP增强(比如@PostConstruct方法里才加标记)。
三级缓存通过存储ObjectFactory,将“要不要代理”的判断延迟到第一次被其他Bean引用时才执行。这样既保证了AOP代理能按需生成,又不破坏Bean的生命周期。三级缓存的存在,本质是为了兼顾“循环依赖解决”和“AOP代理按需生成”两个需求-4-21。
Q3:哪些情况下Spring无法解决循环依赖?
参考答案(列举完整,展示全面性)
Spring无法解决以下三种循环依赖:
构造器注入循环依赖:构造器注入要求在实例化时就提供所有依赖,而循环依赖场景下两个Bean都无法完成实例化,Spring的三级缓存机制依赖于提前暴露早期引用,这在构造器注入场景下无法实现-13。
原型作用域(Prototype)循环依赖:Spring不缓存原型Bean,每次都是新对象,无法提前暴露引用-13。
多例循环依赖:涉及到多个Bean实例相互引用时,Spring也处理不了。
注意:Spring Boot 2.6+默认已禁止循环依赖,即使使用Setter/Field注入,也需要显式配置spring.main.allow-circular-references=true才能启动-31。
Q4:三级缓存之间是如何协同工作的?
参考答案
以A依赖B、B依赖A为例,协同流程如下:
创建A:实例化A → 将A的ObjectFactory放入三级缓存 → 开始填充属性,发现需要B
创建B:实例化B → 将B的ObjectFactory放入三级缓存 → 开始填充属性,发现需要A
命中循环:执行
getSingleton("A"),一级没有、二级没有、三级有 → 调用工厂获取A的早期引用 → 放入二级缓存 → 移除三级缓存 → 完成B的初始化并放入一级缓存完成A:回到A的属性填充,从一级缓存拿到B → 完成A的初始化并放入一级缓存
从源码角度看,核心逻辑在DefaultSingletonBeanRegistry.getSingleton()方法中:先从一级缓存找 → 没有且Bean正在创建中 → 再从二级缓存找 → 没有且允许提前引用 → 最后从三级缓存取工厂并创建早期引用-1。
Q5:如何通过配置或重构解决循环依赖?(实践题)
参考答案
如果项目中存在循环依赖,有几种解决方案:
临时兼容方案:在
application.properties中添加spring.main.allow-circular-references=true,恢复Spring旧版的三级缓存处理逻辑-12。推荐方案:重构代码消除循环依赖。将相互依赖的部分提取到第三个Bean中,或使用接口分离职责。这是最彻底的解决方案-。
使用@Lazy延迟加载:在循环依赖的一侧添加
@Lazy注解,Spring会注入一个代理对象,首次调用时才真正初始化,从而打破循环-40。Setter注入替代构造器注入:Spring可以处理Setter注入和字段注入的循环依赖,但无法处理构造器注入的循环依赖-40。
注意:虽然Spring提供了技术解决方案,但循环依赖通常是代码设计有问题的信号,实际项目中应优先考虑设计层面的优化-31。
七、结尾总结
核心知识点回顾
| 知识点 | 核心要点 |
|---|---|
| 三级缓存职责 | 一级存成品、二级存半成品、三级存工厂 |
| 为什么是三级 | 为了兼顾循环依赖解决和AOP代理按需生成 |
| 能解决的场景 | 单例 + Setter/字段注入 |
| 不能解决的场景 | 构造器注入、原型Bean |
| 底层依赖 | Java引用传递 + 动态代理 |
| Spring Boot 2.6+变化 | 默认禁止循环依赖,需显式开启 |
重点强调与易错点
重中之重:三级缓存的核心不是“三级”这个数字,而是每一级的职责和协作逻辑。一级存成品,二级存已确定的早期引用,三级存“将来可能生成代理”的工厂——逻辑清晰了,答案自然就对了。
易错点提醒
❌ 错误理解:三级缓存解决了所有循环依赖。→ ✅ 正确理解:只解决单例 + Setter/字段注入的循环依赖,构造器注入和原型Bean无法解决。
❌ 错误理解:三级缓存是为了性能优化。→ ✅ 正确理解:三级缓存是为了解决循环依赖问题,而不是性能优化,虽然它确实通过缓存提升了效率-。
预告下一篇内容
下一篇我们将深入探讨 Spring AOP的底层原理,从动态代理(JDK Proxy vs CGLIB)到@Transactional的事务传播行为,结合源码和实战案例,帮你彻底打通Spring核心知识链路。
本文由天天ai助手基于最新技术资料整理发布,数据截止2026年4月9日。