本文适合:Spring技术栈开发者、面试备考者
阅读收益:搞懂循环依赖的本质、掌握三级缓存机制、拿下高频面试题
在 Spring 框架体系中,循环依赖(Circular Dependency) 是面试中出镜率最高的核心考点之一,也是许多开发者“会用但说不清”的知识痛点。日常开发中,我们习惯性地使用 @Autowired 字段注入,项目顺利跑起来,却很少追问:当 A 依赖 B、B 依赖 A 时,Spring 到底是如何“破局”的?为什么换成构造器注入就报错了?Spring 2.6.0 之后默认禁用循环依赖又是什么原因?本文将逐一拆解,从痛点场景到三级缓存原理,从源码关键逻辑到高频面试题,带你建立完整的知识链路。

更新时间:北京时间 2026 年 4 月 10 日
适用版本:Spring Boot 3.4.x / 4.0.x 及以上
一、痛点切入:为什么会出现循环依赖?

先看一段“能跑起来”的代码:
@Service public class A { @Autowired private B b; // A 依赖 B } @Service public class B { @Autowired private A a; // B 依赖 A —— 形成闭环 }
项目启动成功,一切正常。但当你将 @Autowired 改成构造器注入后:
@Service public class A { private final B b; public A(B b) { this.b = b; } } @Service public class B { private final A a; public B(A a) { this.a = a; } }
启动时直接抛出 BeanCurrentlyInCreationException,报错信息以循环箭头图示标出 A ↔ B 的依赖闭环。-37
为什么同样的循环依赖,字段注入能过,构造器注入就过不了?
关键在于 Bean 创建的时机差异。Spring Bean 的创建分为三步:
实例化:调用构造器创建对象(此时对象已有内存地址,但属性均为默认值)
属性填充:注入依赖
初始化:执行
@PostConstruct等初始化方法
构造器注入将“实例化”与“属性填充”合并一步完成——这意味着在构造器中需要传入依赖对象时,该依赖对象必须已经完整可用。当 A 和 B 通过构造器互相依赖时,A 的实例化需要 B,B 的实例化需要 A,双方都无法先完成实例化,形成“死锁”。
而字段注入(以及 Setter 注入)则给了 Spring 一个缓冲空间:A 先完成实例化(此时 b 为 null),然后 Spring 在填充属性时去创建 B;B 实例化后发现自己依赖 A,此时 A 的“半成品”已经存在,可以直接拿来用。-12
二、核心概念讲解:三级缓存
Spring 解决单例 Bean 字段 / Setter 循环依赖的核心机制,就是“三级缓存”。 Spring 在 DefaultSingletonBeanRegistry 类中定义了三个 Map,分别存储处于不同状态的 Bean 实例:-11
| 缓存级别 | 缓存名称 | 作用 |
|---|---|---|
| 一级缓存 | singletonObjects | 存放完全初始化完成的单例 Bean(成品),供业务直接使用 |
| 二级缓存 | earlySingletonObjects | 存放提前暴露的半成品 Bean(已实例化但未完成属性填充和初始化) |
| 三级缓存 | singletonFactories | 存放 ObjectFactory(对象工厂),这是一个函数式接口,调用 getObject() 时才真正创建 Bean 实例 |
生活化类比:想象一个“新员工入职流程”。
三级缓存:人事部门收到一份“简历模板”,不急着打印,等你问“张三在哪”时才当场打印出来。
二级缓存:简历已经打印出来了,但还没录入系统和分配工位,是个“半成品”。
一级缓存:员工完成入职培训、分配到工位,成为“正式员工”,可以直接开展工作。
关键理解:三级缓存存的不是 Bean 实例,而是一个“能生产 Bean 实例的工厂函数”。这种“延迟创建”的设计,恰恰是解决 AOP 代理问题的关键。
三、关联概念讲解:二级缓存与三级缓存的关系
二级缓存 earlySingletonObjects 和三级缓存 singletonFactories 共同服务于“提前暴露”这一目标,但分工明确:
三级缓存:存的是
ObjectFactory<?>,即 “如何创建 Bean”的工厂,不主动创建,只有调用时才会生产。二级缓存:存的是实际的 Bean 实例(半成品),是三级缓存工厂的“产出结果”。
两者的流转关系是:当一个 Bean 完成实例化后,Spring 会将其包装成一个 ObjectFactory 放入三级缓存。当其他 Bean 在依赖注入时通过 getSingleton() 获取该 Bean:
先查一级缓存(成品)
没有则查二级缓存(早期暴露的半成品)
还没有则从三级缓存取出
ObjectFactory,调用getObject()创建实例,存入二级缓存,并移除三级缓存,最后返回该实例。-11
一句话概括:三级缓存负责“生产”,二级缓存负责“临时存储生产出来的半成品”,一级缓存负责“最终存放成品”。三级用完转二级,二级用完转一级。
四、概念关系与区别总结
| 对比维度 | 一级缓存 singletonObjects | 二级缓存 earlySingletonObjects | 三级缓存 singletonFactories |
|---|---|---|---|
| 存储内容 | 完整的 Bean 实例 | 半成品 Bean 实例 | ObjectFactory 工厂对象 |
| 何时放入 | 初始化完成后 | 从三级缓存生产后 | 实例化后、属性填充前 |
| 何时移除 | 长期保留 | 放入一级缓存时移除 | 转为二级缓存时移除 |
| 能否直接使用 | ✅ 可以 | ⚠️ 依赖尚未注入完成 | ❌ 只是工厂,需调用才生成 |
一句话记忆口诀:三级存工厂、二级存半成品、一级存成品,层层递进,逐步完善。
⚠️ 注意:三级缓存只能解决单例(Singleton)+ 字段注入 / Setter 注入场景下的循环依赖。构造器循环依赖、原型(Prototype)Bean 循环依赖均无法解决。-17
五、代码示例:三级缓存协作流程
以下是一个典型的循环依赖解决流程(以 A ↔ B 为例):
1. 开始创建 A: - 实例化 A → 将 A 的 ObjectFactory 放入三级缓存 - 填充 A 的属性,发现需要 B 2. 转去创建 B: - 实例化 B → 将 B 的 ObjectFactory 放入三级缓存 - 填充 B 的属性,发现需要 A - 调用 getSingleton("A"): · 一级缓存没有 A · A 正在创建中(满足条件) · 从三级缓存取出 A 的 ObjectFactory · 调用 getObject() 获得 A 的早期引用 · 将 A 的早期引用放入二级缓存 · 从三级缓存移除 A · 返回 A 的早期引用给 B - B 完成属性填充和初始化 - 将完整的 B 放入一级缓存 3. 回到 A 的创建: - A 获取到 B(此时 B 已是完整 Bean) - A 完成属性填充和初始化 - 将完整的 A 放入一级缓存 - 从二级缓存移除 A
核心洞察:正是“提前暴露 A 的半成品引用”这一机制,打破了 A 和 B 之间的创建僵局。A 尚未初始化完成,但 B 已经能拿到它的引用地址——Java 引用传递保证了最终指向的是同一个对象。-22
六、底层原理 / 技术支撑
Spring 的三级缓存机制底层依赖以下核心技术:
Java 引用传递:B 拿到的是 A 的引用地址,而非副本。即便 A 此时只是半成品,后续 A 完成初始化后,B 持有的引用自动指向完成态对象,无需额外更新。-22
ObjectFactory 函数式接口:
ObjectFactory是函数式接口,仅定义getObject()方法。Spring 将 Bean 的创建逻辑封装成 Lambda 表达式存入三级缓存,实现“按需创建、延迟执行”。AOP 代理的提前生成:如果 A 需要 AOP 代理(如事务管理、日志切面),Spring 必须在早期暴露时生成代理对象,而非等到初始化完成后才创建。三级缓存的
singletonFactories通过存储 Lambda 表达式,允许在getObject()调用时提前创建代理对象,从而保证循环依赖场景下 AOP 仍能正常工作。-12
追问:为什么不用两级缓存就够了?
答:若不用三级缓存,则需要在实例化后就立即生成 AOP 代理对象,而不是延迟到需要时再创建。这会破坏 AOP 的正常创建时机(通常是在 BeanPostProcessor 后置处理阶段),且会增大内存开销——每个 Bean 无论是否被循环依赖依赖,都要提前生成代理。三级缓存通过“按需创建”解决了这一设计矛盾。-22
七、高频面试题与参考答案
Q1:什么是循环依赖?Spring 能解决所有循环依赖吗?
标准答案:
循环依赖指两个或多个 Bean 相互持有对方引用,形成闭环依赖关系。Spring 只能解决单例模式 + 字段注入 / Setter 注入场景下的循环依赖,通过三级缓存提前暴露半成品 Bean。无法解决构造器循环依赖、原型 Bean 循环依赖以及多例模式下的循环依赖。-34-17
踩分点:定义 → 能解决的场景 → 不能解决的场景,缺一不可。
Q2:Spring 三级缓存分别存什么?为什么需要三级缓存?
标准答案:
一级缓存
singletonObjects:存放完全初始化完成的单例 Bean。二级缓存
earlySingletonObjects:存放提前暴露的半成品 Bean(已实例化,未初始化)。三级缓存
singletonFactories:存放ObjectFactory对象工厂,用于延迟创建 Bean 实例。
三级缓存的必要性:引入三级缓存的核心原因是处理 AOP 代理场景。若只用两级缓存,需要在实例化后就立即创建 AOP 代理对象,这会破坏 AOP 的创建时机,并导致不必要的性能开销。三级缓存通过存储工厂 Lambda 表达式,实现“按需创建代理对象”,是 Spring 设计哲学中“单一职责”和“延迟初始化”的体现。-12
Q3:构造器循环依赖为什么无法解决?
标准答案:
构造器注入将“实例化”与“属性填充”合并为一步。当 A 和 B 通过构造器互相依赖时,A 的实例化需要 B 实例,B 的实例化又需要 A 实例,双方都无法先完成实例化,形成“死锁”。Spring 的三级缓存依赖于“实例化后提前暴露引用”这一前提,而构造器注入在实例化阶段就要求依赖对象已完整可用,因此无法应用该机制。-17
Q4:Spring Boot 2.6.0 之后为什么默认禁用循环依赖?
标准答案:
Spring Boot 2.6.0 开始默认将 spring.main.allow-circular-references 设为 false,目的是引导开发者写出更健康的代码。循环依赖本质上是代码耦合过高的信号,长期依赖 Spring 的自动解决可能掩盖设计问题。若项目确实存在合理循环依赖,可通过配置 spring.main.allow-circular-references=true 临时开启,或使用 @Lazy 延迟注入解决。-30
Q5:@Lazy 注解是如何解决循环依赖的?
标准答案:
@Lazy 注解告诉 Spring 不要立即创建被注解的 Bean,而是返回一个代理对象。当真正调用该 Bean 的方法时,代理对象才会触发实际 Bean 的创建和初始化。通过这种方式,循环依赖中的一方被延迟到真正使用时才初始化,从而避免了启动时的创建死锁。-13
八、结尾总结
回顾全文,核心知识点如下:
| 知识点 | 要点 |
|---|---|
| 循环依赖定义 | Bean 之间相互引用形成闭环 |
| Spring 解决范围 | 单例 + 字段/Setter 注入 ✅;构造器/原型 ❌ |
| 三级缓存 | 一级存成品、二级存半成品、三级存工厂 |
| 为什么需要三级 | 解决 AOP 代理的提前创建问题 |
@Lazy 方案 | 延迟初始化,返回代理对象 |
| Spring Boot 2.6+ | 默认禁用,需显式开启或重构 |
易错点提醒:不要误以为 Spring 能解决所有循环依赖;构造器循环依赖直接报错,不是配置就能解决的。最佳实践:优先重构代码打破循环依赖,而非依赖框架的自动处理。
下一篇预告:深度解析 Spring AOP 底层原理——JDK 动态代理 vs CGLIB,从源码到面试一网打尽。