小说助手AI带你彻底搞懂Spring循环依赖
北京时间:2026年4月9日
在Java后端开发的面试现场,面试官缓缓问出一句——“Spring如何解决循环依赖?”此时,如果你只能支支吾吾地说出“三级缓存”三个字,却讲不清为什么需要三级、二级够不够、构造器注入为何无法解决,那么这道“送分题”大概率会变成“送命题”。小说助手AI本期带你从零拆解Spring循环依赖,让概念不再模糊,让原理真正落地。

一、痛点切入:为什么会出现循环依赖?
假设你在开发一个订单系统,OrderService需要调用UserService查询用户信息,而UserService又需要调用OrderService获取订单列表。这种“你中有我、我中有你”的依赖关系,就是典型的循环依赖(Circular Dependency)。

先看一个错误示例——构造器注入:
@Service public class OrderService { private final UserService userService; public OrderService(UserService userService) { this.userService = userService; } } @Service public class UserService { private final OrderService orderService; public UserService(OrderService orderService) { this.orderService = orderService; } }
启动项目后,你会看到如下报错:
The dependencies of some of the beans in the application context form a cycle: ┌─────┐ | orderService defined in ... ↑ ↓ | userService defined in ... └─────┘
为什么会失败? 创建OrderService时需要UserService,创建UserService时又需要OrderService,两个Bean相互等待对方先完成创建,形成死锁,最终抛出BeanCurrentlyInCreationException异常-1。
这种问题在日常开发中并不罕见。据2024年Java生态调研报告显示,约23%的Spring应用开发者曾遇到过循环依赖问题,其中字段注入导致的循环依赖占比高达67%-1。
二、核心概念:什么是循环依赖?
循环依赖(Circular Dependency) ,指的是两个或多个Bean之间互相持有对方的引用,形成闭环依赖关系-7。
最典型的场景是:Bean A 依赖 Bean B,同时 Bean B 又依赖 Bean A。在Spring容器中,这种依赖关系如果不能被正确处理,会导致Bean初始化过程陷入死循环或抛出异常-5。
用生活化类比来理解:
想象两个人面对面站在一条窄桥上,甲要过桥必须等乙先让路,乙要过桥必须等甲先让路。两人都站在桥上等待对方先动,结果谁也过不去——这就是循环依赖的“死锁”困境。
循环依赖的三种常见形态:
| 类型 | 注入方式 | Spring能否自动解决 | 常见程度 |
|---|---|---|---|
| 构造器循环依赖 | 构造器参数注入 | ❌ 不能 | 较少见 |
| Setter循环依赖 | setter方法注入 | ✅ 能 | 较常见 |
| 字段循环依赖 | @Autowired字段注入 | ✅ 能 | 最常见 |
三、Spring Bean的生命周期:理解循环依赖的前提
要理解Spring如何解决循环依赖,先得搞清楚一个Bean从出生到成品的完整过程。
简化来说,一个单例Bean的创建分为三个阶段-5:
实例化(Instantiation) :通过反射调用构造器创建原始对象。此时的Bean只是一个“空壳”,属性全是null。
属性注入(Populate) :为@Autowired字段或setter方法注入依赖。
初始化(Initialization) :执行@PostConstruct方法、InitializingBean接口方法等。
问题恰恰出在第2步:当Bean A在第2步发现需要Bean B时,如果Bean B也依赖Bean A,而两者都卡在第2步无法推进,死锁就此形成。
Spring的破局思路:在第1步实例化完成后,立刻把这个“半成品”Bean提前暴露出去。这样一来,当Bean B需要Bean A时,不需要等待A完全创建完成,而是直接拿到A的早期引用,先完成自己的创建。这个策略,用大白话讲就是:先把“半成品的Bean”暴露出去,虽然属性还没填完,但对象地址已经有了,先拿去用,等后面再慢慢完善-。
四、三级缓存:Spring的精妙设计
Spring通过三级缓存(Three-Level Cache) 机制来实现上述策略。这三个缓存位于DefaultSingletonBeanRegistry类中,是三个Map对象-7:
| 缓存级别 | 缓存名称 | 存储内容 | 作用 |
|---|---|---|---|
| 一级缓存 | singletonObjects | 完全初始化的成品Bean | 供业务直接使用 |
| 二级缓存 | earlySingletonObjects | 提前暴露的半成品Bean | 存储已实例化但未初始化的Bean引用 |
| 三级缓存 | singletonFactories | ObjectFactory对象工厂 | 按需生成半成品Bean,支持AOP代理的延迟创建 |
三者关系一句话概括:三级缓存存工厂 → 工厂生成对象 → 对象升级到二级缓存 → 完整初始化后放入一级缓存。
为什么需要三级缓存?(面试高频考点)
一个常见的面试追问是:“解决循环依赖,二级缓存就够了,为什么非要三级?”
答案的核心:如果仅仅是为了解决循环依赖,二级缓存确实够用。只要在Bean实例化后,不管它需不需要AOP,都直接把它的代理对象生成出来放到二级缓存里,另一个Bean就可以拿到了-29。
但这样做的代价是:提前生成所有Bean的代理对象,包括那些根本不需要AOP的Bean,会造成不必要的性能开销。
三级缓存的设计巧妙之处在于——延迟生成代理对象。ObjectFactory是一个函数式接口,只有在真正需要(即循环依赖被触发)时,才会调用getObject()生成代理对象。如果没有循环依赖,三级缓存中的工厂对象永远不会被调用,Bean会按正常流程完成初始化后才生成代理-。
一句话总结:二级缓存解决的是“循环依赖能不能解”的问题,三级缓存解决的是“循环依赖怎么解才优雅(支持AOP代理且性能最优)”的问题。
五、完整流程演示:从代码到原理
5.1 可运行的代码示例
下面是一个字段注入(@Autowired)的循环依赖示例,Spring可以自动解决:
@Service public class OrderService { @Autowired private UserService userService; public void createOrder() { System.out.println("创建订单,关联用户:" + userService.getUserName()); } } @Service public class UserService { @Autowired private OrderService orderService; public String getUserName() { orderService.createOrder(); // 调用OrderService的方法 return "张三"; } }
5.2 执行流程解析
假设OrderService先被创建,Spring通过三级缓存解决循环依赖的完整流程如下-2:
| 步骤 | 当前操作 | 一级缓存 | 二级缓存 | 三级缓存 | 说明 |
|---|---|---|---|---|---|
| ① | 实例化OrderService | 空 | 空 | OrderService的ObjectFactory | 构造完成后,将工厂对象放入三级缓存 |
| ② | 填充OrderService属性,发现需要UserService | 空 | 空 | OrderService的ObjectFactory | 转而去创建UserService |
| ③ | 实例化UserService | 空 | 空 | OrderService的ObjectFactory + UserService的ObjectFactory | UserService构造完成后放入三级缓存 |
| ④ | 填充UserService属性,发现需要OrderService | 空 | 空 | 同上 | 开始寻找OrderService |
| ⑤ | 从三级缓存获取OrderService的ObjectFactory,调用生成早期引用 | 空 | OrderService半成品 | 同上 | 将早期引用升级到二级缓存 |
| ⑥ | UserService获得OrderService引用,完成初始化 | UserService成品 | OrderService半成品 | 同 | 注入成功,UserService初始化完成 |
| ⑦ | 返回OrderService继续填充属性 | UserService成品 | OrderService半成品 | 同 | OrderService获得UserService成品 |
| ⑧ | OrderService完成初始化 | OrderService成品 + UserService成品 | 空 | 空 | 成品移入一级缓存,清理临时缓存 |
5.3 核心源码定位
Spring解决循环依赖的核心逻辑集中在DefaultSingletonBeanRegistrygetSingleton()方法中,关键判断是“一级缓存没有且当前Bean正在创建中”-7。当满足这个条件时,Spring会依次从二级缓存和三级缓存中查找早期引用。这部分源码建议结合Spring源码边debug边理解效果更佳-。
六、底层原理:AOP代理与三级缓存
Spring的AOP(Aspect Oriented Programming,面向切面编程)功能,如@Transactional事务注解,底层通过动态代理实现。当一个Bean需要被代理时,Spring不能直接把原始对象暴露出去——暴露出去的必须是代理对象,否则事务等功能会失效。
三级缓存正是为此而设计:ObjectFactory在调用getObject()时,会检查当前Bean是否需要AOP代理,如果需要则返回代理对象,否则返回原始对象。这样既保证了循环依赖能够被解决,又保证了代理对象的正确性。
七、不能解决的情况(面试必问)
Spring的三级缓存并非万能,以下情况无法自动解决循环依赖-2:
| 场景 | 原因 | 解决方案 |
|---|---|---|
| 构造器注入 | 实例化阶段就需要依赖,此时Bean尚未放入任何缓存 | 改用字段注入或setter注入 |
| 原型作用域(Prototype) | 每次请求创建新实例,无法提前暴露引用 | 避免在设计中使用原型Bean的循环依赖 |
| AOP代理对象异常 | 代理对象的创建时机可能导致循环依赖失败 | 使用@Lazy延迟加载 |
为什么构造器注入无法解决?
构造器注入要求在实例化时就提供所有依赖,而实例化发生在第1步。此时Bean还没有机会放入三级缓存,两个Bean相互等待对方先实例化,形成死锁-4。
八、高频面试题与参考答案
面试题1:Spring如何解决循环依赖?
参考答案:Spring通过三级缓存机制解决单例Bean的Setter注入和字段注入场景下的循环依赖。三级缓存分别是:
一级缓存
singletonObjects:存放完全初始化完成的成品Bean。二级缓存
earlySingletonObjects:存放提前暴露的半成品Bean(已实例化但未初始化)。三级缓存
singletonFactories:存放ObjectFactory工厂对象,支持AOP代理的延迟创建。
核心思路:在Bean实例化完成后,立即将其工厂对象放入三级缓存,提前暴露引用。当另一个Bean依赖它时,从三级缓存获取工厂并生成早期引用,从而打破依赖闭环。
局限性:构造器注入和原型Bean的循环依赖无法解决。
面试题2:为什么需要三级缓存,二级不够吗?
参考答案:单纯解决循环依赖,二级缓存确实够用。但Spring需要兼顾AOP代理对象的正确生成和性能优化。如果只用二级缓存,必须在实例化后立即生成代理对象(不管是否需要),会造成不必要的开销。三级缓存通过ObjectFactory延迟生成代理对象,只在真正发生循环依赖时才触发生成,既解决了循环依赖,又保证了AOP的正确性和性能。
面试题3:什么情况下Spring无法解决循环依赖?
参考答案:三种情况:
构造器注入的循环依赖:实例化阶段就需要依赖,Bean尚未放入缓存。
原型作用域的循环依赖:每次创建新实例,无法缓存早期引用。
AOP代理对象的循环依赖:代理对象生成时机特殊,可能导致失败。
面试题4:从Spring Boot 2.6开始,循环依赖默认还能自动解决吗?
参考答案:不能。从Spring Boot 2.6(Spring Framework 5.3)开始,为了鼓励更清晰的代码设计,默认禁用了循环依赖的自动解决。如果项目中存在循环依赖,启动时会直接报错,需要显式设置spring.main.allow-circular-references=true才能开启-28。
九、最佳实践与避坑指南
✅ 推荐做法
优先使用构造器注入:构造器注入本身就是一种“无法循环依赖”的设计,能迫使你写出更清晰的代码结构。
重构双向依赖:将A和B的共同逻辑提取到第三个类C中,让A和B都依赖C,从根本上消除循环依赖。
使用@Lazy延迟加载:在其中一个依赖上添加
@Lazy,让Bean在首次使用时才初始化。
❌ 避坑提示
不要依赖Spring的循环依赖自动解决机制——它是“急救方案”,而非“设计规范”。
字段注入虽然方便,但会隐藏设计问题,且不利于单元测试。
从Spring Boot 2.6开始,新项目中如果出现循环依赖会直接启动失败,不要抱有侥幸心理。
十、总结
回顾本文核心知识点:
| 知识点 | 核心结论 |
|---|---|
| 什么是循环依赖 | 两个或多个Bean相互引用形成闭环 |
| Spring如何解决 | 三级缓存机制 + 提前暴露半成品Bean |
| 三级缓存各司何职 | 一级存成品、二级存半成品、三级存工厂 |
| 为什么需要三级 | 支持AOP代理的延迟生成,兼顾性能与正确性 |
| 不能解决的情况 | 构造器注入、原型作用域 |
| 版本变化 | Spring Boot 2.6+默认禁用,需显式开启 |
理解Spring循环依赖,不仅是应对面试的需要,更是深入理解IoC容器设计哲学的必经之路。下一期,小说助手AI将带你深入Spring AOP的底层原理,聊聊动态代理和切面执行的细节,敬请期待。