Spring 循环依赖三级缓存源码级解析
问题
Spring 如何解决构造器注入和 setter 注入的循环依赖?三级缓存各存什么?
循环依赖是什么
循环依赖指 Bean A 依赖 Bean B,Bean B 又依赖 Bean A(或更复杂的链路 A → B → C → A)。如果不做特殊处理,容器在创建 A 时发现需要 B,去创建 B 时发现需要 A,形成死锁。
Spring 并非对所有循环依赖都能解决,它只解决 setter 注入(字段注入也算 setter 注入的一种) 的循环依赖,构造器注入的循环依赖会直接报错。
三级缓存的定义
三级缓存定义在 DefaultSingletonBeanRegistry 中,三个 ConcurrentHashMap:
// 一级缓存:完全初始化好的单例 bean(成品)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:提前暴露的早期 bean(半成品,已完成实例化但未完成属性注入和初始化)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 三级缓存:ObjectFactory 工厂,用于生成早期 bean 的代理对象
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);为什么需要三级缓存
要理解三级缓存的设计,先看一个循环依赖场景:
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}只能用一级缓存 —— 不行
如果只有一级缓存(成品库存),A 创建到一半时发现需要 B,但 B 还没创建,死锁。A 不可能把自己"半成品"放进去,因为一级缓存只装成品。
只用两级缓存 —— 勉强可行,但有性能问题
如果只有一级 + 二级缓存,流程会是:
- A 实例化,放入二级缓存(半成品)
- A 注入属性,发现需要 B
- B 实例化,放入二级缓存
- B 注入属性,从二级缓存拿到 A 的早期引用
- B 完成初始化,放入一级缓存
- A 从二级缓存移到一级缓存
看起来可以?问题出在 AOP 代理上。假设 A 需要被 AOP 增强(比如加了 @Transactional),那么:
- 在步骤 2 中,A 的早期引用(从二级缓存拿到的)还没有被代理
- 当 B 把 A 的原始引用存入自己的字段后,后续 A 在步骤 6 生成了 AOP 代理对象
- 最终 B 持有的是 A 的原始对象,而不是 AOP 代理对象 —— 事务/缓存等增强全部失效
如果去掉二级缓存,每次直接从二级缓存拿"代理后的对象",那所有 bean 即使没有循环依赖,也要提前生成代理对象,浪费性能。
三级缓存的设计 —— 延迟代理
三级缓存通过 ObjectFactory 延迟代理的创建,核心方法:
// DefaultSingletonBeanRegistry.getSingleton()
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1. 查一级缓存(成品)
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 2. 查二级缓存(早期半成品)
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
// 3. 查三级缓存,执行 ObjectFactory 生成早期引用
synchronized (this.singletonObjects) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
// 放入二级缓存,删除三级缓存,避免重复创建
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}三级缓存的 ObjectFactory 在哪里放进去的?在 doCreateBean() 方法中:
// AbstractAutowireCapableBeanFactory.doCreateBean()
// 实例化后,在属性注入之前,判断是否允许提前暴露
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences
&& isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}getEarlyBeanReference 是关键入口——它会调用所有 SmartInstantiationAwareBeanPostProcessor 的 getEarlyBeanReference 方法。Spring AOP 的代理对象就是在这里生成的:
// AbstractAutoProxyCreator.getEarlyBeanReference()
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
// 如果有循环依赖,提前创建代理对象
if (this.earlyProxyReferences.add(beanName)) {
return wrapIfNecessary(bean, beanName, null);
}
return bean;
}完整流程演示
用 A → B → A 的循环依赖走一遍完整流程:
1. createBean(A) 开始
→ A 实例化(反射,分配内存)
→ addSingletonFactory(A, factory) // A 放入三级缓存
→ populateBean(A) 开始注入属性
→ 发现需要 B
→ getBean(B)
2. createBean(B) 开始
→ B 实例化
→ addSingletonFactory(B, factory) // B 放入三级缓存
→ populateBean(B) 开始注入属性
→ 发现需要 A
→ getSingleton(A) // 尝试获取 A
→ 一级缓存没有 A(A 还没完成初始化)
→ 二级缓存没有 A
→ 三级缓存有 A 的 ObjectFactory
→ 执行 factory.getObject() → getEarlyBeanReference(A)
→ 如果 A 需要 AOP,生成 AOP 代理
→ 得到 A 的早期引用(可能是代理对象)
→ 放入二级缓存,删除三级缓存
→ 将 A 的早期引用注入到 B 的 a 字段
→ B 完成属性注入
→ B 执行初始化回调(@PostConstruct, InitializingBean 等)
→ addSingleton(B) // B 放入一级缓存,删除二三级缓存
3. 回到 A 的 populateBean
→ 注入 B 属性(从一级缓存拿到 B 的完整实例)
→ A 完成属性注入
→ A 执行初始化回调
→ addSingleton(A) // A 放入一级缓存,删除二三级缓存构造器注入为什么不行
@Component
public class A {
public A(B b) {} // 构造器注入
}
@Component
public class B {
public B(A a) {} // 构造器注入
}构造器注入在 createBeanInstance() 阶段(实例化)就卡住了。此时:
- 三级缓存还没建立(
addSingletonFactory在实例化之后才执行) - Spring 在实例化 A 时发现需要 B,去创建 B;B 实例化时发现需要 A,再次尝试创建 A
- 进入死循环,直到
singletonsCurrentlyInCreation集合检测到重复创建,抛出BeanCurrentlyInCreationException
源码位置:DefaultSingletonBeanRegistry.beforeSingletonCreation() 中检查 singletonsCurrentlyInCreation 是否已包含当前 beanName,有则抛异常。
三级缓存常见面试追问
为什么三级缓存不用两级?用两级配合 AOP 提前代理不行吗?
两级缓存方案:一级放成品,二级放"代理后的半成品"。所有 bean 实例化后都默认生成代理对象并放入二级缓存。
问题在于:大多数 bean 没有循环依赖,也不需要 AOP。如果全部提前生成代理,每个 bean 实例化后都要走一遍 wrapIfNecessary,对于几千个 bean 的大项目,这完全是浪费。Spring 通过三级缓存 + ObjectFactory 实现了按需代理——只有真正发生循环依赖时,才会触发 getEarlyBeanReference 生成代理。
@Async 为什么会导致循环依赖报错?
@Async 通过 AsyncAnnotationBeanPostProcessor 生成代理,但这个 PostProcessor 的 getEarlyBeanReference 方法没有实现(只实现了 postProcessAfterInitialization)。所以当循环依赖发生时,getEarlyBeanReference 返回的是原始对象,而不是代理对象。后续在 postProcessAfterInitialization 中会再次生成代理,但 B 已经持有了 A 的原始引用,导致 @Async 不生效。
@Async 的循环依赖直接报错是 Spring 的主动防御机制——与其让用户拿到一个失效的代理,不如直接报错。
解法有哪些?
最推荐:重构代码,消除循环依赖。循环依赖往往是设计上耦合过重的信号,常见的重构手法:
- 将 A 依赖 B 的字段移到方法参数中,通过方法调用传递
- 引入中间层(如
EventPublisher),用事件驱动解耦 - 使用
@Lazy延迟加载——@Lazy会生成一个懒加载代理,真正用到时才去容器中获取
@Component
public class A {
@Lazy
@Autowired
private B b; // 通过懒加载代理解决循环依赖
}@Lazy 的原理是生成一个 JDK/CGLIB 代理,当真正调用 B 的方法时,代理才去容器中 getBean。这样 A 的创建不会被 B 阻塞,B 创建时 A 已经完成初始化和代理。
总结
Spring 三级缓存的设计是性能与正确性的平衡:
| 缓存 | 内容 | 用途 |
|---|---|---|
一级 singletonObjects | 完全初始化好的 bean | 正常获取 bean |
二级 earlySingletonObjects | 提前暴露的早期 bean | 循环依赖时获取半成品 |
三级 singletonFactories | ObjectFactory 工厂 | 延迟创建 AOP 代理,避免无谓的代理生成 |
理解三级缓存不能只背"三个 map",关键是理解每个 map 的职责边界和为什么需要这个边界。一级管成品,二级管早期引用缓存,三级管延迟代理——少一级功能不完整,多一级纯属浪费。
参考资料:Spring 源码 —
DefaultSingletonBeanRegistry.getSingleton();AbstractAutowireCapableBeanFactory.doCreateBean();AbstractAutoProxyCreator.getEarlyBeanReference()