Spring循环依赖(Circular Dependency)是Java后端面试中几乎100%会出现的核心考点,也是许多开发者在项目启动时遇到的“死锁式”报错根源。_ai健康助手发现,多数开发者虽然能背出“三级缓存”这四个字,却说不清每一级缓存存的是什么、为什么需要三级、二级缓存够不够、构造器注入为何无法解决。本文将从概念、原理、代码示例到面试要点,带你彻底搞懂Spring循环依赖的前世今生。
一、痛点切入:为什么会出现循环依赖?

// ❌ 典型循环依赖场景:ServiceA 依赖 ServiceB,ServiceB 依赖 ServiceA @Servicepublic class OrderService { @Autowired private UserService userService; // OrderService -> UserService } @Service public class UserService { @Autowired private OrderService orderService; // UserService -> OrderService(形成闭环) }
上面这段代码看似简单,但在Spring Boot 2.6+版本中,默认会禁止循环依赖,启动时直接抛出异常-1。问题出在哪里?
通俗理解:就像两个人去领证,A说“B来了我才签”,B说“A来了我才签”,结果谁也签不了-4。Spring在创建Bean时,同样会陷入这种“鸡生蛋,蛋生鸡”的困境。
传统方案只能靠开发者手动规避,代码耦合度高、维护困难。于是,Spring设计了三级缓存机制,在容器层面自动解决这个问题。
二、核心概念:三级缓存(Three-Level Cache)
2.1 标准定义
三级缓存是Spring IoC容器在创建单例Bean过程中,用于管理不同阶段Bean对象的一组Map集合,核心目的是通过提前暴露半成品Bean的方式打破循环依赖闭环。
Spring通过三级缓存机制,可以解决单例Bean在Setter/Field注入场景下的循环依赖问题-1。
2.2 生活化类比
三级缓存就像医院的挂号系统:
一级缓存:已就诊完成的病人档案(成品)
二级缓存:正在就诊的病人(半成品,正在处理中)
三级缓存:挂号排队单(可随时生成就诊卡)
当医生(B)需要查阅另一个正在就诊病人(A)的资料时,可以从“排队单”生成临时病历先使用,等A看完病再补全。
三、关联概念:三级缓存详细拆解
Spring内部维护了三个Map,各司其职-2:
| 级别 | 缓存名称 | 存储内容 | 作用 |
|---|---|---|---|
| 一级 | singletonObjects | 完全初始化完成的单例Bean | 供业务直接使用 |
| 二级 | earlySingletonObjects | 提前暴露的半成品Bean(已实例化,未完成属性填充) | 存放早期引用,避免重复创建 |
| 三级 | singletonFactories | ObjectFactory(对象工厂) | 存储Lambda表达式,仅在调用getObject()时才创建Bean实例 |
概念关系梳理
三级 vs 二级:三级缓存存的是“如何创建对象”的工厂(懒加载),二级缓存存的是“已经创建好的半成品”对象(提前暴露)
思想 vs 实现:三级缓存是一种设计思想(提前暴露引用),三个Map是其具体实现手段
一句话记忆:一级存成品,二级存半成品,三级存工厂;一、二级是“存东西”,三级是“存怎么造东西”。
四、循环依赖解决流程(图文示例)
4.1 场景设定
A 依赖 B,B 依赖 A(字段注入)
4.2 完整执行流程
1. 创建A:实例化A → 将A的ObjectFactory放入三级缓存 2. 填充A属性:发现需要B → 转去创建B 3. 创建B:实例化B → 将B的ObjectFactory放入三级缓存 4. 填充B属性:发现需要A → 从三级缓存获取A的工厂 → 生成A的早期引用 → 放入二级缓存,移除三级 5. B拿到A的引用后完成初始化 → 放入一级缓存 6. A继续填充属性 → 从一级缓存获取B → 完成初始化 → 放入一级缓存
-1
4.3 关键源码解析(Spring 5.3.x)
Spring处理循环依赖的核心逻辑在DefaultSingletonBeanRegistrygetSingleton方法中-2:
public Object getSingleton(String beanName, boolean allowEarlyReference) { // 第一步:从一级缓存获取成品Bean Object singletonObject = this.singletonObjects.get(beanName); // 如果一级缓存没有,且当前Bean正在创建中(循环依赖核心判断条件) if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { // 第二步:从二级缓存获取半成品Bean singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { synchronized (this.singletonObjects) { // 双重检查锁 // 第三步:从三级缓存获取ObjectFactory → 创建早期引用 → 移入二级 } } } return singletonObject; }
五、概念关系与区别总结
5.1 三级缓存 vs 二级缓存:为什么需要三级?
很多面试者会问:“二级缓存够用吗?为什么非要三级?”
如果只是为了解决循环依赖,二级缓存确实够用——只要在Bean实例化后,不管是否需要AOP,都直接生成代理对象放到二级缓存,B就能拿到引用-。
但这样会有两个严重问题:
性能浪费:如果这个Bean从未参与循环依赖,提前生成代理对象就是多余操作
AOP代理冲突:一个Bean可能有多个代理(事务代理、日志代理等),提前生成哪个?
三级缓存的精妙之处:用ObjectFactory(Lambda表达式)实现懒加载,只有真正需要提前暴露时才生成代理对象,既保证性能又确保AOP正确性。
5.2 构造器注入 vs Setter/字段注入
| 注入方式 | 是否支持循环依赖 | 原因 |
|---|---|---|
| 构造器注入 | ❌ 不支持 | 实例化时必须传入依赖,无法提前暴露引用 |
| Setter/字段注入 | ✅ 支持 | 实例化后可先暴露半成品,再注入依赖 |
-43
六、代码示例:从报错到解决
6.1 完整示例项目结构
src/main/java/com/example/ ├── DemoApplication.java ├── service/ │ ├── OrderService.java │ └── UserService.java └── config/ └── AppConfig.java
6.2 ❌ 错误示例(启动报错)
// OrderService.java @Service public class OrderService { private final UserService userService; // final字段,构造器注入 public OrderService(UserService userService) { this.userService = userService; } } // UserService.java @Service public class UserService { private final OrderService orderService; public UserService(OrderService orderService) { this.orderService = orderService; } }
报错信息:BeanCurrentlyInCreationException: Requested bean is currently in creation
6.3 ✅ 正确示例(解决方案)
方案一:改用字段注入(Spring Boot 2.6+默认禁止,不推荐)
@Service public class OrderService { @Autowired // 字段注入 private UserService userService; }
方案二:使用@Lazy延迟加载(推荐)
@Service public class OrderService { private final UserService userService; public OrderService(@Lazy UserService userService) { // 加@Lazy this.userService = userService; } }
@Lazy会在构造函数参数上注入一个代理对象,真正调用方法时才加载真实Bean,从而打破循环依赖-1。
方案三:重构代码消除循环依赖(最佳实践)
提取公共逻辑到第三方服务,从根本上消除相互依赖-1。
// 提取中介服务 @Service public class CustomerOrderService { @Autowired private CustomerService customerService; @Autowired private OrderService orderService; public List<Order> getCustomerOrders(Long customerId) { Customer customer = customerService.getCustomer(customerId); return orderService.getOrdersByCustomer(customerId); } }
七、底层原理与面试进阶要点
7.1 底层技术支撑
三级缓存机制底层依赖以下核心技术:
反射(Reflection):动态创建Bean实例
代理模式(Proxy Pattern):AOP代理对象的生成
Lambda表达式:
ObjectFactory的函数式编程特性ConcurrentHashMap:保证高并发下的线程安全
7.2 Spring版本变化关键提示(面试加分项)
从Spring Boot 2.6(Spring Framework 5.3)开始,官方默认禁止循环依赖,鼓励更清晰的代码设计。如果项目中存在循环依赖,启动时会直接报错,需要显式配置spring.main.allow-circular-references=true才能开启-22-30。
到Spring Boot 3.x(基于Spring Framework 6),默认禁用的策略延续,且问题暴露得更明显-29。
7.3 Spring无法解决的循环依赖情况
构造器注入的循环依赖:实例化阶段就死锁,Spring无能为力
原型(Prototype)作用域的循环依赖:Spring不缓存原型Bean,无法提前暴露
多例Bean之间的循环依赖
-22
八、高频面试题与参考答案
面试题一:Spring是如何解决循环依赖的?请详细说明。
标准答案(踩分点):
结论先行:Spring通过三级缓存机制解决单例Bean在Setter/字段注入场景下的循环依赖。
三级缓存分别是什么:
一级
singletonObjects:存放完全初始化的成品Bean二级
earlySingletonObjects:存放提前暴露的半成品Bean三级
singletonFactories:存放ObjectFactory工厂
核心流程:实例化A后放入三级缓存→发现需要B→创建B→实例化B后放入三级缓存→B发现需要A→从三级缓存获取A的工厂生成早期引用放入二级→B完成初始化→A继续初始化。
为什么需要三级:二级缓存虽能解决循环依赖,但无法处理AOP代理对象的提前暴露问题,三级缓存通过ObjectFactory实现懒加载,按需生成代理。
-2-7
面试题二:为什么构造器注入无法解决循环依赖?
标准答案:
构造器注入要求在实例化阶段就传入所有依赖,而循环依赖发生时,两个Bean都处于“正在创建但未完成”的状态,彼此无法获取对方的实例。三级缓存机制依赖“实例化后、属性填充前”这个时间窗口来提前暴露引用,构造器注入恰恰没有这个窗口,因此Spring在启动时就能检测到并抛出异常。
-22
面试题三:使用@Lazy注解解决循环依赖的原理是什么?
标准答案:
@Lazy会为依赖的Bean生成一个代理对象注入到当前Bean中,而不是真正的Bean实例。当第一次调用该依赖的方法时,代理对象才会触发真实的Bean加载和初始化。这个机制打破了循环依赖的时间死锁——A不需要B的真实实例,只需要一个“占位符”代理即可完成初始化。
-4
面试题四:Spring Boot 2.6+版本为什么默认禁止循环依赖?
标准答案:
循环依赖本质上是代码设计问题的信号,而非框架缺陷。Spring官方希望引导开发者写出更清晰、解耦的代码。默认禁止循环依赖后,开发者会被迫审视代码设计,主动通过重构、接口隔离、事件驱动等方式消除不必要的循环依赖,提升代码质量。同时,禁止默认支持也简化了框架的启动逻辑。
-22
面试题五:如果同时使用AOP和循环依赖,Spring如何处理?
标准答案:
当Bean需要AOP代理时,三级缓存中的ObjectFactory会在调用getObject()时提前生成代理对象而非原始对象。Spring通过getEarlyBeanReference()方法判断是否需要代理,如果需要在三级缓存阶段就生成代理对象放入二级缓存。这样即使发生循环依赖,B获取到的A也是正确的代理对象,而不是原始对象。这正是三级缓存优于二级缓存的关键所在。
--48
九、结尾总结
回顾核心知识点
循环依赖:两个或多个Bean互相持有对方引用,形成闭环
三级缓存:
singletonObjects(成品)→earlySingletonObjects(半成品)→singletonFactories(工厂)解决范围:✅ Setter/字段注入 | ❌ 构造器注入 | ❌ 原型Bean
版本变化:Spring Boot 2.6+默认禁止,需要显式开启或重构
最佳实践:优先通过重构消除循环依赖,而非依赖框架特性
易错提醒
不是所有循环依赖Spring都能解决——构造器注入的原型Bean循环依赖会直接报错
@Lazy是治标方案,真正的解决之道是代码重构Spring Boot 2.6+默认禁止循环依赖,升级项目时需要注意
进阶预告
下一篇我们将深入探讨:Spring AOP底层原理与动态代理的源码实现,敬请期待。
本文基于Spring Framework 5.3.x / 6.x版本编写,发布于2026年4月,数据来源包括官方文档、阿里云开发者社区、CSDN技术博客等公开资料。
📌 系列文章预告:
下篇:Spring AOP底层原理深度解析——动态代理的源码之旅
后续:Spring事务管理机制与失效场景全解
