本文导读:循环依赖是Spring面试中出场率超过5%的核心考点-,却让无数开发者“知其然而不知其所以然”。方舟AI助手今天就从痛点出发,带你打通“问题→概念→原理→面试”的完整链路,一篇搞定Spring循环依赖。
一、开篇引入:为什么循环依赖是Spring必考点

在Spring IoC容器中,循环依赖(Circular Dependency)是一个高频出现的问题,也是各大厂面试中的核心考点-1。所谓循环依赖,指的是两个或多个Bean之间互相持有对方的引用,形成闭环依赖关系——最典型的就是Bean A依赖Bean B,同时Bean B又依赖Bean A-1。
不少开发者在日常开发中会遇到这样的困境:

代码跑得好好的,突然启动时报
BeanCurrentlyInCreationException,一脸懵面试时只知道“三级缓存”四个字,但说不清每一级存的是什么
知道用
@Lazy能解决,但说不明白为什么被追问“为什么需要三级,二级不够吗”就卡住了
这些痛点的根源在于:只停留在“会用”层面,没有建立完整的知识链路。
本文将从问题剖析→核心概念→关联关系→代码示例→底层原理→面试要点六个维度,由浅入深地带你彻底搞懂Spring循环依赖。无论你是技术入门者、面试备考者,还是经验丰富的开发工程师,这篇文章都将帮你建立清晰的知识体系。
二、痛点切入:为什么需要循环依赖解决机制
2.1 传统依赖注入的困境
先看一个最简单的循环依赖场景:
@Component public class A { @Autowired private B b; // A依赖B } @Component public class B { @Autowired private A a; // B依赖A,形成循环 }
在不做特殊处理的情况下,Spring启动时会按以下流程尝试创建这两个Bean:
开始创建A → 实例化A(调用构造函数生成原始对象)→ 填充属性,发现需要B
容器中找不到B,于是开始创建B → 实例化B → 填充属性,发现需要A
容器中找不到A,于是尝试创建A → 此时A正在创建中,无法重新创建 → 抛出
BeanCurrentlyInCreationException异常-1
2.2 旧有方式的缺陷
如果没有Spring的三级缓存机制,面对这种相互依赖的场景,开发者只能采取以下几种“笨办法”:
手动拆分类,将相互依赖的逻辑提取到第三方类
使用getter/setter延迟获取,在第一次使用时才去容器中获取
使用setter注入并在XML中配置依赖顺序
这些方式的共同缺陷是:耦合度高、侵入性强、可维护性差。 而Spring的设计初衷恰恰是“让开发者专注于业务,把依赖管理的复杂性交给框架”。这也是三级缓存机制诞生的核心动机——在框架层面透明地解决循环依赖,对开发者零侵入。
三、核心概念讲解:什么是循环依赖
3.1 标准定义
循环依赖(Circular Dependency),英文全称 Circular Dependency,是指在Spring IoC容器管理的Bean之间,存在一个相互依赖的“环”。例如,Bean A的创建需要注入Bean B,而Bean B的创建又需要注入Bean A,导致容器无法确定谁应该先被完全初始化-。
3.2 关键拆解
要理解循环依赖,需要抓住三个关键点:
依赖方向:依赖必须是双向的(A→B且B→A),单向依赖不会构成循环
作用域限定:Spring能解决的循环依赖仅限于单例(Singleton)作用域,原型(Prototype)作用域的循环依赖无法解决-55
注入方式限定:Spring能解决的仅限于setter注入或字段注入(即属性填充阶段发生的依赖),构造器注入无法通过三级缓存解决-55
3.3 生活化类比
想象一个场景:A和B同时要办身份证,但办证中心要求必须出示对方的担保函才能办理。
传统的做法是:A等B先办完拿到担保函,B等A先办完拿到担保函——两人互相等待,永远办不完
Spring的做法是:A先领一张“承诺函”(半成品引用),B拿着这张承诺函去办证,办完后回来把真正的担保函给A——两人都能办完
这个“承诺函”就是Spring三级缓存中的“提前暴露的半成品Bean”。
四、关联概念讲解:三级缓存的三个层次
4.1 标准定义
三级缓存(Three-Level Cache),英文对应 singletonObjects、earlySingletonObjects、singletonFactories,是Spring框架为解决单例Bean循环依赖而设计的三个核心Map缓存结构-1。
4.2 三层次详解
| 缓存级别 | 缓存名称 | 存储内容 | 作用 |
|---|---|---|---|
| 一级缓存 | singletonObjects | 完全初始化完成的Bean(成品) | 供业务直接使用,所有getBean()优先从此获取 |
| 二级缓存 | earlySingletonObjects | 提前暴露的半成品Bean(已实例化,未填充属性) | 解决循环依赖时,让依赖方能拿到“半成品”引用 |
| 三级缓存 | singletonFactories | ObjectFactory对象工厂(Lambda表达式) | 按需生成Bean实例,延迟决定是否创建AOP代理 |
4.3 三级缓存与循环依赖的关系
循环依赖是“问题” ,需要一种机制来打破依赖闭环
三级缓存是“解决方案” ,通过提前暴露半成品引用来打破闭环
三级缓存中的三级缓存(
singletonFactories)是最关键的设计,它不直接存储Bean对象,而是存储一个“工厂”,等到真正需要暴露引用时才调用工厂生成对象-2
五、概念关系与区别总结:一句话串起逻辑
循环依赖是“病”,三级缓存是“药”;三级缓存是循环依赖的具体解决方案,循环依赖是三级缓存要解决的核心问题。
更精确地说:
| 维度 | 循环依赖 | 三级缓存 |
|---|---|---|
| 本质 | 问题/现象 | 解决方案/机制 |
| 触发条件 | 双向依赖关系 | 检测到循环依赖时启用 |
| 核心操作 | 依赖闭环 | 提前暴露半成品引用 |
| 适用场景 | 单例+setter/字段注入 | 同左 |
记忆口诀:一级存成品,二级存半成品,三级存工厂;依赖出现时,工厂变半成品,半成品变成品-2。
六、代码示例:从现象到解决
6.1 场景:循环依赖的复现
@Component public class ServiceA { @Autowired private ServiceB serviceB; // A依赖B public void doSomething() { System.out.println("ServiceA执行中"); serviceB.execute(); } } @Component public class ServiceB { @Autowired private ServiceA serviceA; // B依赖A,形成循环 public void execute() { System.out.println("ServiceB执行中"); serviceA.doSomething(); } }
启动结果:Spring Boot 2.6+版本默认禁止循环依赖,直接抛出异常-54:
APPLICATION FAILED TO START Description: The dependencies of some of the beans in the application context form a cycle: serviceA (field private com.example.ServiceB com.example.ServiceA.serviceB) serviceB (field private com.example.ServiceA com.example.ServiceB.serviceA)
6.2 Spring的三级缓存解决流程(以A↔B为例)
假设Spring容器开始创建Bean A和Bean B,详细步骤如下:
步骤1:开始创建A
实例化A(调用构造函数,生成原始对象,此时
b=null)将A的
ObjectFactory放入三级缓存singletonFactories开始填充A的属性,发现需要注入B
步骤2:开始创建B
实例化B,生成原始对象
将B的
ObjectFactory放入三级缓存singletonFactories填充B的属性,发现需要注入A
在容器中查找A → 一级缓存无 → 二级缓存无 → 从三级缓存获取A的
ObjectFactory,调用后生成A的早期引用将A的早期引用放入二级缓存
earlySingletonObjects,同时移除三级缓存中的A用二级缓存中的A引用完成B的属性注入,B继续完成初始化
B初始化完成后,存入一级缓存
singletonObjects,清除二三级缓存中的B
步骤3:完成A的创建
A的属性注入继续,此时从一级缓存拿到B(已完成)
A完成初始化,存入一级缓存
singletonObjects,清除二三级缓存中的A
核心要点:B在属性填充时拿到的是A的“早期引用”(已实例化但未完全初始化的A),这个引用虽然是“半成品”,但Java的引用传递机制保证了后续A完成初始化后,B持有的引用自动指向完整对象。
6.3 新旧对比:直观展示改进效果
| 维度 | 传统做法 | Spring三级缓存 |
|---|---|---|
| 代码侵入 | 需要手动拆分或使用延迟加载技巧 | 零侵入,正常写@Autowired即可 |
| 可维护性 | 依赖关系复杂,修改成本高 | 框架透明处理,开发者专注业务 |
| 性能开销 | 每次获取都需要额外处理 | 仅首次创建时有Map操作开销 |
| 是否支持双向依赖 | 需手动规避 | 自动支持(限定场景内) |
6.4 解决方案:调整注入方式
将构造器注入改为setter注入或字段注入,Spring就能正常处理:
@Component public class A { private B b; @Autowired // 字段注入,支持循环依赖 public void setB(B b) { this.b = b; } } @Component public class B { private A a; @Autowired // 字段注入,支持循环依赖 public void setA(A a) { this.a = a; } }
七、底层原理支撑
三级缓存机制能够优雅地解决循环依赖,底层依赖以下几个关键技术:
7.1 Java引用传递机制
Java中的对象引用本质上是一个指向堆内存地址的指针。当B持有A的早期引用时,B只是拿到了A在堆中的“地址”。后续A完成初始化后,同一地址上的内容变为完整对象,B持有的引用自动“看到”了完整内容-。
7.2 反射机制
Spring通过反射(Reflection)实现Bean的实例化和属性注入。Constructor.newInstance()和Field.set()是属性填充阶段的核心操作。反射让Spring可以在编译时未知类结构的情况下动态操作对象。
7.3 动态代理(AOP相关)
当Bean需要被AOP代理(如添加@Transactional)时,Spring不会在实例化时就创建代理,而是通过三级缓存中的ObjectFactory在第一次被其他Bean引用时才按需创建代理对象-2。这正是三级缓存必须存在而非二级缓存的核心原因。
7.4 工厂模式
三级缓存中的singletonFactories本质上是工厂设计模式的体现——它不直接存储对象,而是存储一个ObjectFactory(函数式接口),在getObject()被调用时才真正创建对象-。这种“延迟创建”的设计既解决了循环依赖,又为AOP代理保留了灵活性。
7.5 为什么必须用三级缓存?二级不够吗?
这是面试中的终极追问。答案是:如果只是解决简单的循环依赖(无AOP代理),二级缓存确实够用。
但Spring必须支持AOP场景。假设只有二级缓存:
Bean A实例化后,需要立即决定是否创建代理并放入二级缓存
但此时还没走到
BeanPostProcessor的初始化后处理阶段,无法知道A是否需要AOP增强如果提前创建了代理,可能和最终代理对象不一致;如果不创建代理,B拿到的就是原始对象而非代理对象
三级缓存将“要不要代理”的决策延迟到第一次被引用时,通过ObjectFactory中的getEarlyBeanReference()方法按需生成代理,保证了无论何时暴露出去的对象都和最终对象一致-2-。
八、高频面试题与参考答案
面试题1:Spring是如何解决循环依赖的?三级缓存分别是什么?
标准答案(踩分点) :
核心机制:Spring通过三级缓存(Three-Level Cache)机制解决单例Bean的setter/字段注入循环依赖问题-13。
三级缓存定义:
一级缓存
singletonObjects:存放完全初始化完成的成品Bean二级缓存
earlySingletonObjects:存放提前暴露的半成品Bean三级缓存
singletonFactories:存放ObjectFactory对象工厂
解决思路:在Bean实例化后、属性填充前,将其
ObjectFactory放入三级缓存。当出现循环依赖时,从三级缓存获取工厂生成早期引用,放入二级缓存供依赖方使用-55。适用范围:仅适用于单例作用域 + setter/字段注入的场景。
面试题2:为什么需要三级缓存?二级缓存不够吗?
标准答案(高分版) :
二级缓存可以解决简单的循环依赖,但无法正确处理AOP代理场景-13。
详细分析:
如果只有二级缓存,Bean实例化后就必须决定是否创建代理对象并放入缓存
但此时尚未执行
BeanPostProcessor的初始化后处理,无法确定是否需要AOP增强三级缓存通过存储
ObjectFactory,将代理对象的创建延迟到第一次被其他Bean引用时,通过getEarlyBeanReference()按需决定返回原始对象还是代理对象这保证了循环依赖中暴露的对象与最终单例池中的对象完全一致
一句话概括:三级缓存的核心价值是兼顾循环依赖破局与AOP代理的正确性。
面试题3:构造器注入的循环依赖为什么解决不了?
标准答案:
构造器注入要求在实例化阶段就提供所有依赖,而循环依赖场景下两个Bean的构造器相互等待对方完成实例化,形成死锁-13。三级缓存机制依赖于“实例化后、属性填充前”这个时间窗口来提前暴露半成品引用,但构造器注入的依赖在实例化阶段就被触发了,根本来不及走到暴露引用的步骤。
解决方案:改用setter注入或字段注入,或使用@Lazy注解延迟加载。
面试题4:Spring Boot 2.6+版本中循环依赖发生了什么变化?
标准答案:
从Spring Boot 2.6.0开始,默认禁止了循环依赖。如果项目中存在循环依赖,启动时会直接抛出异常并拒绝启动-。
原因:官方认为循环依赖本质上是“代码坏味道”,应当通过重构代码来消除而非依赖框架解决。
解决方案:
重构代码(推荐):提取公共逻辑到新Service,采用单向依赖
使用
@Lazy注解:延迟初始化,打破创建顺序依赖临时回退:在配置文件中设置
spring.main.allow-circular-references=true(不推荐)
面试题5:原型(prototype)作用域的循环依赖能解决吗?
标准答案:
不能。Spring只在单例作用域下通过缓存机制解决循环依赖。原型作用域的Bean每次获取都会创建新实例,没有缓存可以复用,Spring无法提前暴露“半成品”引用-55。遇到原型作用域的循环依赖,会直接抛出异常。
九、结尾总结
9.1 核心知识点回顾
| 知识点 | 核心内容 |
|---|---|
| 循环依赖定义 | Bean之间形成双向依赖闭环 |
| 三级缓存 | singletonObjects(成品)、earlySingletonObjects(半成品)、singletonFactories(工厂) |
| 适用范围 | 单例 + setter/字段注入 |
| 不适用场景 | 构造器注入、原型作用域、AOP代理(三级缓存已解决) |
| 解决原理 | 实例化后提前暴露半成品引用,打破依赖闭环 |
| 底层支撑 | Java引用传递、反射、工厂模式、动态代理 |
9.2 重点与易错点提醒
✅ 易错点1:三级缓存并非“三个缓存同时存同一个Bean”,而是Bean在不同生命周期存放在不同级别的缓存中
✅ 易错点2:Spring能解决的循环依赖仅限于单例+setter/字段注入,不要认为“Spring能解决所有循环依赖”
✅ 易错点3:Spring Boot 2.6+默认禁止循环依赖,生产项目中遇到循环依赖优先考虑重构而非开启配置
✅ 面试踩分关键:答出三级缓存的三个Map名称、各自存储内容、以及为什么需要三级而非两级(AOP场景)
9.3 进阶预告
本文完整梳理了Spring循环依赖的核心原理与三级缓存机制。如果读者想进一步深入,后续文章将围绕以下方向展开:
Spring Bean完整生命周期:从BeanDefinition到最终Bean的12步全过程
Spring AOP底层原理:JDK动态代理 vs CGLIB,代理对象的创建时机
循环依赖与AOP的深度结合:
getEarlyBeanReference()的源码级分析
本文由方舟AI助手整理,数据截止2026年4月11日。如需获取最新技术资讯或深度源码分析,欢迎持续关注。