2026年4月10日 北京时间
开篇引入

循环依赖(Circular Dependency)在Spring框架中的地位,堪称面试中最“高频”且最容易“翻车”的知识点之一。简单来说,当Bean A依赖Bean B,而Bean B又反过来依赖Bean A时,便形成了一个“相互等待”的闭环。
不少开发者在日常工作中天天用Spring,却只知其然不知其所以然——遇到项目启动抛出BeanCurrentlyInCreationException时,要么一头雾水,要么盲目添加@Lazy草草了事;到了面试环节,虽然能背出“三级缓存”四个字,却说不清为什么需要三级、二级缓存到底够不够、构造器注入为何无法解决-22。

本文将从痛点切入 → 概念拆解 → 关系梳理 → 代码示例 → 底层原理 → 高频面试题这一完整链路,帮你彻底搞懂Spring循环依赖的来龙去脉。无论你是技术入门/进阶学习者、在校学生、面试备考者,还是相关技术栈开发工程师,这篇文章都将兼顾易懂性与实用性,让你读完后既能写出正确代码,也能在面试中对答如流。
一、痛点切入:为什么需要三级缓存?
先看一段“会出问题”的代码:
// 构造器注入——Spring 无法解决循环依赖 @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; } }
启动项目,Spring容器直接抛出:
┌─────┐ | a defined in ... ↑ └─────┘ ↓ | b defined in ... └─────┘ BeanCurrentlyInCreationException
传统/旧有实现方式的痛点分析:
构造器注入循环依赖:要求在实例化时就提供所有依赖,而循环依赖场景下两个Bean都无法完成实例化,Spring会直接抛出异常-11。
原型(prototype)作用域:Spring不缓存原型Bean,每次请求都是新对象,无法提前暴露引用,因此也无法解决循环依赖-11。
代码设计信号:循环依赖往往是代码设计存在缺陷的信号——模块之间耦合过高、职责边界不清-23。
危害深远:未妥善处理的循环依赖会导致应用启动失败、内存泄漏(部分半成品Bean无法被GC回收)、调试困难、测试障碍等问题-1。
正是为了解决上述问题,Spring框架设计者才在DefaultSingletonBeanRegistry类中引入了三级缓存机制——通过提前暴露“半成品Bean”的方式,优雅地打破了依赖闭环-1。
二、核心概念讲解:什么是“循环依赖”与“三级缓存”?
2.1 循环依赖
标准定义:循环依赖(Circular Dependency)是指两个或多个Bean之间互相持有对方的引用,形成闭环依赖关系-6。
场景化类比:就像两个人同时等对方先开口——A说“等B先说话我就说”,B说“等A先说话我就说”,结果谁也没法开口,陷入死锁。
2.2 三级缓存
标准定义:三级缓存是Spring容器内部维护的三个Map集合,用于存储单例Bean在不同创建阶段的状态-6。
| 缓存级别 | 缓存名称 | 存储内容 | 作用 |
|---|---|---|---|
| 一级缓存 | singletonObjects | 完全初始化完成的单例Bean(已注入所有属性、完成初始化方法) | 对外提供“成品”Bean |
| 二级缓存 | earlySingletonObjects | 提前曝光的“半成品”Bean(已实例化,未完成属性注入) | 存储早期引用,避免重复生成 |
| 三级缓存 | singletonFactories | ObjectFactory对象工厂 | 延迟生成早期引用,支持AOP代理 |
场景化类比:可以把三级缓存想象成工厂的“流水线跟踪系统”——一级缓存是“成品仓库”(贴好标签可直接发货),二级缓存是“半成品暂存区”(框架搭好了但零件还没装),三级缓存是“模具登记表”(存着每个产品如何被制造出来的配方,需要时才调用)-。
三、关联概念讲解:为什么需要三级,两级不够吗?
3.1 核心概念辨析
如果仅仅是为了解决循环依赖,二级缓存其实就够了。只要在Bean实例化(new)之后,不管它需不需要AOP,都直接把它的代理对象生成出来放到二级缓存里,B就可以拿到了-。
3.2 第三级缓存的核心价值
那么第三级缓存singletonFactories存在的意义是什么?答案藏在AOP代理中。
如果Bean需要被AOP代理(比如加了@Transactional或@Async),代理对象必须在属性注入阶段就得准备好。如果只暴露原始对象,注入给B的是原生A,后面再生成代理就晚了——B拿到的永远不是原始对象而非代理对象-4。
三级缓存通过存储ObjectFactory,实现了延迟生成:只有在真正需要早期引用时才调用getObject()去生成代理对象。这正是两级缓存做不到的精妙之处。
3.3 概念关系总结
| 对比维度 | 一级缓存 | 二级缓存 | 三级缓存 |
|---|---|---|---|
| 存储内容 | 成品Bean | 半成品Bean | Bean工厂 |
| 触发时机 | 初始化完成后 | 循环依赖发生时 | 实例化完成后立即放入 |
| 核心价值 | 对外提供可用实例 | 缓存早期引用 | 延迟生成代理对象 |
四、概念关系与区别总结
一句话高度概括:
一级缓存存成品,二级缓存存半成品,三级缓存存“配方”;前两级解决“有没有”,第三级解决“怎么造”。
更精炼的记忆口诀:
一级给外用,二级防重造,三级管代理,三级缺一不可。
五、代码/流程示例演示
5.1 构建循环依赖场景(Setter注入方式)
@Component public class ServiceA { private ServiceB b; @Autowired public void setB(ServiceB b) { // Setter 注入 this.b = b; } public void doSomething() { System.out.println("ServiceA doing something..."); b.help(); } } @Component public class ServiceB { private ServiceA a; @Autowired public void setA(ServiceA a) { // Setter 注入 this.a = a; } public void help() { System.out.println("ServiceB helping..."); } }
关键注释:✅ 使用@Autowired在Setter方法上,属于Setter注入,Spring可通过三级缓存解决循环依赖-11。
5.2 执行流程逐步拆解(以A→B→A为例)
| 步骤 | 当前创建 | 操作 | 缓存变化 |
|---|---|---|---|
| 1 | A | getBean("a")开始创建A | — |
| 2 | A | 实例化new A(),此时A属性全为空 | — |
| 3 | A | 将A的ObjectFactory放入三级缓存 | → singletonFactories |
| 4 | A | 填充A属性,发现需要B | 暂停A,去创建B |
| 5 | B | 实例化new B() | — |
| 6 | B | 将B的ObjectFactory放入三级缓存 | → singletonFactories |
| 7 | B | 填充B属性,发现需要A | 去容器找A |
| 8 | B | 一级缓存无A→二级缓存无A→三级缓存命中A的工厂 | — |
| 9 | B | 执行factory.getObject()生成A的早期引用 | → earlySingletonObjects(二级) |
| 10 | B | B拿到A的引用,注入成功,B创建完成 | → singletonObjects(一级) |
| 11 | A | 回到A,拿到已完成的B注入成功 | → singletonObjects(一级) |
-4
核心洞察:B在创建过程中通过三级缓存拿到了“还在创建中的A”的早期引用,从而打破了循环依赖链条。
六、底层原理/技术支撑点
6.1 底层依赖的核心技术
三级缓存机制底层主要依赖以下几个关键设计:
反射:Spring通过反射调用构造器完成Bean的实例化,通过反射调用Setter方法完成属性注入。
ObjectFactory函数式接口:这是一个函数式接口,仅在调用getObject()时才会真正创建Bean实例,实现了延迟创建-32。DefaultSingletonBeanRegistry:Spring管理单例Bean的核心类,getSingleton、addSingletonFactory和removeSingletonFactory是处理循环依赖的关键方法-。
6.2 源码关键点(定位版)
核心代码位于DefaultSingletonBeanRegistrygetSingleton方法中:
// 大致逻辑(非完整源码,仅供理解流程) public Object getSingleton(String beanName, boolean allowEarlyReference) { // 1. 先从一级缓存拿 Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { // 2. 再从二级缓存拿 singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { // 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; }
-32
上述查找顺序 —— 一级 → 二级 → 三级 —— 构成了Spring解决循环依赖的核心逻辑。值得留意的是,从Spring Boot 2.6开始(Spring Framework 5.3),为了鼓励更清晰的设计,默认已禁用循环依赖支持,若项目中存在循环依赖,启动时会直接报错,需要显式设置spring.main.allow-circular-references=true才能开启-23。
七、高频面试题与参考答案
面试题1:Spring是如何解决循环依赖的?
踩分点:三级缓存 + 提前暴露 + 作用域限制
参考答案:Spring通过三级缓存机制解决单例Bean的Setter/字段注入场景下的循环依赖。三个缓存分别是:singletonObjects(一级,存成品)、earlySingletonObjects(二级,存半成品)、singletonFactories(三级,存对象工厂)。当Bean A依赖Bean B时,Spring在实例化A后立即将其ObjectFactory放入三级缓存,然后在填充A属性时发现需要B,便去创建B;B在填充属性时发现需要A,此时从三级缓存拿到A的工厂生成早期引用,B拿到A的引用后完成初始化,A随后也完成初始化-23。
面试题2:为什么需要三级缓存,两级缓存不行吗?
踩分点:AOP代理 + 延迟生成
参考答案:如果只是为了解决循环依赖,两级缓存确实够了。引入第三级缓存的核心原因是支持AOP代理。当Bean需要被AOP代理(如@Transactional)时,代理对象必须在属性注入阶段就准备好。三级缓存通过存储ObjectFactory,实现了延迟生成——只有在真正需要早期引用时才生成代理对象,而不是在实例化后就立即生成。这样既保证了循环依赖的解决,又避免了对所有Bean提前生成代理的性能开销-4-。
面试题3:什么情况下Spring无法解决循环依赖?
踩分点:构造器注入 + 原型Bean + 多例
参考答案:Spring无法解决以下三种循环依赖:①构造器注入——实例化阶段就需要完成依赖注入,没有机会提前暴露引用;②原型作用域(prototype)——Spring不缓存原型Bean,每次请求都是新对象,无法提前暴露;③多例Bean之间的循环依赖——同样无法缓存和提前暴露-11。
面试题4:如何从设计层面避免循环依赖?(加分题)
踩分点:重构意识 + 设计模式
参考答案:虽然Spring提供了技术解决方案,但循环依赖通常是代码设计有问题的信号,我更倾向于从设计层面解决:①提取公共接口或抽象类,让双方都依赖抽象;②使用@Lazy延迟加载作为临时方案;③重新分析类职责,看是否可以拆分成三个类;④使用事件驱动机制(ApplicationEvent)将直接调用改为事件发布/订阅-23。
八、结尾总结
本文围绕Spring循环依赖这一核心知识点,完成了以下内容的梳理:
| 模块 | 核心要点 |
|---|---|
| 痛点剖析 | 构造器注入循环依赖会抛出BeanCurrentlyInCreationException,原因是实例化阶段无法提前暴露引用 |
| 三级缓存 | 一级存成品、二级存半成品、三级存工厂——核心是第三级的ObjectFactory延迟生成机制 |
| 执行流程 | 实例化 → 放三级 → 填充属性发现依赖 → 创建依赖Bean → 从三级取工厂生成早期引用 |
| 适用边界 | ✅ Setter/字段注入 + 单例;❌ 构造器注入 + 原型Bean |
| 设计优化 | 优先从设计层面解耦,而非完全依赖框架兜底 |
重点记忆:循环依赖的解决依赖于三级缓存 + 提前暴露早期引用;第三级缓存的核心价值是支持AOP代理的延迟生成;Spring Boot 2.6+默认禁用循环依赖,需显式开启。
下一篇预告:我们将深入Spring AOP的底层原理,从动态代理到切面执行顺序,带你彻底搞懂声明式事务和日志切面的实现奥秘。