原创首发:2026年4月9日
目标读者:技术入门/进阶学习者、在校学生、面试备考者、Java开发工程师
文章定位:技术科普 + 原理讲解 + 代码示例 + 面试要点
核心目标:让读者理解Spring循环依赖的底层逻辑,看懂三级缓存的设计精妙,拿下面试高频考点
在Spring生态中,循环依赖(Circular Dependency) 是一个高频出现的问题,也是Java面试中被问到“翻来覆去”的核心考点。很多开发者日常写代码时依赖@Autowired字段注入,项目跑得风生水起,但当面试官追问“Spring是如何解决循环依赖的?为什么需要三级缓存而不是二级?”时,却答不上来,甚至把“三级缓存”当成了面试“标准答案”背一背就过去了。本文由AI助手演示演示技术资料、内容整合与结构化写作能力,带你从“只会用”到“懂原理”,层层拆解Spring循环依赖的底层机制,配合代码示例和面试高频题,帮助你建立完整的知识链路。

📌 本文涉及的所有数据、版本信息均来源于公开技术资料及官方文档,核心原理讲解基于Spring 5.3.x源码,时效性截至2026年4月。
一、痛点切入:为什么会出现循环依赖?

在Spring IoC容器中,Bean的创建分为三个关键阶段:实例化(实例化) → 填充属性(注入依赖) → 初始化-30。问题就出在第二阶段。
1.1 传统实现方式的困境
假设有UserService依赖OrderService,OrderService又依赖UserService,形成典型的循环依赖-2:
@Service public class UserService { @Autowired private OrderService orderService; } @Service public class OrderService { @Autowired private UserService userService; }
这段代码在日常开发中完全可以正常启动,但背后的“真相”并非如此简单。
1.2 为什么会报错?——构造器注入的崩溃
如果把上面的字段注入改为构造器注入:
@Service public class UserService { private final OrderService orderService; public UserService(OrderService orderService) { this.orderService = orderService; } } @Service public class OrderService { private final UserService userService; public OrderService(UserService userService) { this.userService = userService; } }
启动后直接报错:
APPLICATION FAILED TO START Description: The dependencies of some of the beans in the application context form a cycle: ┌─────┐ | userService defined in ... ↑ ↓ | orderService defined in ... └─────┘
-2
为什么会这样? 构造器注入要求在实例化时就传入所有依赖参数,这意味着UserService在创建之前必须先有OrderService实例,而OrderService在创建之前又必须有UserService实例——死锁了。
1.3 传统方式的本质缺陷
无论是构造器注入的“无法解决”还是字段注入的“神奇解决”,传统实现方式暴露了以下问题:
| 缺陷 | 说明 |
|---|---|
| 耦合度高 | 两个Bean之间形成了强依赖关系,职责边界模糊 |
| 理解成本高 | 开发者只知道“字段注入能跑通”,不知道背后原理 |
| 面试答不出 | 背了“三级缓存”四个字,却说不出为什么需要三级、二级够不够 |
| 版本变化敏感 | Spring Boot 2.6.x开始默认禁止循环依赖,传统写法直接报错- |
1.4 Spring的设计初衷
Spring引入三级缓存机制的核心目的,就是通过提前暴露半成品Bean的方式,在单例Bean的setter/字段注入场景下打破依赖闭环,让上述代码“跑起来”-13。但理解这个机制,需要先理清Spring Bean的完整生命周期。
二、核心概念讲解:Spring Bean的生命周期
2.1 什么是Bean的生命周期?
一个Spring Bean从创建到销毁,大致经历以下关键阶段-30:
读取BeanDefinition
实例化
new 对象
填充属性
@Autowired等
初始化
@PostConstruct等
放入一级缓存
成品Bean
销毁
记住一个关键点:实例化 ≠ 初始化。实例化只是调用了构造函数创建了对象(“半成品”),填充属性和初始化才是让Bean真正“完整”的过程。
2.2 为什么这个流程重要?
因为循环依赖问题的核心就在于:A在填充属性时需要B,但B还没创建完成;B在填充属性时又需要A,而A也还没创建完成。Spring的解决思路是:在A实例化之后、填充属性之前,提前把A的“半成品”暴露出去,让B可以先拿到A的引用-2。
三、关联概念讲解:三级缓存
3.1 三级缓存的定义
Spring在DefaultSingletonBeanRegistry类中维护了三个Map(缓存),用于存储不同状态的Bean-15:
| 缓存级别 | 缓存名称 | 数据类型 | 作用 |
|---|---|---|---|
| 一级缓存 | singletonObjects | Map<String, Object> | 存放完全初始化完成的单例Bean(成品),业务代码直接使用的Bean |
| 二级缓存 | earlySingletonObjects | Map<String, Object> | 存放提前暴露的半成品Bean(已实例化,未填充属性/初始化) |
| 三级缓存 | singletonFactories | Map<String, ObjectFactory<?>> | 存放ObjectFactory对象工厂,仅在调用getObject()时才创建Bean实例 |
3.2 三级缓存之间的关系
这三个缓存是互斥的——同一个Bean在任意时刻只会存在于其中一个缓存中,不会同时出现在多个缓存里-34。
Spring获取Bean时的读取顺序是:一级缓存 → 二级缓存 → 三级缓存-34。
// 源码来自 DefaultSingletonBeanRegistry private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 一级缓存 private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); // 二级缓存 private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); // 三级缓存
-15
四、概念关系与区别总结
为了帮助理解和记忆,这里用一句话概括三者关系:
三级缓存“管生产”(存放工厂,按需生成)→ 二级缓存“管过渡”(存放半成品)→ 一级缓存“管成品”(存放最终可用Bean)。
| 对比维度 | 三级缓存(singletonFactories) | 二级缓存(earlySingletonObjects) | 一级缓存(singletonObjects) |
|---|---|---|---|
| 存储内容 | 工厂对象(Lambda/函数式接口) | Bean实例(半成品) | Bean实例(成品) |
| 是否可被业务使用 | 否,需调用getObject() | 否,仅用于循环依赖中的早期注入 | 是,业务代码直接获取 |
| 存在阶段 | 实例化后→被消费前 | 三级缓存被消费后 | 初始化完成后 |
| 容量默认值 | 16 | 16 | 256 |
一句话帮助记忆:一级放成品,二级放半成品,三级放“工厂生产线”。
五、代码示例与流程演示
5.1 模拟循环依赖场景
假设有A依赖B,B依赖A的经典场景-15:
@Component public class A { @Autowired private B b; public void doA() { System.out.println("A do something"); } } @Component public class B { @Autowired private A a; public void doB() { System.out.println("B do something"); } }
5.2 如果没有三级缓存,会发生什么?
实例化A → 发现需要B → 实例化B → 发现需要A → 但A还没创建完 → 死循环 → 抛出异常5.3 有了三级缓存后的完整流程
下面以A→B→A的场景,逐步拆解Spring的处理过程-15:
| 步骤 | 操作 | 缓存变化 |
|---|---|---|
| Step 1 | 实例化A(调用构造函数) | 将A的ObjectFactory放入三级缓存 singletonFactories |
| Step 2 | 开始给A填充属性,发现需要B | 去容器中查找B,发现B不存在,开始创建B |
| Step 3 | 实例化B,将B的ObjectFactory放入三级缓存 | singletonFactories 中同时存在A和B的工厂 |
| Step 4 | 给B填充属性,发现需要A | 查找A:一级没有,二级没有,三级有 → 从三级缓存取出A的工厂,调用getObject()生成A的早期引用 |
| Step 5 | 将A的早期引用放入二级缓存 earlySingletonObjects | 从三级缓存移除A的工厂 |
| Step 6 | B拿到A的引用后,完成属性填充和初始化 | 将B放入一级缓存 singletonObjects,从二、三级缓存中移除B |
| Step 7 | A从“等待”中恢复,继续填充B | 此时B已经在一级缓存中,A直接拿到完整B |
| Step 8 | A完成填充和初始化 | 将A放入一级缓存,从二级缓存中移除A |
5.4 新旧实现方式对比
| 对比维度 | 无三级缓存(传统) | 有三级缓存(Spring) |
|---|---|---|
| 构造器注入 | ❌ 启动报错 | ❌ 仍然报错(无法解决) |
| setter/字段注入 | ❌ 循环依赖异常 | ✅ 正常启动 |
| 代理对象一致性 | 不适用 | ✅ 通过三级缓存保证同一代理对象 |
| 开发体验 | 需要手动规避 | 对开发者透明,自动处理 |
对比结论:三级缓存的核心价值不是“让所有循环依赖都能跑通”,而是在setter/字段注入场景下优雅地打破依赖闭环,同时保证AOP代理对象的唯一性。
六、底层原理与技术支撑
6.1 三级缓存的底层依赖
Spring解决循环依赖的底层依赖于以下几个关键设计:
① 实例化与初始化的分离
将Bean的创建过程拆分为“实例化”和“初始化”两个阶段,中间留出“提前暴露”的空间。这是三级缓存机制能起作用的前提-37。
② 函数式接口ObjectFactory
三级缓存存储的不是Bean实例,而是ObjectFactory函数式接口。调用getObject()方法时才会真正创建对象。这个设计允许将Lambda表达式作为工厂对象传入,在需要时才执行,实现了“懒创建”的效果-34。
// 三级缓存中存放的是这样的工厂对象 singletonFactories.put(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
③ AOP动态代理的支持
三级缓存引入的核心原因之一,就是为了支持AOP场景下的循环依赖。当Bean需要被代理时,三级缓存中的工厂返回的是代理对象而非原始对象,二级缓存则缓存这个代理对象,确保循环依赖中的多方拿到的是同一个代理对象,而不是多次生成的不同对象-63-。
6.2 为什么二级缓存不够用?
这是面试中的高频追问。简单来说:
没有AOP时:二级缓存确实足够解决循环依赖。
有AOP时:如果只用二级缓存,需要在实例化时就判断是否需要生成代理对象,这违反了Bean生命周期的设计原则(代理通常在初始化后生成)。且如果循环依赖中有多个Bean需要注入同一个代理对象,没有二级缓存会导致每次取到的地址值不一样-63。
三级缓存通过工厂模式延迟代理对象的生成时机,同时用二级缓存保证代理对象的唯一性,是兼顾设计优雅性与功能完备性的精妙方案。
七、高频面试题与参考答案
以下是Spring循环依赖相关的核心面试题,建议背诵以下答题要点:
Q1:Spring是如何解决循环依赖的?
参考答案(踩分点:一句话概括 + 三级缓存 + 限制条件)
Spring通过三级缓存机制解决单例Bean在setter/字段注入场景下的循环依赖问题-3。具体来说:
Spring维护了三个Map:
singletonObjects(一级缓存,放成品)、earlySingletonObjects(二级缓存,放半成品)、singletonFactories(三级缓存,放工厂);Bean实例化后,会将其工厂对象放入三级缓存,提前暴露;
当循环依赖发生时,另一方可以从三级缓存获取工厂,生成早期引用后放入二级缓存并完成注入;
最终双方都完成初始化后进入一级缓存。
局限性:构造器注入、原型Bean、多例Bean的循环依赖无法解决。
Q2:为什么需要三级缓存,二级不行吗?
参考答案(踩分点:AOP场景 + 代理对象唯一性 + 设计原则)
二级缓存在没有AOP的场景下确实足够解决循环依赖,但引入三级缓存主要有两个原因--63:
AOP代理支持:如果Bean需要被代理,代理对象的生成时机通常在初始化后。三级缓存中的工厂可以在被调用时才生成代理对象,避免过早创建;
保证代理对象唯一性:当多个地方注入同一个代理对象时,二级缓存确保大家拿到的是同一个对象(地址值一致),而如果每次从三级缓存工厂重新生成,会得到不同对象,导致运行时错误。
Q3:什么情况下Spring无法解决循环依赖?
参考答案(踩分点:三种不可解场景 + 原因)
Spring无法解决以下三种循环依赖场景-3-2:
构造器注入:构造函数在实例化时就需要依赖,无法“提前暴露”;
原型Bean(prototype) :Spring不缓存原型Bean,每次都是新对象,无法提前暴露;
多例Bean:涉及多个实例相互引用时同样无法解决。
Q4:Spring Boot 2.6.x版本之后有什么变化?
参考答案(踩分点:默认禁用 + 开启方式 + 设计理念)
从Spring Boot 2.6.x版本开始,循环依赖默认被禁用。如果项目中出现循环依赖,启动时会直接报错--3。如果需要临时开启,可以在配置文件中添加:
spring: main: allow-circular-references: true
但官方推荐的做法是重构代码,因为循环依赖通常意味着代码设计存在问题。
Q5:如何从设计层面彻底避免循环依赖?
参考答案(踩分点:4种重构方案)
虽然Spring提供了技术解决方案,但循环依赖通常是代码设计不合理的信号-3-46。推荐的重构方案:
提取公共逻辑:将相互依赖的逻辑提取到第三个Service;
使用接口抽象:让双方都依赖接口而非具体实现;
使用@Lazy延迟加载:临时方案,在其中一个依赖上添加
@Lazy注解;事件驱动解耦:使用Spring的
ApplicationEvent将直接调用改为事件发布/订阅。
八、结尾总结
核心知识点回顾
| 知识点 | 一句话总结 |
|---|---|
| 循环依赖定义 | 两个或多个Bean相互持有对方引用,形成闭环-13 |
| Spring解决方式 | 三级缓存机制,提前暴露半成品Bean |
| 三级缓存作用 | 一级存成品、二级存半成品、三级存工厂 |
| 关键限制 | 构造器注入、原型Bean、多例Bean无法解决 |
| 版本变化 | Spring Boot 2.6.x开始默认禁用循环依赖 |
| 设计建议 | 重构优于技术规避,循环依赖通常是设计问题信号 |
重点强调与易错点
⚠️ 不要误以为Spring能解决所有循环依赖——构造器注入的循环依赖依然会报错,这是面试中最容易混淆的点。
⚠️ 三级缓存的核心价值不是“性能”,而是“AOP支持” ——很多开发者答错这一点。
⚠️ Spring Boot 2.6.x后的默认行为变化——如果面试官问到版本问题,能答出这一点的考生明显更有竞争力。
⚠️ 面试回答要“先讲技术实现,再说限制条件,最后给出设计建议” ——这种结构最能体现思维全面性-3。
进阶预告
下一篇文章我们将深入Spring AOP的底层原理,解析JDK动态代理和CGLIB的区别,以及AOP与循环依赖之间的深层关联。敬请期待!
版权声明:本文由AI助手演示生成,内容基于公开技术资料整理,仅供学习交流使用。文中涉及的Spring源码分析基于Spring 5.3.x版本,各版本间可能存在差异,请以实际使用的版本为准。