Spring 循环依赖深度解析:三级缓存的设计本质与 AOP 协同机制
Spring 对循环依赖的处理是其 IOC 容器设计的精髓之一,尤其是三级缓存的运用,不仅解决了依赖循环问题,更兼顾了 AOP 代理等复杂场景。从 “循环依赖分类→三级缓存工作机制→三级缓存必要性(为什么二级缓存不够)” 三个维度,彻底讲透 Spring 循环依赖的解决方案。
循环依赖的两种类型与处理差异
循环依赖指两个或多个 Bean 相互依赖形成闭环(如 A→B→A)。Spring 对不同注入方式的循环依赖处理方式截然不同:
1. 构造器循环依赖:无法解决,直接报错
当 Bean 通过构造器注入形成循环依赖时,Spring 无法解决,会抛出 BeanCurrentlyInCreationException
。
核心原因:实例化阶段就依赖对方,无提前暴露机会
构造器注入的依赖在 Bean 实例化阶段(调用构造器)就必须满足,而此时被依赖的 Bean 可能尚未实例化,形成 “先有鸡还是先有蛋” 的死锁。
源码验证:创建前的状态检查
Spring 通过 beforeSingletonCreation()
方法跟踪正在创建的 Bean,若检测到循环依赖,直接报错:
1 | protected void beforeSingletonCreation(String beanName) { |
示例流程(A 构造器依赖 B,B 构造器依赖 A):
- 创建 A:调用
beforeSingletonCreation(A)
,singletonsCurrentlyInCreation
加入 A; - A 构造器需要 B:调用
getBean(B)
; - 创建 B:调用
beforeSingletonCreation(B)
,singletonsCurrentlyInCreation
加入 B; - B 构造器需要 A:调用
getBean(A)
; - 检测到 A 已在
singletonsCurrentlyInCreation
中,抛出异常。
2. Setter 循环依赖:单例模式下可解决,依赖三级缓存
当 Bean 通过Setter 方法注入形成循环依赖时,Spring 可通过 “提前暴露未完成初始化的 Bean” 解决,且仅支持单例 Bean(原型 Bean 无缓存,无法提前暴露)。
核心原理:将 “实例化” 与 “初始化” 分离,提前暴露实例
- 实例化:通过构造器创建 Bean 空对象(未注入属性);
- 提前暴露:将实例化后的空对象通过工厂放入三级缓存;
- 初始化:注入属性时若依赖其他 Bean,可从缓存中获取对方的提前暴露实例。
示例流程(A Setter 依赖 B,B Setter 依赖 A):
- 创建 A:实例化(无参构造器)→ 提前暴露 A 到三级缓存 → 准备注入属性(需要 B);
- 创建 B:实例化(无参构造器)→ 提前暴露 B 到三级缓存 → 准备注入属性(需要 A);
- B 从三级缓存获取 A 的提前暴露实例 → 完成 B 的属性注入和初始化 → 放入一级缓存;
- A 从一级缓存获取 B 的完整实例 → 完成 A 的属性注入和初始化 → 放入一级缓存。
三级缓存的工作机制:从暴露到缓存升级
Spring 通过三级缓存实现 Setter 循环依赖的解决,三级缓存的角色和转换逻辑是核心:
1. 三级缓存的定义与分工
1 | // DefaultSingletonBeanRegistry 中定义的三级缓存 |
缓存级别 | 存储内容 | 作用场景 |
---|---|---|
一级缓存 | 完全初始化的单例 Bean | 供外部获取最终可用的 Bean |
二级缓存 | 实例化完成但未初始化的早期 Bean | 缓存从三级缓存工厂生成的早期实例,避免重复生成 |
三级缓存 | 生成早期 Bean 的 ObjectFactory | 延迟生成早期实例(含 AOP 代理逻辑) |
2. 三级缓存的核心交互流程
以 A 依赖 B、B 依赖 A 的 Setter 循环为例,三级缓存的交互步骤如下:
步骤 1:A 实例化后,放入三级缓存
A 通过无参构造器实例化后,Spring 调用 addSingletonFactory()
将生成 A 早期实例的工厂放入三级缓存:
1 | // doCreateBean() 中触发 |
步骤 2:A 需要 B,触发 B 的创建
A 进入属性注入阶段,发现需要 B,调用 getBean(B)
,B 重复 A 的流程:实例化→放入三级缓存→准备注入属性。
步骤 3:B 需要 A,从三级缓存获取 A 的早期实例
B 进入属性注入阶段,发现需要 A,调用 getBean(A)
,触发缓存查找:
1 | // getSingleton() 缓存查找逻辑 |
步骤 4:B 完成初始化,放入一级缓存
B 注入 A 的早期实例后,完成属性注入和初始化,调用 addSingleton(B)
放入一级缓存:
1 | protected void addSingleton(String beanName, Object singletonObject) { |
步骤 5:A 注入 B 的完整实例,完成初始化
A 从一级缓存获取 B 的完整实例,完成属性注入和初始化,最终放入一级缓存。
为什么需要三级缓存?二级缓存不够吗?
这是理解循环依赖的关键问题。表面上看,二级缓存(earlySingletonObjects
)似乎可以存储早期实例,但三级缓存(singletonFactories
)的设计与 AOP 代理密切相关,是解决 “代理对象一致性” 的核心。
1. 没有 AOP 时,二级缓存确实可以解决循环依赖
若所有 Bean 都是普通对象(无需 AOP 代理),流程可简化为:
- 实例化 A 后,直接将 A 的原始对象放入二级缓存;
- B 依赖 A 时,从二级缓存获取 A 的原始对象;
- 此时二级缓存足够,无需三级缓存。
但 Spring 必须支持 AOP 场景,此时三级缓存的必要性就凸显了。
2. 有 AOP 时,三级缓存是保证 “代理对象一致性” 的关键
当 Bean 需要被 AOP 代理时(如标注 @Transactional
),最终放入一级缓存的是代理对象,而非原始对象。若使用二级缓存直接存储原始对象,会导致依赖方获取的是原始对象,与最终的代理对象不一致。
三级缓存通过 ObjectFactory
延迟生成代理对象,确保依赖方获取的是与最终一级缓存中一致的代理对象。
关键逻辑:getEarlyBeanReference()
生成代理
三级缓存中 ObjectFactory
的 getObject()
方法会调用 getEarlyBeanReference()
,该方法通过后置处理器(如 AbstractAutoProxyCreator
)生成代理对象:
1 | protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { |
三级缓存解决的核心问题:代理对象的延迟生成与一致性
- 延迟生成:代理对象不在实例化后立即生成,而是在首次被依赖时(循环依赖发生时)通过
ObjectFactory
生成,避免无循环依赖时的性能损耗; - 一致性:无论是否有循环依赖,最终放入一级缓存的和依赖方获取的都是同一个代理对象(若需要代理)。
反例:若用二级缓存直接存储原始对象
- A 实例化后,将原始对象放入二级缓存;
- B 依赖 A 时,从二级缓存获取原始对象并注入;
- A 后续初始化时生成代理对象,放入一级缓存;
- 最终 B 中注入的是 A 的原始对象,而容器中实际的 A 是代理对象,导致不一致。
3. 三级缓存的本质:平衡 “循环依赖解决” 与 “代理对象生成时机”
三级缓存的设计并非为了单纯存储,而是通过 ObjectFactory
实现了代理对象的按需生成:
- 无循环依赖时:
ObjectFactory
不会被调用,代理对象在 Bean 初始化完成后生成(initializeBean()
阶段),放入一级缓存; - 有循环依赖时:
ObjectFactory
被触发,提前生成代理对象并放入二级缓存,确保依赖方获取的是代理对象,与最终一级缓存中的对象一致。
总结:三级缓存的设计哲学
Spring 三级缓存解决循环依赖的核心逻辑可概括为:
- 分离实例化与初始化:允许 Bean 在实例化后、初始化前(未注入属性)就被暴露;
- 三级缓存分工:一级缓存存完整 Bean,二级缓存存早期实例,三级缓存存生成早期实例的工厂(含代理逻辑);
- AOP 协同:通过
ObjectFactory
延迟生成代理,确保循环依赖场景下代理对象的一致性
v1.3.10