硬核拆解Spring三级缓存:从源码到实战,彻底搞懂循环依赖
引言
在日常的Spring开发中,循环依赖是一个高频出现的问题,也是面试中的核心考点。当两个或多个Bean之间互相持有对方的引用,形成闭环依赖关系时,Spring如果不做特殊处理,项目启动时会抛出BeanCurrentlyInCreationException异常。那么,Spring是如何优雅地解决这一问题的?答案就是三级缓存机制。
本文将从头到尾梳理Spring三级缓存的原理,结合源码深入分析,并通过实战案例帮你真正搞懂这一核心机制。
一、什么是循环依赖?
简单来说,循环依赖就是两个或多个Bean之间互相依赖,形成闭环。最典型的场景是:
@Component
class A {
@Resource
private B b; // A依赖B
}
@Component
class B {
@Resource
private A a; // B依赖A,形成循环
}
这种相互依赖关系如果处理不当,会导致无限递归,最终抛出BeanCurrentlyInCreationException异常。
二、三级缓存是什么?
Spring的三级缓存定义在DefaultSingletonBeanRegistry类中,本质上是三个Map:
缓存级别 缓存名称 作用
一级缓存 singletonObjects 存放完全初始化完成的单例Bean(成品对象)
二级缓存 earlySingletonObjects 存放提前暴露的半成品Bean(已实例化但未完成属性填充)
三级缓存 singletonFactories 存放ObjectFactory对象工厂,仅在调用getObject()时才会创建Bean实例
// 一级缓存:存放完全初始化好的单例Bean(成品)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:存放提前暴露的Bean实例(半成品)
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
// 三级缓存:存放Bean的工厂对象
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
// 记录当前正在创建的Bean名称
private final Set<String> singletonsCurrentlyInCreation =
Collections.newSetFromMap(new ConcurrentHashMap<>(16));
三级缓存的引入,经历了从一级缓存到二级缓存再到三级缓存的渐进式演进。早期只有一级缓存,在Bean完整创建后才存入,但这会导致多线程环境下重复创建的问题;引入二级缓存后,在Bean实例化后立刻放入二级缓存,缩短了创建链路的锁定时间;但当遇到AOP循环依赖场景时,二级缓存方案会导致代理对象创建时机与Bean创建逻辑耦合,因此引入三级缓存,通过ObjectFactory函数式接口解耦,实现了延迟创建的设计。
三、三级缓存如何工作?(核心源码解析)
3.1 Bean创建的完整流程
Spring创建Bean的大致流程如下:
getBean → doGetBean → getSingleton(缓存查找) → createBean → doCreateBean
在doCreateBean方法中,关键的步骤如下:
- 1. createBeanInstance:通过反射调用构造器创建Bean实例(此时属性还是null,属于半成品)
- 2. addSingletonFactory:将ObjectFactory放入三级缓存
- 3. populateBean:填充属性,注入依赖的Bean
- 4. initializeBean:初始化Bean,生成代理对象(如果需要AOP)
3.2 getSingleton():三级缓存查找逻辑
当Spring需要获取一个Bean时,会调用getSingleton()方法,按一级 → 二级 → 三级的顺序查找:
@Nullable
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) {
// 双重检查
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
// 第三步:从三级缓存获取ObjectFactory
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
// 放入二级缓存
this.earlySingletonObjects.put(beanName, singletonObject);
// 从三级缓存移除
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
3.3 addSingletonFactory:暴露早期引用
在Bean实例化完成后,Spring会调用addSingletonFactory方法,将Bean的ObjectFactory放入三级缓存:
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
// 放入三级缓存
this.singletonFactories.put(beanName, singletonFactory);
// 从二级缓存移除(保证数据一致性)
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}
这里的singletonFactory是一个Lambda表达式,核心逻辑是:() -> getEarlyBeanReference(beanName, mbd, bean)。注意,此时Lambda只是定义,并不会立即执行。只有当其他Bean在属性填充时发现循环依赖、调用getSingleton()查找时,才会触发singletonFactory.getObject()的执行。这种延迟执行的设计正是三级缓存解耦的核心所在。
四、实战演示:完整解决流程
以A依赖B、B依赖A的循环依赖为例,Spring的处理过程如下:
- 1. 开始创建A:标记A为”正在创建中”,实例化A,将A的ObjectFactory放入三级缓存
- 2. 填充A的属性:发现需要注入B,开始创建B
- 3. 开始创建B:标记B为”正在创建中”,实例化B,将B的ObjectFactory放入三级缓存
- 4. 填充B的属性:发现需要注入A,调用getSingleton(“a”)查找A
· 一级缓存:没有(A未完成初始化)
· 二级缓存:没有
· 三级缓存:找到A的ObjectFactory
· 调用getObject()获取A的早期引用,放入二级缓存,从三级缓存移除 - 5. 完成B的初始化:B成功获取到A的引用,完成B的所有属性填充和初始化,将B放入一级缓存
- 6. 继续完成A的初始化:B创建完成后,A成功注入B的实例,完成A的属性填充和初始化,将A放入一级缓存,清理二级/三级缓存中的临时数据
五、为什么一定要三级缓存?二级不行吗?
这是一个经典的面试题。二级缓存能否解决循环依赖?单纯解决setter注入的循环依赖,二级缓存确实够了。但引入三级缓存的核心原因是为了处理AOP代理场景。
设想一个场景:A依赖B,B依赖A,且A被AOP代理(比如添加了@Transactional注解)。如果只有二级缓存,A在实例化后直接暴露原始对象,后续B拿到的是A的原始对象而非代理对象,等A的AOP代理创建完成时,B中持有的仍然是原始对象引用,导致代理完全失效。
三级缓存通过ObjectFactory延迟获取早期引用的方式解决了这个问题:当B需要注入A时,调用getEarlyBeanReference()方法,该方法会判断是否需要提前生成代理对象,如果需要,返回代理对象;如果不需要,返回原始对象。这样就确保了循环依赖场景下注入的是正确的代理对象。
简言之:二级缓存解决的是”能不能找到”的问题,三级缓存解决的是”找到的是什么”的问题——在AOP场景下,确保找到的是代理对象而非原始对象。
六、Spring无法解决的循环依赖场景
三级缓存机制并非万能,以下场景Spring无法解决:
6.1 构造器注入循环依赖
@Component
class A {
private B b;
public A(B b) { this.b = b; } // 构造器注入
}
@Component
class B {
private A a;
public B(A a) { this.a = a; } // 构造器注入,循环依赖
}
构造器注入要求在实例化时就提供所有依赖,此时对象尚未创建,无法提前暴露引用,Spring会直接抛出BeanCurrentlyInCreationException异常。
6.2 多例(Prototype)Bean的循环依赖
@Scope("prototype")
@Component
class A {
@Autowired private B b;
}
对于多例Bean,每次请求都会创建新实例,无法提前暴露对象引用,Spring也无法解决这类循环依赖。
6.3 解决方案
· 构造器循环依赖:改用Setter注入或字段注入,或使用@Lazy注解实现延迟加载
· 多例循环依赖:重新设计代码结构,避免循环依赖
七、举一反三:从三级缓存中学到的设计思想
三级缓存机制之所以精妙,不仅在于它解决了循环依赖问题,更在于它体现了一系列经典的软件设计思想,这些思想可以应用到日常开发中。
思想一:延迟执行(Deferred Execution)
三级缓存存放的不是Bean实例,而是ObjectFactory工厂对象。这意味着真正创建Bean引用的时机被延迟到了”真正需要的时候”。这种”定义但不执行,等到条件满足再执行”的模式,本质上就是回调函数的设计思想。
应用场景:当你的业务逻辑中存在”A操作依赖B操作结果,但B操作尚未完成”的情况,可以用回调或Future模式来解耦。比如,异步任务完成后通知调用方,而不是让调用方同步等待。
思想二:单一职责原则(Single Responsibility)
为什么不是二级缓存直接暴露代理对象?因为那样会把”何时创建代理对象”的逻辑和Bean的创建流程强耦合在一起。三级缓存把”获取Bean早期引用”的逻辑单独抽离到ObjectFactory.getObject()中,让每个方法只做一件事。
应用场景:在设计复杂流程时,将”什么时候做”和”做什么”分离开。例如,订单处理流程中的延迟计算、按需加载的资源初始化,都可以通过工厂模式实现职责分离。
思想三:渐进式优化思维
三级缓存的演进路径——从一级缓存到二级缓存再到三级缓存——本身就是一个经典的”发现问题→解决问题→发现新问题→再优化”的迭代过程。这种思维在技术设计中非常重要:不要一开始就追求完美设计,而是在迭代中逐步完善。
思想四:分层缓存策略
一级缓存存放成品,二级缓存存放半成品,三级缓存存放工厂。这种分层设计本身就是一种通用的缓存策略模式:
应用场景:
· 前端-后端-数据库三级缓存:浏览器缓存(一级)→ CDN缓存(二级)→ 源站(三级)
· 多级数据校验:本地缓存(一级)→ Redis(二级)→ 数据库查询(三级)
· 懒加载与预热结合:预热的放在一级,按需加载的放在三级
思想五:提前暴露稳定身份
循环依赖的本质是”A需要B,B需要A”,两者都尚未完成。Spring通过提前暴露A的”半成品引用”打破了僵局。这种”在不完整状态下提供一个可用的稳定身份”的思路很有借鉴价值。
应用场景:
· 分布式事务中的TCC模式:Try阶段预留资源但不提交,Confirm阶段真正执行
· 前端表单分步提交:第一步保存草稿(获得ID),第二步继续完善
· 身份预分配:在完整信息尚未收集完成时,先为用户分配一个唯一标识
八、高频面试题速答
Q1:Spring如何解决循环依赖?
通过三级缓存机制,在Bean实例化后提前暴露ObjectFactory,当其他Bean需要注入时从缓存获取早期引用,打破依赖闭环。
Q2:为什么一定要三级缓存?
二级缓存可以解决普通循环依赖,但无法处理AOP场景。三级缓存通过ObjectFactory延迟获取早期引用,确保在循环依赖发生时能正确返回代理对象。
Q3:哪些循环依赖无法解决?
构造器注入循环依赖、多例Bean循环依赖、@Async注解与循环依赖同时出现的场景。
Q4:如何避免循环依赖?
优先使用Setter/字段注入而非构造器注入;合理使用@Lazy注解;必要时重构代码解耦。
九、总结
Spring的三级缓存机制是其IoC容器最精妙的设计之一。通过singletonObjects、earlySingletonObjects和singletonFactories三个缓存的精妙协作,既保证了单例Bean的正确创建,又避免了循环依赖导致的应用启动失败。理解这一机制,不仅能帮助你应对面试中的高频问题,更能让你在设计复杂系统时,借鉴其中的分层缓存、延迟执行、职责分离等设计思想,写出更优雅的代码。

夜雨聆风