一、先搞懂:什么是循环依赖?
最常见的场景:
java
// 类 A 依赖 B
@Service
public class AService {
@Autowired
private BService bService;
}
// 类 B 依赖 A
@Service
public class BService {
@Autowired
private AService aService;
}
循环依赖过程:
- A 创建时,需要注入 B → B 还没创建,去创建 B
- B 创建时,需要注入 A → A 还没创建,去创建 A
- 陷入死循环:A ← B ← A ← B...
Spring 如何打破这个死循环? 答案就是 三级缓存。
二、核心前提:Spring 只解决「单例 + setter/field 注入」的循环依赖
| 场景 | 能否解决 | 说明 |
|---|---|---|
| ✅ 单例 Bean + setter 注入(@Autowired) | 能 | Spring 默认支持 |
| ✅ 构造器注入 + @Lazy 注解 | 能 | 通过代理延迟实例化,打断循环 |
| ❌ 多例 Bean(@Scope("prototype")) | 不能 | 无缓存机制 |
| ❌ 无 @Lazy 注解的构造器注入 | 不能 | 实例化时就需要依赖 |
| ❌ 原型 Bean 与单例 Bean 交叉循环依赖 | 不能 | 缓存机制不匹配 |
原因: 后面源码里会逐行说清。
三、三级缓存核心定义
三级缓存全部定义在 DefaultSingletonBeanRegistry 类中,是 Spring 解决循环依赖的核心容器,按优先级从高到低排列:
| 缓存级别 | 缓存名称 | 存储内容 | 核心作用 |
|---|---|---|---|
| 一级缓存 | singletonObjects |
成品 Bean(完全初始化完成,可直接使用) | 供外部获取,避免重复创建,保证单例唯一性 |
| 二级缓存 | earlySingletonObjects |
早期暴露的 Bean 引用(实例化完成、未填充属性、未初始化) | 保证循环依赖过程中,注入的是同一个早期实例,避免重复执行三级缓存的工厂逻辑 |
| 三级缓存 | singletonFactories |
Bean 工厂(lambda 表达式,用于生成早期引用) | 延迟生成代理对象,确保无论是否有循环依赖,Bean 最终只有一个代理实例(或原始实例) |
源码佐证(DefaultSingletonBeanRegistry):
java
// 一级缓存:成品 Bean(key:beanName,value:成品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 集合(标记哪些 Bean 正在创建,避免重复创建)
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));
四、循环依赖破解全流程(源码级追踪,A ↔ B 举例)
我们以 AService 和 BService 循环依赖为例,逐行追踪源码,看三级缓存如何工作。
前提:
- A、B 都是默认单例、非懒加载、setter 注入(@Autowired)
- 容器启动时,先创建 A(按 BeanName 字典序,可自行调试验证)
步骤 1:创建 A → 实例化 A(未填充属性)
容器执行 preInstantiateSingletons(),遍历 BeanName,先执行 getBean("aService")
- 进入
doGetBean(),从一级缓存singletonObjects拿 A → 无(null) - 标记 A 为「正在创建」,加入
singletonsCurrentlyInCreation - 进入
createBean()→doCreateBean() - 执行
createBeanInstance():通过构造器实例化 A(此时 A 只是个空对象,bService为 null)
步骤 2:A 暴露早期引用到三级缓存
实例化 A 后,Spring 会主动暴露 A 的早期引用(未填充属性),存入三级缓存:
java
// doCreateBean() 内部代码(AbstractAutowireCapableBeanFactory)
// 暴露早期引用:将 A 的工厂放入三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
进入 addSingletonFactory(DefaultSingletonBeanRegistry):
java
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);
}
}
}
此时状态:
- 三级缓存有 A 的工厂
- 一级、二级缓存无 A
步骤 3:A 填充属性 → 需要注入 B
实例化 A 后,进入 populateBean(),开始填充 @Autowired 的 BService:
- Spring 发现 A 需要 B → 执行
getBean("bService"),去创建 B - 进入 B 的
doGetBean(),从一级缓存拿 B → 无(null) - 标记 B 为「正在创建」,加入
singletonsCurrentlyInCreation - 进入 B 的
doCreateBean(),执行createBeanInstance(),实例化 B(B 的aService为 null) - B 实例化后,同样执行
addSingletonFactory(),将 B 的工厂放入三级缓存 - B 进入
populateBean(),开始填充@Autowired的AService→ 执行getBean("aService")
步骤 4:B 获取 A → 从三级缓存拿到 A 的早期引用
B 执行 getBean("aService") 后,流程如下:
- 从一级缓存
singletonObjects拿 A → 无 - 从二级缓存
earlySingletonObjects拿 A → 无 - 从三级缓存
singletonFactories拿 A 的工厂 → 有! - 执行工厂的 lambda 表达式(
getEarlyBeanReference()),生成 A 的早期引用(未填充属性) - 将 A 的早期引用从三级缓存移到二级缓存(三级缓存移除 A,二级缓存添加 A)
- 将 A 的早期引用注入到 B 中 → B 的
aService不再为 null
步骤 5:B 完成初始化 → 成为成品 Bean,放入一级缓存
B 注入 A 后,继续执行 initializeBean()(Aware、前后置处理器、AOP 代理)
B 初始化完成,执行 addSingleton(),将 B 放入一级缓存:
java
// DefaultSingletonBeanRegistry
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
// 放入一级缓存(成品 Bean)
this.singletonObjects.put(beanName, singletonObject);
// 清空三级、二级缓存
this.singletonFactories.remove(beanName);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
- B 创建完成,返回给 A,注入到 A 的
bService中 → A 的bService不再为 null
步骤 6:A 完成初始化 → 成为成品 Bean,放入一级缓存
A 注入 B 后,继续执行 initializeBean(),完成初始化(生成 AOP 代理等)
A 初始化完成,执行 addSingleton(),将 A 放入一级缓存,清空二级缓存中的 A
标记 A 为「创建完成」,从 singletonsCurrentlyInCreation 中移除
步骤 7:循环依赖破解完成
- 一级缓存: A(成品)、B(成品)
- 二级、三级缓存: A、B 均已清空
- 后续获取 A 或 B,直接从一级缓存拿,无循环依赖
五、关键源码拆解(断点能追到的核心方法)
1. getEarlyBeanReference(生成早期引用)
java
// AbstractAutowireCapableBeanFactory
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
// 执行 BeanPostProcessor 后置处理(如果有 AOP 代理,这里会生成代理)
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
return exposedObject;
}
作用:
- 生成 Bean 的早期引用(未初始化,但可能已生成 AOP 代理)
- 保证循环依赖时,注入的是同一个 Bean 引用(避免重复生成代理)
2. getSingleton(核心缓存获取逻辑)
java
// DefaultSingletonBeanRegistry
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1. 先从一级缓存拿成品 Bean
Object singletonObject = this.singletonObjects.get(beanName);
// 2. 一级缓存没有,且 Bean 正在创建中
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
// 3. 从二级缓存拿早期引用
singletonObject = this.earlySingletonObjects.get(beanName);
// 4. 二级缓存没有,且允许早期引用(循环依赖核心开关)
if (singletonObject == null && allowEarlyReference) {
// 5. 从三级缓存拿工厂,生成早期引用
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
// 6. 移到二级缓存,清空三级缓存(避免重复生成)
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
关键细节:
allowEarlyReference:是否允许获取早期引用(循环依赖的核心开关,默认 true)- 流程:一级 → 二级 → 三级,层层获取,避免死循环
六、面试高频问题
1. 为什么需要三级缓存?不能用二级缓存吗?
答: 为了 保证 AOP 代理的唯一性。
- 三级缓存存的是「Bean 工厂」,不是直接的 Bean 引用
- 只有当发生循环依赖时,才会执行工厂生成早期引用(可能包含 AOP 代理)
- 如果只用二级缓存,提前生成代理,会导致 Bean 初始化完成后,出现「两个代理对象」(破坏单例)
- 三级缓存的延迟生成机制,确保无论是否有循环依赖,Bean 最终只有一个代理对象
2. 为什么 Spring 只能解决 setter 注入的循环依赖?
答: 核心是「注入时机不同」:
- setter 注入: 实例化后、初始化前 注入(先实例化 A,再注入 B)
- 构造器注入: 实例化时 就需要注入依赖(创建 A 的构造器时,就需要 B,此时 B 还没实例化,无法暴露早期引用)
循环依赖破解的前提是「先实例化,再暴露早期引用」,构造器注入无法满足这个前提,所以解决不了。
3. 多例 Bean 为什么不能解决循环依赖?
答: 多例 Bean 每次 getBean() 都会创建新对象,没有缓存(三级缓存只针对单例):
- A 创建时,需要 B → 新建 B
- B 创建时,需要 A → 新建 A
- 新建的 A 又需要新建 B,陷入无限循环,无法通过缓存打破
4. 二级缓存的作用是什么?
答: 核心作用是 保证循环依赖过程中,注入的是同一个早期实例,性能优化是附带效果:
- 当多个 Bean 依赖同一个早期实例时(如 A ↔ B、A ↔ C),B 第一次获取 A 时从三级缓存生成早期引用并移入二级缓存,C 再获取 A 时直接从二级缓存拿,避免重复执行三级缓存的工厂逻辑(尤其是避免重复生成 AOP 代理)
- 若没有二级缓存,每次获取早期引用都要执行工厂逻辑,不仅低效,还可能导致实例不一致
七、循环依赖完整链路
A 创建 → 实例化 A → 暴露 A 工厂到三级缓存 → 填充属性需要 B
↓
B 创建 → 实例化 B → 暴露 B 工厂到三级缓存 → 填充属性需要 A
↓
B 获取 A → 三级缓存取 A 工厂 → 生成 A 早期引用 → 移到二级缓存 → 注入 B
↓
B 填充完成 → 初始化(AOP 代理)→ 成为成品 → 放入一级缓存 → 返回给 A 注入
↓
A 填充完成 → 初始化(若已生成代理则跳过)→ 成为成品 → 放入一级缓存 → 循环依赖破解
八、总结
Spring 通过三级缓存机制,巧妙地解决了单例 Bean 的 setter/field 注入循环依赖问题。核心思想是 提前暴露未初始化的 Bean 引用,让依赖方能够先拿到一个"半成品",待自身初始化完成后再补全依赖关系。
关键要点:
- 三级缓存各司其职: 一级存成品、二级存早期引用、三级存工厂
- AOP 代理唯一性: 三级缓存的工厂机制确保代理对象只生成一次
- 注入时机是关键: setter 注入允许先实例化后注入,构造器注入则不行
- 多例 Bean 无解: 因为没有缓存机制,每次都是新对象
理解三级缓存不仅是面试必备,更是深入 Spring IoC 容器工作原理的重要一步。建议结合源码调试,亲自走一遍循环依赖的创建流程,印象会更加深刻。