更新时间:2026年4月10日 10:30 · 北京
【天工AI助手体验】 据2026年Java生态调研报告数据,约23%的Spring应用开发者曾遇到过循环依赖问题,字段注入导致的循环依赖占比高达67%-29。无论你是在校学生准备面试,还是技术入门者初学Spring框架,理解循环依赖的底层原理,都是通往Spring进阶之路上一道绕不开的门槛。

一、开篇引入:为什么循环依赖是Spring必学知识点
循环依赖(Circular Dependency)是Spring IoC容器中的核心高频考点,也是大厂面试中考察候选人框架理解深度的“试金石”。很多开发者在使用Spring时,可能遇到过项目启动失败、控制台打印BeanCurrentlyInCreationException异常的经历,却搞不清楚Spring到底是如何解决这个问题的-1。

本文将从概念定义、痛点分析、三级缓存机制、源码解析到面试考点,层层递进,带你彻底搞懂Spring解决循环依赖的底层原理。
二、痛点切入:传统实现方式的问题
什么是循环依赖?
循环依赖,简单来说,就是两个或多个Bean之间互相持有对方的引用,形成了一个闭环依赖关系-1。最典型的场景如下:
@Component public class ServiceA { @Autowired private ServiceB serviceB; // A依赖B } @Component public class ServiceB { @Autowired private ServiceA serviceA; // B依赖A,形成循环 }
不处理循环依赖会发生什么?
在默认情况下,如果Spring不做特殊处理,项目启动时就会抛出BeanCurrentlyInCreationException异常-1。让我们来剖析一下Spring创建Bean的三个关键阶段:
| 阶段 | 说明 | 完成后的状态 |
|---|---|---|
| 实例化 | 调用构造函数创建对象实例 | 半成品(属性仍为null) |
| 属性填充 | 注入依赖的属性(@Autowired生效) | 正在装配 |
| 初始化 | 调用初始化方法,完成AOP代理等 | 成品Bean |
当ServiceA依赖ServiceB、ServiceB又依赖ServiceA时,两个Bean互相等待对方先完成创建,形成了“先有鸡还是先有蛋”的死锁困境-29。
二、核心概念讲解(一):三级缓存
三级缓存的定义
为了解决单例Bean的循环依赖问题,Spring设计了三级缓存机制(Three-Level Cache) ,通过提前暴露半成品Bean的方式打破依赖闭环-1。
三级缓存的三个核心组件定义如下:
| 缓存级别 | 缓存名称 | 英文全称 | 作用 |
|---|---|---|---|
| 一级缓存 | singletonObjects | Singleton Objects | 存放完全初始化完成的成品Bean |
| 二级缓存 | earlySingletonObjects | Early Singleton Objects | 存放提前暴露的半成品Bean |
| 三级缓存 | singletonFactories | Singleton Factories | 存放ObjectFactory对象工厂 |
生活化类比理解
把三级缓存想象成“快餐店的备餐流程” :
一级缓存(成品) :顾客可以直接取走的完成品套餐
二级缓存(半成品) :已经切好食材、半熟状态的备料
三级缓存(工厂) :存放着“如何制作这道菜”的配方卡,需要时才按配方制作
当两个顾客互相等待对方的餐食时(循环依赖),厨师就可以先拿出配方卡(三级缓存),快速备好半成品给另一方先用,等主菜完成后最终补上成品。
三、核心概念讲解(二):源码层面的三级缓存
源码中的缓存定义
Spring处理Bean创建的核心逻辑集中在DefaultSingletonBeanRegistry类中-1:
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry { // 一级缓存:存放完全初始化好的单例Bean(成品) private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 三级缓存:存放Bean的工厂对象 private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); // 二级缓存:存放提前暴露的Bean实例(半成品) private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); // 记录当前正在创建的Bean名称 private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16)); }
为什么三级缓存要存ObjectFactory而不是直接存对象?
这是理解Spring循环依赖机制的关键问题。三级缓存存储的是ObjectFactory(对象工厂) ,这是一个函数式接口,只有调用getObject()时才会真正创建Bean实例-1。
这样做的好处在于:
按需生成代理:如果Bean需要AOP代理(如
@Transactional、@Async),就在这里动态生成代理对象-2延迟决策:代理的生成时机延迟到第一次被其他Bean引用时才执行,避免过早代理带来的副作用-2
四、概念关系与区别总结
三级缓存的核心协作逻辑
| 缓存 | 存放内容 | 访问优先级 | 何时移出 |
|---|---|---|---|
| 一级缓存 | 成品Bean | 最高(先查) | 不移出(单例池) |
| 二级缓存 | 半成品Bean | 次之 | Bean初始化完成后 |
| 三级缓存 | ObjectFactory工厂 | 最后 | 工厂被调用后移出 |
一句话概括:一级缓存存“成品”,二级缓存存“已确定的早期引用”,三级缓存存“将来可能生成代理的工厂”-2。
五、代码示例演示:完整的循环依赖解决流程
示例代码
以ServiceA依赖ServiceB、ServiceB依赖ServiceA为例:
@Service public class ServiceA { @Autowired private ServiceB serviceB; public void doA() { serviceB.doSomething(); } } @Service public class ServiceB { @Autowired private ServiceA serviceA; public void doB() { serviceA.doSomething(); } }
核心流程解析
Spring的getSingleton()方法是解决循环依赖的入口-1:
@Nullable public Object getSingleton(String beanName, boolean allowEarlyReference) { // 步骤1:优先从一级缓存获取成品Bean Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { // 步骤2:从二级缓存获取提前暴露的半成品Bean singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { synchronized (this.singletonObjects) { // 双重检查 // 步骤3:从三级缓存获取ObjectFactory并生成早期引用 ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return singletonObject; }
完整创建流程
创建ServiceA:实例化A,将A的工厂对象放入三级缓存
注入依赖B:发现A需要B,开始创建B
创建ServiceB:实例化B,将B的工厂对象放入三级缓存
B需要A:给B注入属性时发现需要A,从三级缓存获取A的工厂,生成A的早期引用,放入二级缓存
完成B创建:B拿到A的引用后完成初始化,放入一级缓存
完成A创建:A接着完成初始化,也放入一级缓存-25
六、底层原理与技术支撑
Spring循环依赖解决机制底层依赖以下核心技术点:
反射机制(Reflection) :通过
createBeanInstance方法反射调用构造函数创建Bean实例代理模式(Proxy Pattern) :AOP代理的生成依赖JDK动态代理或CGLIB代理
工厂模式(Factory Pattern) :ObjectFactory本质上是工厂模式的函数式实现
三级缓存的并发控制:通过
synchronized和ConcurrentHashMap保证线程安全
七、高频面试题与参考答案
面试题1:Spring是如何解决循环依赖的?
标准答案:Spring通过三级缓存机制解决了单例Bean的Setter/Field注入场景下的循环依赖。当A依赖B、B依赖A时,Spring在创建A实例后将其ObjectFactory放入三级缓存;当B需要注入A时,从三级缓存获取ObjectFactory生成A的早期引用并放入二级缓存;B完成初始化后放入一级缓存;A接着完成初始化,完成循环依赖的打破-25。
面试题2:为什么要用三级缓存?两级缓存不够吗?
标准答案:两级缓存无法解决AOP代理场景下的循环依赖问题。如果只有二级缓存,需要在实例化时立即决定是否生成代理对象,但此时尚未走到初始化阶段,无法判断是否需要增强。三级缓存通过ObjectFactory将代理生成延迟到第一次被其他Bean引用时才执行,既按需代理,又不破坏生命周期,保证了最终暴露的对象与最终单例一致-2-9。
面试题3:构造器注入的循环依赖为什么解决不了?
标准答案:构造器注入要求在实例化时就提供所有依赖,而循环依赖的场景下,两个Bean都无法完成实例化。Spring的三级缓存机制依赖于提前暴露早期引用,这在构造器注入的场景下无法实现,因为实例化阶段尚未完成,无法提前暴露引用-25-4。
面试题4:哪些循环依赖场景Spring无法解决?
标准答案:①构造器注入的循环依赖;②多例(prototype)作用域的Bean;③循环中使用了@Async注解的Bean(代理对象与原始对象不一致导致报错)-19-12。
面试题5:Spring Boot 2.6之后循环依赖默认行为有何变化?
标准答案:从Spring Boot 2.6开始,为了鼓励更清晰的代码设计,默认已禁用循环依赖,需要在application.properties中设置spring.main.allow-circular-references=true才能开启。构造器注入的循环依赖即使在配置开启后也无法解决-25-41。
八、结尾总结
回顾全文,Spring循环依赖的核心知识点可以浓缩为以下要点:
| 知识点 | 核心内容 |
|---|---|
| 概念 | Bean之间相互依赖形成闭环 |
| 解决方案 | 三级缓存机制(singletonObjects、earlySingletonObjects、singletonFactories) |
| 适用场景 | 单例Bean + Setter/字段注入 |
| 不适用场景 | 构造器注入、多例Bean、@Async循环依赖 |
| 底层支撑 | 反射、代理、工厂模式 |
| Spring Boot 2.6+ | 默认禁止,需手动开启 |
面试应答技巧:面试时建议按照“先说结论→阐述三级缓存流程→说明限制条件→补充设计优化建议”的逻辑层次递进,既能展示技术深度,又能体现系统思考能力-25。
下篇预告:下一篇将深入讲解Spring AOP的底层原理——JDK动态代理与CGLIB的区别与实现机制,敬请期待。