欢迎来到 Spring 容器的**"死结解开中心"**
在分布式系统中,我们害怕死锁;在代码逻辑中,我们害怕死循环。但在 Spring 的 Bean 容器里,**循环依赖(Circular Dependency)**却是一个被精心设计的"特性",而非 Bug。
生活化比喻:
- 循环依赖 = "鸡生蛋,蛋生鸡" 。
A 说:"没有 B 我活不了。" B 说:"没有 A 我也活不了。"如果两人僵持不下,程序就死锁了(栈溢出)。- 三级缓存 = "半成品中转站" + "预言家工厂" 。
Spring 的做法是:A 刚出生(实例化),还没穿衣服(属性填充),就先把自己的一张**"裸照"** (早期引用)或者**"穿着未来衣服的照片"** (代理对象)拍下来,存进中转站。
当 B 需要 A 时,不用等 A 完全长大,直接去中转站拿这张照片先用着。等 B 长大了,A 再拿着 B 继续完成自己的成长。- 核心逻辑 :"先暴露半成品,再完成初始化"。
今天,我们要深入 DefaultSingletonBeanRegistry 的核心,用源码和逻辑推导,彻底讲透为什么必须是三级缓存 ,以及AOP 代理在这个局中扮演的关键角色。
场景复现:成功 vs 失败
首先,我们用代码看看 Spring 能解决什么,不能解决什么
@Component
class ServiceA {
@Autowired // 字段注入
private ServiceB serviceB;
public void doA() { serviceB.doB(); }
}
@Component
class ServiceB {
@Autowired // 字段注入
private ServiceA serviceA;
public void doB() { serviceA.doA(); }
}
// 结果:启动成功,运行正常。
场景二:构造器注入(失败)
Spring 直接报错:BeanCurrentlyInCreationException。
@Component
class ServiceA {
private final ServiceB serviceB;
// 构造器注入
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
@Component
class ServiceB {
private final ServiceA serviceA;
// 构造器注入
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
// 结果:启动报错!
// Reason: Requested bean is currently in creation: Is there an unresolvable circular reference?
报错堆栈
当你运行那个 ServiceA <-> ServiceB 的构造器循环依赖代码时,Spring 不会温柔地提示你,它会直接抛出 BeanCurrentlyInCreationException,并附带一段长长的堆栈:
org.springframework.beans.factory.BeanCurrentlyInCreationException:
Error creating bean with name 'serviceA': Requested bean is currently in creation:
Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:265)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:205)
// ... 省略中间调用 ...
at com.example.ServiceA.<init>(ServiceA.java:14) // 看这里!A 的构造函数在等 B
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
// ...
at com.example.ServiceB.<init>(ServiceB.java:14) // 看这里!B 的构造函数在等 A
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
// ... 无限递归直到栈溢出或 Spring 主动拦截 ...
at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:65)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1328)
注意看堆栈中的 Requested bean is currently in creation。
Spring 内部有一个 singletonsCurrentlyInCreation 的 Set,用来记录正在创建中的 Bean。
- 创建 A,把 "serviceA" 加入 Set。
- A 调构造器需要 B,去创建 B,把 "serviceB" 加入 Set。
- B 调构造器需要 A,发现 "serviceA" 已经在 Set 里了!
- Spring 此时判断:这是构造器注入,没法提前暴露引用(因为还没实例化完),于是直接抛异常,阻止栈溢出。
so、为什么构造器注入无法解决?
- 逻辑死锁 :构造器执行发生在实例化阶段 (第一步)。
- 创建 A -> 调用
new A(B)-> 需要先创建 B。 - 创建 B -> 调用
new B(A)-> 需要先创建 A。 - 死循环:A 等 B,B 等 A,谁都无法迈出"实例化"这第一步。
- 直接**"熔断机制",**报错也不能死循环
- 创建 A -> 调用
- 无隙可乘 :Spring 解决循环依赖的核心是**"提前暴露早期引用"** 。但这一步发生在实例化之后,属性填充之前 。构造器注入连"实例化"都没完成,根本没有机会把"早期引用"放出去。
尽量避免构造器循环依赖。如果必须,使用 @Lazy 注解注入一个代理对象来打破僵局
核心地图:三级缓存的真面目
Spring 解决循环依赖的秘密武器,藏在 DefaultSingletonBeanRegistry 类的三个 Map 中
// 1. 一级缓存:成品池
// 存放已经完全初始化好的 Bean(如果是 AOP Bean,这里存的是代理对象)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 2. 二级缓存:早期对象池
// 存放原始的 Bean 对象,或者提前生成的代理对象。
// 用于解决循环依赖,避免重复创建代理。
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
// 3. 三级缓存:工厂池 (核心中的核心)
// 存放 ObjectFactory<?>,这是一个函数式接口,用于按需生成早期引用。
// 关键点:它不存对象,存的是"创建对象的逻辑"。
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
灵魂拷问:为什么需要三级?两级不够吗?
很多教程说:"三级缓存是为了支持 AOP"。但具体是怎么支持的?如果只有两级会发生什么矛盾?
让我们通过逻辑推导来揭开谜底。
假设只有两级缓存(去掉 singletonFactories)
流程推演:
- A 实例化 (
new A())。 - A 需要暴露早期引用 :因为没有三级缓存的工厂,我们必须立即 决定暴露什么对象放入二级缓存
earlySingletonObjects。- 选择 1:暴露原始对象 A 。
- B 依赖 A,从二级缓存拿到原始 A。
- B 初始化完成。
- A 继续初始化,走到后置处理器(AOP 环节)。
- 问题 :A 需要被代理(比如加了
@Transactional)。Spring 创建了代理对象ProxyA。 - 灾难 :B 手里拿的是原始 A ,而容器最终注册的是
ProxyA。 - 后果 :B 调用 A 的方法时,事务切面失效!因为 B 调用的不是代理对象。而且容器中出现了两个 A 的实例(一个是 B 持有的原始 A,一个是容器管理的 ProxyA),破坏了单例原则。
- 选择 2:立即暴露代理对象
ProxyA。- 后果:要么报错(重复代理),要么性能浪费(提前创建了可能不需要的代理,或者创建了两次)。
- 灾难 :如果我们在第二步就创建了
ProxyA,而在正常的后置处理流程中,AnnotationAwareAspectJAutoProxyCreator又会尝试创建一次代理。 - 问题 :AOP 的创建通常依赖于 Bean 的完整状态(虽然大部分不依赖,但逻辑上 AOP 应该在初始化后期执行)。更严重的是,如果 A 没有发生循环依赖(即 B 不依赖 A),那么 A 会正常走完后置处理流程。
- 在 A 刚实例化完(还没填充属性,还没执行 Aware 回调),就强制创建代理对象
ProxyA放入二级缓存。
- 选择 1:暴露原始对象 A 。
三级缓存的"神来之笔":延迟决策
Spring第三级缓存 singletonFactories存储是一个 ObjectFactory (Lambda 表达式)
核心逻辑:
- 实例化后 :我不立即创建代理,也不立即暴露原始对象。我只把一个**"制造早期引用的工厂"**放进去。
- 只有当别人真正需要我时(发生循环依赖) :
- B 来找 A。
- B 调用工厂的
getObject()方法。 - 就在这一刻 ,工厂内部执行
getEarlyBeanReference()。 - 判断 :A 是否需要 AOP?
- 是 :立即创建代理对象
ProxyA,放入二级缓存,返回ProxyA。 - 否:返回原始对象 A,放入二级缓存。
- 是 :立即创建代理对象
- 如果没有循环依赖 :
- 工厂永远不会被调用。
- A 正常走完所有流程,在标准的后置处理阶段创建代理。
结论 :
三级缓存的核心价值在于**"延迟初始化代理对象"** 。它解决了**"提前暴露"** 和**"AOP 代理时机"**之间的矛盾。
- 它保证了:如果发生循环依赖,B 拿到的一定是正确的代理对象。
- 它保证了:如果不发生循环依赖,A 的代理对象只在标准流程中创建一次。
源码深挖:doCreateBean 中的关键三步
第一步:实例化后,存入三级缓存
位置 :AbstractAutowireCapableBeanFactory.doCreateBean()
// 1. 实例化 Bean (new A())
instanceWrapper = createBeanInstance(beanName, mbd, args);
Object exposedObject = instanceWrapper.getWrappedInstance();
// 2. 判断是否需要提前暴露(单例 + 允许循环依赖 + 正在创建中)
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
// 【关键】存入三级缓存
// 注意:这里传入的是一个 Lambda (ObjectFactory),而不是对象本身!
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, exposedObject));
}
解读:此时 A 只是个空壳。Spring 并没有创建代理,只是注册了一个"承诺":如果有人要 A,我就执行这个 Lambda 给他!
第二步:别人来借,调用工厂,升入二级缓存
位置 :DefaultSingletonBeanRegistry.getSingleton() (被 doGetBean 调用)
当 B 需要 A 时,会调用 getSingleton("A", false) (allowEarlyReference=true)。
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<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 【核心时刻】调用工厂,获取早期引用
singletonObject = singletonFactory.getObject();
// 4. 从三级移除,放入二级
this.singletonFactories.remove(beanName);
//朋友,加急整了
this.earlySingletonObjects.put(beanName, singletonObject);
}
}
}
return singletonObject;
}
- 只有当 B 真的需要 A 时,
singletonFactory.getObject()才会被执行。 - 在这个 Lambda 内部 (
getEarlyBeanReference),Spring 会调用SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference。 - 这正是
AnnotationAwareAspectJAutoProxyCreator介入的地方!如果需要 AOP,现在就创建代理! - 一旦创建,对象就从三级升到二级 ,保证后续再有人来借,拿到的都是同一个对象(避免重复创建代理)!!!🌟
第三步:自己完工,清理缓存,存入一级
位置 :AbstractAutowireCapableBeanFactory.doCreateBean() 结尾
// A 完成了属性填充、Aware 回调、初始化方法...
// 现在到了后置处理阶段 (postProcessAfterInitialization)
// 如果之前因为循环依赖已经创建了代理,这里会直接返回二级缓存中的代理对象
// 如果之前没创建(无循环依赖),这里才正式创建代理
// 最后,将成品放入一级缓存
addSingleton(beanName, exposedObject);
// addSingleton 内部会:
// 1. put into singletonObjects (一级)
// 2. remove from earlySingletonObjects (二级)
// 3. remove from singletonFactories (三级,以防万一)
迷你剧场:A (带事务) <-> B 的循环依赖之旅
假设 ServiceA 有 @Transactional,ServiceB 没有。A 依赖 B,B 依赖 A。
- 创建 A :
new ServiceA()(原始对象)。- 发现 A 正在创建中,将工厂放入三级缓存。
- 工厂逻辑:
if (needAOP) return createProxy(A); else return A;
- A 填充属性 :
- 发现需要
ServiceB。
- 发现需要
- 创建 B :
new ServiceB()。- B 填充属性,发现需要
ServiceA。
- B 寻找 A :
- 一级缓存?无。
- 二级缓存?无。
- 三级缓存?有工厂!
- 调用工厂
getObject()。 - 关键时刻 :工厂检测到 A 有
@Transactional,立即创建 A 的代理对象ProxyA。 - 将
ProxyA放入二级缓存,移除三级缓存。 - 返回
ProxyA给 B。 - B 持有
ProxyA。B 初始化完成,放入一级缓存。
- A 继续 :
- A 拿到 B,完成属性填充。
- A 执行 Aware 回调。
- A 执行
@PostConstruct等初始化方法。 - A 进入后置处理 (
postProcessAfterInitialization) :AnnotationAwareAspectJAutoProxyCreator再次介入。- 它检查二级缓存,发现 A 已经在里面了(且是
ProxyA)。 - 策略 :直接返回二级缓存中的
ProxyA,不再创建新代理。
- A 初始化完成,放入一级缓存,清理二、三级。
- 结局 :
- 容器中:
ServiceA=ProxyA。 - B 中持有的:
ServiceA=ProxyA。 - 完美一致!事务生效!
- 容器中:
实战锦囊------如何优雅地"破解"死锁?
当遇到 Spring 无法自动解决的循环依赖(如构造器注入、多例 Prototype)时,我们该怎么办?这里有三种"手术方案"。
1、@Lazy 注解(最推荐的"作弊"手段)
@Lazy 会让 Spring 注入一个代理对象,而不是真实对象。只有当你第一次调用方法时,代理对象才会去容器里获取真实的 Bean。这就打破了构造时的"即时依赖"。
@Component
class ServiceA {
private final ServiceB serviceB;
// 加上 @Lazy,注入的是 ServiceB 的代理
public ServiceA(@Lazy ServiceB serviceB) {
this.serviceB = serviceB;
}
public void doA() {
// 这里才真正触发 ServiceB 的初始化
serviceB.doB();
}
}
- 效果:构造器瞬间完成,因为代理对象创建很快,不需要等待 B 完全初始化。
- 适用场景 :无法修改架构,必须用构造器注入,且确定逻辑上不会在构造器内部直接调用 B 的方法。
2、实现 ApplicationContextAware 手动获取(下策)
先注入容器上下文,在需要的时候再去 getBean。这相当于把依赖查找从"编译期/启动期"推迟到"运行期"。这么说有点抽象,下面来看代码:
@Component
class ServiceA implements ApplicationContextAware {
private ApplicationContext context;
private ServiceB serviceB;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.context = applicationContext;
}
public void doSomething() {
// 用的时候再找,这时候 B 肯定已经好了
if (serviceB == null) {
serviceB = context.getBean(ServiceB.class);
}
serviceB.doB();
}
}
代码耦合了 Spring API,不优雅,测试困难。除非万不得已,不推荐。
架构重构(上上策)
环依赖往往是**设计 smell(代码异味)**的信号。说明这两个类职责耦合太紧。
-
提取第三方类:把 A 和 B 共同依赖的逻辑提取出来,变成 C。A 依赖 C,B 依赖 C。C 不依赖 A 和 B。
-
事件驱动:A 做完事发个事件,B 监听事件。解耦。
// 重构前:A <-> B
// 重构后:A -> C <- B
@Component
class CommonLogic { // C
public void sharedMethod() {}
}@Component
class ServiceA {
private final CommonLogic commonLogic; // 依赖 C
// ...
}@Component
class ServiceB {
private final CommonLogic commonLogic; // 依赖 C
// ...
}
验证实验------亲眼看见三级缓存的变化
我们来写一个测试,利用反射"黑入"Spring 容器,打印出三级缓存的内容,见证奇迹时刻
@SpringBootTest
public class CircularDependencyTest {
@Autowired
private ApplicationContext context;
@Test
public void testCircularReferenceCache() throws Exception {
// 1. 获取 DefaultSingletonBeanRegistry (通过反射访问私有字段)
DefaultListableBeanFactory factory = (DefaultListableBeanFactory) context.getAutowireCapableBeanFactory();
Field singletonObjectsField = DefaultSingletonBeanRegistry.class.getDeclaredField("singletonObjects");
Field earlySingletonObjectsField = DefaultSingletonBeanRegistry.class.getDeclaredField("earlySingletonObjects");
Field singletonFactoriesField = DefaultSingletonBeanRegistry.class.getDeclaredField("singletonFactories");
singletonObjectsField.setAccessible(true);
earlySingletonObjectsField.setAccessible(true);
singletonFactoriesField.setAccessible(true);
Map<String, Object> level1 = (Map<String, Object>) singletonObjectsField.get(factory);
Map<String, Object> level2 = (Map<String, Object>) earlySingletonObjectsField.get(factory);
Map<String, ObjectFactory<?>> level3 = (Map<String, ObjectFactory<?>>) singletonFactoriesField.get(factory);
// 2. 触发 Bean 创建 (假设 ServiceA 和 ServiceB 有循环依赖)
// 注意:如果是单例,启动时就已经创建好了。
// 为了观察过程,我们可以尝试在启动钩子中打断点,或者这里只是打印最终状态
System.out.println("=== 一级缓存 (成品池) ===");
level1.keySet().forEach(k -> System.out.println(" [L1] " + k + " -> " + level1.get(k).getClass().getSimpleName()));
System.out.println("\n=== 二级缓存 (早期对象池) ===");
// 正常启动完成后,二级缓存应该是空的,因为循环依赖已解决,对象都升入 L1 了
System.out.println("当前大小: " + level2.size() + " (正常应为 0)");
System.out.println("\n=== 三级缓存 (工厂池) ===");
// 正常启动完成后,三级缓存也应该是空的
System.out.println("当前大小: " + level3.size() + " (正常应为 0)");
// 想要看到非空状态?
// 你需要在 AbstractAutowireCapableBeanFactory.doCreateBean 方法中打断点。
// 当执行到 addSingletonFactory 后,但在 populateBean 之前,查看 level3,你会看到 ServiceA 的工厂!
}
}
- 启动完成后 :二、三级缓存通常是空的。因为循环依赖一旦解决,对象就会晋升到一级缓存,临时缓存会被清理。
- 调试技巧 :想看三级缓存里有东西?必须在
doCreateBean方法里打断点 ,停在addSingletonFactory之后。那时你会发现singletonFactories里躺着那个神奇的 Lambda!
边界与禁忌------Spring 搞不定的场景
不要以为有了三级缓存就万事大吉。以下场景,三级缓存直接失效,踩中必死。
1. Prototype (多例) 作用域
现象 :如果 A 是 Singleton,B 是 Prototype,且互相依赖。
结果 :报错 BeanCurrentlyInCreationException。
原因:
- Spring 的循环依赖缓存(一、二、三级)只针对 Singleton。
- Prototype Bean 每次请求都
new一个,容器不缓存,也不管理其完整生命周期(特别是销毁)。 - 既然不缓存,就没法"提前暴露引用"。B 每次要 A 都是新的,A 每次要 B 也是新的,死循环无解。
2. @Async 异步代理
现象 :A 和 B 互相依赖,且其中一个方法加了 @Async。
结果 :可能报错,或者 AOP 失效。
原因:
@Async会生成代理对象。- 如果在循环依赖中,早期引用暴露的是原始对象,而最终需要的是异步代理对象。
- 虽然三级缓存试图处理 AOP,但
@Async的代理创建逻辑比较特殊(有时在更晚的阶段),容易导致getEarlyBeanReference拿到的对象和最终对象不一致。 - 解决 :尽量避免在循环依赖链中使用
@Async,或者全部改为@Lazy。
3. 字段注入 vs 构造器注入的混合地狱
现象 :A 用构造器注入 B,B 用字段注入 A。
结果 :依然报错。
原因 :只要链条中任何一个环节是构造器注入,整个链条的"实例化"阶段就会卡死。木桶效应,最短的那块板(构造器)决定了死锁的发生。
复习时刻:深度回答
Q1: 为什么 Spring 解决循环依赖需要三级缓存?两级行不行
两级缓存不足以完美解决包含 AOP 代理 的循环依赖场景。
- 如果只有两级 :在 Bean 实例化后,我们必须立即决定向二级缓存放入"原始对象"还是"代理对象"。
- 若放入原始对象 :当该 Bean 需要 AOP 时,后续流程会创建代理对象。这导致其他 Bean 持有的是原始对象,而容器中最终注册的是代理对象,造成单例不一致 且AOP 失效。
- 若放入代理对象 :必须在实例化后立即创建代理。但这违背了 Spring 的设计原则(代理应在后置处理阶段创建)。且如果该 Bean 没有 发生循环依赖,会导致代理对象被提前创建 甚至重复创建,造成资源浪费或逻辑错误。
- 三级缓存的优势 :
- 第三级缓存存储的是
ObjectFactory(工厂函数),实现了延迟加载。 - 只有在真正发生循环依赖(其他 Bean 来索取)时,才调用工厂。
- 工厂内部根据是否需要 AOP,动态决定是返回原始对象还是立即创建代理对象。
- 这样既保证了循环依赖时拿到的是正确的代理对象,又保证了无循环依赖时代理对象按正常流程创建,实现了逻辑的严密性 和性能的最优解。
- 第三级缓存存储的是
Q2: 构造器注入的循环依赖为什么无法解决?
Spring 解决循环依赖的核心机制是**"提前暴露早期引用"** ,这一步发生在 Bean 的实例化之后、属性填充之前。
- 字段/Setter 注入:依赖注入发生在属性填充阶段。此时 Bean 已经实例化,有机会将早期引用放入缓存,供对方使用。
- 构造器注入 :依赖注入发生在实例化阶段 (构造函数执行时)。
- 创建 A 需要调用
new A(b),此时 B 还不存在。 - 为了创建 B,又需要调用
new B(a),此时 A 也还没实例化完成。 - 由于连实例化都没完成,Spring 根本没有机会执行"提前暴露引用"的逻辑。
- 这就形成了一个无法打破的死锁。
- 创建 A 需要调用
- 解决方案 :使用
@Lazy注解。Spring 会注入一个代理对象,实际调用时才去获取真实的 Bean,从而打破构造时的死锁。
Q3: 在循环依赖中,代理对象是什么时候创建的?
代理对象的创建时机取决于是否发生了循环依赖:
- 发生循环依赖时 :
- 当 Bean A 实例化后,将工厂放入三级缓存。
- 当 Bean B 依赖 A,从三级缓存获取工厂并调用
getObject()时。 - 在
getEarlyBeanReference()方法中,AnnotationAwareAspectJAutoProxyCreator会提前创建 A 的代理对象,并将其放入二级缓存返回给 B。
- 未发生循环依赖时 :
- 三级缓存的工厂不会被调用。
- Bean A 正常执行完属性填充、初始化方法后,进入
postProcessAfterInitialization阶段。 - 此时,
AnnotationAwareAspectJAutoProxyCreator按照标准流程创建代理对象。
最后:
- 痛点:构造器注入会死锁,字段注入能活。
- 原理:三级缓存(成品、早期、工厂)。
- 核心 :第三级缓存的
ObjectFactory实现了延迟决策 ,完美解决了AOP 代理 与提前暴露的时空矛盾。 - 实战 :
- 能改架构就重构(提取公共类)。
- 不能改就用
@Lazy(构造器注入神器)。 - 千万别在 Prototype 或复杂
@Async场景下硬搞循环依赖。
- 验证 :断点调试
doCreateBean,亲眼见证工厂的诞生。