⏰ 发布时间:2026年4月9日 09:00
Spring循环依赖是每个后端开发者必须跨越的技术门槛——很多人只会在代码中无意识地使用@Autowired而从未思考过AI有道助手今天要剖析的这个问题:当Bean A依赖Bean B、Bean B又依赖Bean A时,Spring到底是如何“破局”的?它为什么能够自动化解这种看似无解的“死锁”,却又有一些场景会直接启动报错?本文将从痛点切入,逐层拆解三级缓存的每一个环节,通过源码分析与完整代码演示,帮助读者建立起从“会用”到“懂原理”的完整知识链路。

一、痛点切入:为什么会有循环依赖
传统手动管理的困境

在Spring出现之前,开发者需要手动管理对象之间的依赖关系。假设有两个相互依赖的服务:
// 手动管理的痛苦 public class ServiceA { private ServiceB b; public void setB(ServiceB b) { this.b = b; } } public class ServiceB { private ServiceA a; public void setA(ServiceA a) { this.a = a; } } // 客户端代码必须按特定顺序手动注入 ServiceA a = new ServiceA(); ServiceB b = new ServiceB(); a.setB(b); b.setA(a);
这种手动方式存在明显的缺陷:耦合高(对象之间必须知道对方的创建细节)、扩展性差(新增依赖需要修改多处代码)、维护困难(循环依赖无法被自动检测和解决)。
Spring默认行为
当两个Bean互相依赖时,如果Spring不做特殊处理,项目启动会直接抛出BeanCurrentlyInCreationException异常,提示存在无法解决的循环依赖-1。而Spring的核心设计初衷恰恰是自动化解决依赖关系,因此它必须有一套机制来处理这类问题。
二、核心概念讲解:三级缓存(Three-Level Cache)
标准定义
三级缓存是Spring在DefaultSingletonBeanRegistry类中设计的一套存储机制,用于在Bean的创建过程中管理不同生命周期阶段的对象。
Spring通过三个Map来存储不同状态的Bean-1:
| 缓存级别 | 缓存名称 | 存储内容 | 作用 |
|---|---|---|---|
| 一级缓存 | singletonObjects | 完全初始化完成的Bean | 供业务直接使用(成品) |
| 二级缓存 | earlySingletonObjects | 已实例化但未完成初始化的Bean | 存放提前暴露的半成品 |
| 三级缓存 | singletonFactories | ObjectFactory工厂对象 | 按需生成代理或原始对象 |
生活化类比
假设你在工厂里生产机器人:一级缓存是成品仓库,放着可以出厂销售的完整机器人;二级缓存是半成品暂存区,放着刚搭好骨架、还没装零件的“毛坯版”;三级缓存是一张预订券,别人拿着券来找你要机器人时,你才决定是给“毛坯版”还是给“代理版”(比如给机器人装个远程操控模块)-。
三、关联概念讲解:Bean生命周期(Bean Lifecycle)
标准定义
Bean生命周期是指Spring容器中一个Bean从创建到销毁的完整过程,核心阶段为:实例化 → 属性填充 → 初始化 → 使用 → 销毁-62。
与三级缓存的关系
三级缓存正是嵌入在Bean生命周期的“属性填充”阶段发挥作用的-62。理解两者的关系是掌握循环依赖解决逻辑的关键:
实例化阶段:Spring通过反射调用构造函数创建对象实例,此时只是一个“空壳对象”-62
属性填充阶段:Spring开始注入依赖属性,如果发现循环依赖,三级缓存在此介入-62
初始化阶段:执行
@PostConstruct、InitializingBean等回调,AOP代理在此之后生成-62
一句话概括
Bean生命周期定义了“什么时候做什么”,三级缓存定义了“遇到循环依赖时怎么做”。
四、概念关系与区别总结
| 对比维度 | 三级缓存 | Bean生命周期 |
|---|---|---|
| 本质 | 解决方案/机制 | 过程/阶段划分 |
| 角色 | 解决循环依赖的工具 | Bean创建的时间轴 |
| 关系 | 在生命周期中发挥作用 | 为三级缓存提供切入点 |
一句话记忆:Bean生命周期是“时间线”,三级缓存是“破局点”。
五、代码示例演示
场景:用户服务与订单服务相互依赖
@Service public class UserService { @Autowired private OrderService orderService; public void createUser() { System.out.println("User created"); orderService.createOrderForNewUser(); } } @Service public class OrderService { @Autowired private UserService userService; public void createOrderForNewUser() { System.out.println("Order created for user"); } }
启动项目,一切正常! Spring通过三级缓存自动解决了这个循环依赖-40。
完整解决流程演示
创建UserService:实例化 → 放入三级缓存 → 填充属性,发现需要OrderService
转而创建OrderService:实例化 → 放入三级缓存 → 填充属性,发现需要UserService
关键步骤:从三级缓存获取UserService的
ObjectFactory→ 调用getObject()生成早期引用 → 放入二级缓存OrderService完成初始化 → 移入一级缓存
UserService继续填充 → 从一级缓存获取OrderService → 完成初始化
构造器注入为何失败?
将@Autowired字段注入改为构造器注入:
@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; } }
启动直接报循环依赖错误-40。原因:构造器在实例化阶段就被调用,此时Bean尚未放入三级缓存,Spring无法提前暴露半成品-11。
六、底层原理与技术支撑
关键源码解析
Spring处理循环依赖的核心逻辑位于DefaultSingletonBeanRegistry.getSingleton()方法中-1:
public Object getSingleton(String beanName, boolean allowEarlyReference) { // 第一步:从一级缓存获取 Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { // 第二步:从二级缓存获取 singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { synchronized (this.singletonObjects) { // 第三步:从三级缓存获取ObjectFactory ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return singletonObject; }
为什么需要三级缓存(而不是二级)?
如果只有二级缓存,Spring必须在Bean实例化后立刻决定是否生成代理对象。但此时还未执行初始化逻辑(如@PostConstruct中可能动态添加切面标记),无法判断是否需要AOP代理。三级缓存把“要不要代理”的决策延迟到第一次被其他Bean引用时计算,既解决了循环依赖,又保证了AOP的正确性-3。
七、高频面试题与参考答案
面试题1:Spring如何解决循环依赖?
标准答案要点:Spring通过三级缓存机制解决单例Bean在Setter/Field注入场景下的循环依赖-41。在Bean实例化后,将ObjectFactory存入三级缓存;当其他Bean需要引用时,从三级缓存获取工厂并生成早期引用存入二级缓存,从而打破循环。构造器注入和原型Bean的循环依赖无法解决。
面试题2:为什么需要三级缓存?二级不够吗?
标准答案要点:二级缓存可以解决循环依赖,但无法正确处理AOP代理-。三级缓存存放ObjectFactory,在需要时才通过getEarlyBeanReference决定是否生成代理对象,实现代理生成的懒加载,保证最终暴露的对象和最终单例一致。
面试题3:Spring Boot 2.6+对循环依赖有什么变化?
标准答案要点:从Spring Boot 2.6开始(Spring Framework 5.3),默认禁止了循环依赖,如果项目中存在,启动时会直接报错,需要显式设置spring.main.allow-circular-references=true才能开启-41。这一变化旨在鼓励更清晰的代码设计。
面试题4:构造器注入的循环依赖如何解决?
标准答案要点:Spring无法自动解决构造器循环依赖,因为构造器在实例化阶段执行,Bean尚未放入缓存。解决方案:①使用@Lazy延迟加载其中一侧的依赖;②改用Setter/Field注入;③重构代码设计,提取公共逻辑到第三个类-58。
面试题5:循环依赖是设计问题吗?
标准答案要点:循环依赖通常是代码设计存在问题的信号,应优先从设计层面解决-41。Spring提供了技术解决方案,但“能用”不等于“该用”。推荐重构方法:提取公共接口、使用@Lazy临时方案、重新划分职责、采用事件驱动模式。
八、结尾总结
核心知识点回顾
三级缓存:一级存成品、二级存已确定的早期引用、三级存可动态生成代理的工厂
解决条件:仅限于单例Bean + Setter/Field注入场景
不可解决:构造器注入、原型Bean、多例Bean
三级优于二级:关键在于AOP代理的懒加载,决策延迟到真正需要时
版本变化:Spring Boot 2.6+默认禁用了循环依赖
重点与易错点
⚠️ 构造器注入与字段注入对循环依赖的处理结果完全不同
⚠️
@Lazy可以解决构造器循环依赖,但治标不治本⚠️ 三级缓存不是为了性能,而是为了兼顾AOP和循环依赖
下期预告
下一篇将深入讲解Spring AOP的底层原理,从JDK动态代理到CGLIB,从@Transactional失效场景到拦截器链的执行机制,彻底搞懂Spring是如何“偷偷”增强你的方法的。
