概述
在[《依赖注入的精髓:@Autowired 与构造器注入的设计差异》]中,我们深入对比了构造器注入与设值注入在时机、强制性上的设计差异,并留下一个悬念:为何构造器注入无法解决循环依赖。在[《AOP 原理剖析:代理创建、拦截链与通知顺序》]里,我们剖析了 Spring AOP 代理对象的创建时机------通常在 Bean 初始化的 initializeBean 阶段。但现实中,一个被 AOP 增强的 Bean 完全可能处于循环依赖中。本篇,我们将汇聚这两股知识流,直击 Spring 框架最复杂、最精妙的部分:循环依赖的解决机制。
循环依赖是 Spring 面试的终极考题,也是线上故障的高发地带。表面上,它只是两个 Bean "你中有我,我中有你";实际上,它牵涉了 Spring 容器的核心生命周期、三级缓存的设计哲学、AOP 代理的创建时机,甚至是 JVM 内存管理的实战挑战。当单例 Bean 通过设值注入互相依赖时,Spring 是如何利用三级缓存进行"解耦"的?当其中一方需要 AOP 增强时,代理对象又是如何被提前创建的,以确保我们拿到的始终是那个"增强后的"引用?反过来,为什么原型 Bean 的循环依赖不仅无法解决,甚至会演变成内存泄漏的生产事故?所有这些问题的答案,都藏在了 DefaultSingletonBeanRegistry 那精巧的三级缓存设计里。
循环依赖并非单纯的"缓存结构"考题,其本质是对 Spring Bean 生命周期管理粒度的极限考验:容器在"实例化"与"完全就绪"两个状态之间,必须预留一个可供外界引用的"中间态"窗口,而三级缓存正是这一窗口的工程化实现。本文将以 DefaultSingletonBeanRegistry 为核心,系统拆解 singletonObjects、earlySingletonObjects 与 singletonFactories 之间的状态流转,并重点论证 ObjectFactory 延迟暴露机制与 getEarlyBeanReference 回调在保证 AOP 代理唯一性上的设计必然性。最终,读者将获得的不仅是一个具体问题的答案,更是一套可用于分析任何 IoC 容器"创建中状态管理"与"后置处理器交互"的通用思维框架。 核心要点速览
- 三级缓存结构 :
singletonObjects(成品仓库)、earlySingletonObjects(半成品暂存区)、singletonFactories(半成品工厂)并非简单的三张 Map,它们协同构成了一个解决循环引用和保证 AOP 代理唯一性的精密状态机。 - 单例 vs 原型的根本差异:容器是否持有并管理 Bean 的引用,是循环依赖能否被破解的基因级区别。单例 Bean 可以被"提前暴露",而原型 Bean 的"即用即抛"特性使其无法做到。
- 设值注入的解决流程 :解决循环依赖的黄金时机发生在"实例化"与"属性填充"之间。通过
addSingletonFactory提前暴露一个能产生早期引用的工厂,Spring 在属性填充阶段就能安全地注入依赖。 - 原型 Bean 的循环陷阱 :每一次对原型 Bean 的
getBean调用都是一次完整的"新生"。在循环依赖的场景下,这将触发无限的创建递归,导致实例膨胀直至OOM(OutOfMemoryError)。 singletonFactories的必要性 :如果只有二级缓存,一旦 Bean 需要被 AOP 增强,其普通对象和代理对象可能共存,导致容器内出现"真假美猴王"。第三级缓存ObjectFactory的设计,优雅地解决了代理对象必须提前创建且保证唯一性的难题。getEarlyBeanReference:这是SmartInstantiationAwareBeanPostProcessor暴露的"后门",专为 AOP 代理在循环依赖场景下提前登场而设计。- 构造器注入的死锁:无论单例还是原型,构造器注入的循环依赖都是一个无解的"先有鸡还是先有蛋"的死锁,因为进入构造器本身就是实例化的过程,此时还未有任何 Bean 引用可以被暴露。
@Lazy的破解原理 :@Lazy并非解决了循环依赖,而是"绕过了"它。它通过注入一个代理对象,将真实的依赖查找和注入延迟到首次方法调用的那一刻。
文章组织架构图
下图展示了本文的知识体系结构,我们将遵循从概念认知到源码剖析,再到工程实践的路径,构建完整的循环依赖知识闭环。
架构图说明
-
总览说明 :全文的 9 个模块构成了一个环环相扣的知识闭环。我们从概念分类(模块1)出发,率先打破"单例优先"的思维定式,首先探讨原型 Bean 为何失败(模块2) ,通过鲜明的反向对比,为理解单例缓存的精妙设计埋下伏笔。接着,我们直击核心的三级缓存数据结构(模块3) ,并在此基础上推演单例设值注入的完整解决流程(模块4) 。理解基本流程后,我们再深入到最复杂的交互------AOP 代理的提前暴露(模块5) 和构造器注入的死锁分析(模块6) 。在穷尽所有失败场景后,我们介绍**
@Lazy这一破解工具(模块7)。最终,将所有理论沉淀到 生产事故(模块8)** 和面试应答(模块9) 这两个实践模块中,完成从理论到实战的升华。 -
逐模块说明:
- 模块 1 建立问题认知:定义循环依赖,并按照作用域和注入方式两个维度建立一个完整的分类框架,为后续所有讨论划定范围。
- 模块 2 先讲失败的案例:特地将原型 Bean 的循环依赖提到最前面,是为了建立一个重要的对比坐标。理解"为什么原型不行",是深刻理解"单例为何需要复杂的缓存机制"的关键。
- 模块 3 是根基:详细拆解三级缓存的 Map 定义、Value 含义、读写时机。这是后续所有流程分析的基石。
- 模块 4 是主干流程 :通过 10 步详解,串联起
doCreateBean、addSingletonFactory、populateBean等核心方法,展示三级缓存如何协同工作。 - 模块 5 揭示核心价值 :回答"为什么必须是三级"这个终极问题。
singletonFactories的存在,本质上是为了处理 AOP 代理在循环依赖中的唯一性问题。 - 模块 6 分析经典死锁:从源码角度证明,构造器注入的循环依赖无论在何种作用域下都是死胡同。
- 模块 7 提供破解工具 :在穷尽框架自身能力后,介绍
@Lazy这种"非常规"手段及其原理。 - 模块 8、9 是理论的价值体现:将晦涩的源码机制映射到实际的生产故障排查和高频面试题中,使知识具备实战力。
-
关键结论 :学习循环依赖必须遵循"作用域差异 → 缓存结构 → 流程 → AOP 交互"的认知链条。原型 Bean 无法解决循环依赖的根本原因是容器不持有其引用,无法在实例化后、填充属性前将其"半成品"状态暴露出去,这与单例 Bean 的生命周期管理方式形成了根本性对立。
1 循环依赖概述与分类
循环依赖(Circular Dependency),简单来说就是两个或多个 Bean 实例之间构成一个引用上的闭环。例如,Bean A 的某个字段是 Bean B,而 Bean B 的某个字段又是 Bean A。这种情形在复杂的业务系统中很常见,尤其是遵循"领域驱动设计"时,实体之间相互引用更是常态。
要深入分析 Spring 如何应对循环依赖,我们不能一概而论,必须从两个核心维度进行精确分类:Bean 的作用域(Scope) 和 依赖的注入方式。
1.1 两种维度的分类
-
维度一:作用域(Scope)
- 单例(Singleton)Bean:在整个 IoC 容器中,一个 Bean 定义只对应一个共享实例。容器持有其引用,并负责管理其完整的生命周期。
- 原型(Prototype)Bean :每次向容器请求(调用
getBean)时,都会创建一个全新的实例。容器不持有其引用,创建并完成(部分)初始化后,就将其完全交给调用者,生命周期由调用者管理。
-
维度二:注入方式(Injection Type)
- 构造器注入(Constructor Injection):依赖通过构造函数的参数在 Bean 实例化阶段提供。
- 设值/字段注入(Setter/Field Injection) :依赖在 Bean 实例化完成后,通过
setter方法或反射直接设置字段值的方式提供,处于属性填充阶段。
1.2 循环依赖分类示意图
通过这两个维度,我们可以建立一个 2×2 矩阵,清晰地看到不同组合下循环依赖的可解性。
1.3 图表详解
- 图表主旨概括:本图通过"作用域"与"注入方式"两个正交维度,将循环依赖划分为四种不同的场景,并直接明了地标出每种场景是否可被 Spring 解决。
- 逐层/逐元素分解 :
- 节点 :
S(单例) 和P(原型) 是纵轴,C(构造器) 和F(设值) 是横轴,它们的交叉点代表了四种具体的组合场景。 - 结果 :
R1、R2、R3、R4是判定的结果。✅代表可解决,❌代表不可解决。 - 连接线:从概念到具体的结果,展示了不同维度的组合会产生截然不同的结局。
- 节点 :
- 设计原理映射:此图背后的设计思想是"关注点分离"。Spring 的容器设计从根本上区分了"对象的创建与组装"和"对象生命周期的管理"。作用域决定了后者,而注入方式决定了前者。循环依赖本质上是在"创建与组装"过程中发生的一个死锁,而"生命周期管理"提供了破解死锁所需的"中间状态存储空间"(即缓存)。
- 工程联系与关键结论 :
R2(单例 + 设值) 是 Spring 官方支持解决的唯一场景。R1和R3都是因为在实例化阶段就需要一个尚未存在的引用,导致死锁。构造器注入的循环依赖是无解的,无论作用域如何。R4(原型 + 设值) 虽然实例化和填充分离,但因为原型 Bean 不会被容器缓存其"早期引用",导致容器无法在后续的填充阶段提供同一个依赖,从而陷入无限创建或抛出异常。原型 Bean 的循环依赖是无法解决的,它要么因 Spring 的检测机制而提前抛出BeanCurrentlyInCreationException,要么因绕开检测而导致实例膨胀,最终引发 OOM。
| 作用域 | 注入方式 | 是否可解决 | 原因概要 |
|---|---|---|---|
| 单例 Singleton | 构造器 Constructor | ❌ 不可解决 | Constructor.newInstance 是一个原子操作,在其完成前,不完整的对象引用无法暴露给任何人。 |
| 单例 Singleton | 设值 Setter/Field | ✅ 可解决 | 实例化与初始化分离 。对象被 new 出来后可立即通过 ObjectFactory 暴露给缓存,等待后续填充。 |
| 原型 Prototype | 构造器 Constructor | ❌ 不可解决 | 与单例构造器注入类似,实例化时无法暴露引用,形成死锁。 |
| 原型 Prototype | 设值 Setter/Field | ❌ 不可解决 | 容器不持有原型 Bean 的引用,每次请求都是"重新创建",无法形成一个可供共享的"早期引用"缓存。 |
2 原型 Bean 循环依赖:无法解决的根本原因
通常,我们一提到循环依赖就想到单例和三级缓存。但让我们先从一个反直觉的场景切入:原型 Bean 的循环依赖。理解它的失败,才能更深刻地理解单例缓存的成功。
2.1 原型 Bean 的核心特征: "即用即抛"
原型 Bean 的精髓在于:容器只负责"生产配方",不负责"终身保养"。每次调用 applicationContext.getBean("protoA"),Spring 都会为你创建一个全新的、独立的实例。容器不会保留对该实例的任何引用,因此也无法管理它的销毁。
2.2 源码分析:doGetBean 中的原型逻辑
我们直接看 AbstractBeanFactory.doGetBean 方法中处理原型 Bean 的部分。
java
// org.springframework.beans.factory.support.AbstractBeanFactory
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
// ... 省略其他代码
// 1. 检查是否是原型 Bean, 并正在创建中 (用于检测循环依赖)
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
// ... 省略其他代码
// 2. 创建 Bean 实例
if (mbd.isPrototype()) {
// 如果是原型 Bean
Object prototypeInstance = null;
try {
// 2.1 在创建之前, 将 beanName 加入 prototypesCurrentlyInCreation (ThreadLocal)
beforePrototypeCreation(beanName);
// 2.2 走完整的创建流程: 实例化 -> 属性填充 -> 初始化
prototypeInstance = createBean(beanName, mbd, args);
}
finally {
// 2.3 创建完成后, 从 prototypesCurrentlyInCreation 移除
afterPrototypeCreation(beanName);
}
// 2.4 返回实例, 不做任何缓存操作
beanInstance = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}
// 3. 而单例 Bean 的处理完全不同...
else if (mbd.isSingleton()) {
// ... 涉及 getSingleton(beanName, () -> createBean(...)) 等复杂缓存逻辑
}
// ...
}
源码解读:
- 循环依赖检测 :
isPrototypeCurrentlyInCreation(beanName)检查当前 BeanName 是否在prototypesCurrentlyInCreation这个 ThreadLocal 集合中。这是 Spring 为原型 Bean 设置的"护城河",用于快速失败。 - 无缓存创建 :原型 Bean 的创建过程
createBean和单例一样,包括实例化、属性填充等。关键区别在于,createBean前后没有任何与singletonFactories、earlySingletonObjects等缓存打交道的逻辑 。addSingletonFactory只有在单例 Bean 的创建流程中才会被调用。 - "裸"返回:创建完成后,实例直接被返回,没有被放入任何一级缓存。
2.3 原型循环依赖的实例膨胀序列图
现在,假设 ProtoA 和 ProtoB 是原型 Bean,且互相依赖。
2.4 图表详解
- 图表主旨概括 :本序列图清晰地展示了原型 Bean 循环依赖时,
getBean调用如何陷入一个经典的重入检测陷阱,并最终抛出异常。 - 逐层/逐元素分解 :
- 步骤 1-4 :外部调用
getBean("protoA")触发了ProtoA的正常实例化与属性填充。因为没有addSingletonFactory调用(步骤 4 注释),ProtoA的早期引用无法被外界获知。 - 步骤 5-8 :在填充
ProtoA的protoB属性时,递归调用getBean("protoB")。ProtoB同样执行创建过程,并再次因为依赖ProtoA而触发递归。 - 步骤 9-11 :当
getBean("protoA")第二次被调用,Spring 的isPrototypeCurrentlyInCreation检测到了这个重入请求,立即抛出BeanCurrentlyInCreationException来终止这个无限循环。
- 步骤 1-4 :外部调用
- 设计原理映射 :这体现了"快速失败"的设计原则。Spring 深知原型 Bean 循环依赖是无解的,与其让程序在不可控的递归中耗尽栈帧(StackOverflowError)或内存,不如显式地用 ThreadLocal 进行标记检测,并提前抛出一个可诊断的异常。这便是
prototypesCurrentlyInCreation存在的价值。 - 工程联系与关键结论 :
- 这个序列图是理想情况。但如果循环链中牵连到单例 Bean,情况会变得极其隐蔽且危险。
- 关键结论:
prototypesCurrentlyInCreation只能检测"同一个原型 Bean 在同一个调用线程中正在被创建"的重入行为。如果循环链较长,特别是中间有单例 Bean 做"桥梁"时,这个检测可能被绕过,从而导致实例无限创建,最终引发 OOM。
2.5 内联示例:原型循环依赖导致 OOM
下面的示例模拟了一个单例 Bean SingletonTrigger 周期性地获取原型 Bean ProtoC,而 ProtoC 和 ProtoD 相互依赖(原型作用域)。这种场景下,BeanCurrentlyInCreationException 不会立刻抛出,但每次触发都会创建一对新的循环引用,最终撑爆内存。
java
// ========= 配置 =========
@Component
@Scope("prototype")
class ProtoC {
@Autowired
private ProtoD protoD;
}
@Component
@Scope("prototype")
class ProtoD {
@Autowired
private ProtoC protoC;
}
@Component
class SingletonTrigger {
@Autowired
private ApplicationContext context;
@PostConstruct
public void trigger() {
// 模拟并发或周期性调用
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
// 每次调用,都会触发 ProtoC->ProtoD->ProtoC... 的创建链
// 由于是原型,每次都是新实例,这会导致实例不断膨胀
// 并最终可能因 StackOverflowError 或 OOM 而使线程死亡
context.getBean(ProtoC.class);
} catch (Exception e) {
System.err.println(Thread.currentThread().getName() + " Error: " + e.getMessage());
// e.printStackTrace();
}
}).start();
}
}
}
// ========= 运行结果 =========
// 控制台将输出大量 StackOverflowError 或 BeanCurrentlyInCreationException 堆栈
// 如果存活时间足够长且并发量大,JVM 堆内存将被打满,最终 OOM。
2.6 单例与原型差异总结
通过此模块,我们能得出一个清晰的对比:
- 单例 Bean :容器持有引用。可以在实例化后、填充属性前,将"半成品"引用通过
ObjectFactory提前暴露出去,供其他 Bean 填充时使用。 - 原型 Bean :容器不持有引用。它是"一次性"的,创建完就交给调用者。不存在一个"全局可查询的半成品状态",因此 无法提前暴露。这是原型 Bean 无法解决循环依赖的根本原因。
3 三级缓存数据结构与源码解析
理解了原型 Bean 的局限性后,我们再回到 Spring 的"主舞台"------单例 Bean。单例 Bean 循环依赖的解决方案,就是这个著名的 三级缓存。
3.1 三级缓存的声明与源码
打开 DefaultSingletonBeanRegistry,我们可以看到这三者的声明:
java
// org.springframework.beans.factory.support.DefaultSingletonBeanRegistry
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
/** 一级缓存:存放完全初始化好的成品单例 Bean,即可以直接使用的 Bean */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** 二级缓存:存放提前暴露的单例 Bean 的早期引用(尚未完成属性填充和初始化),用于解决循环依赖 */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
/** 三级缓存:存放单例 Bean 的创建工厂。当存在循环依赖时,可通过 ObjectFactory.getObject() 获取 Bean 的早期引用 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/** 正在创建中的单例 Bean 的集合,用于循环依赖检测 */
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));
// ... 其他代码
}
3.2 缓存职责与流转关系图
这三个 Map 不是孤立存在的,它们之间有严格的流转顺序。
singletonObjects} L1Check -- 命中 --> ReturnL1[返回成品 Bean] L1Check -- 未命中 --> InCreationCheck{2. 是否正在创建中?
singletonsCurrentlyInCreation} InCreationCheck -- 否 --> ReturnNull1[返回 null] InCreationCheck -- 是 --> L2Check{3. 查二级缓存
earlySingletonObjects} L2Check -- 命中 --> ReturnL2[返回早期 Bean] L2Check -- 未命中 --> L3Check{4. 查三级缓存
singletonFactories} L3Check -- 命中 --> GetFactory[获取 ObjectFactory] GetFactory --> GetEarlyRef[调用 factory.getObject
获取早期引用] GetEarlyRef --> AddToL2[将早期引用
放入二级缓存] AddToL2 --> RemoveFromL3[从三级缓存移除
该 ObjectFactory] RemoveFromL3 --> ReturnL3[返回早期引用] L3Check -- 未命中 --> ReturnNull2[返回 null]
3.3 图表详解
- 图表主旨概括 :本图描述了 Spring 在
getSingleton(beanName, true)方法内部的四级查询逻辑(一级、isCreating、二级、三级),是三级缓存协同工作的核心状态机。 - 逐层/逐元素分解 :
- 一级缓存
singletonObjects:这是所有请求的首选目标。如果找到,直接返回一个完全就绪的"成品"Bean。 singletonsCurrentlyInCreation检查:这是一个性能优化和逻辑分流。如果 Bean 不在创建中,即使一级缓存没有,也说明它要么不存在,要么是原型 Bean,无需再去查二、三级缓存。- 二级缓存
earlySingletonObjects:如果一级未命中且 Bean 正在创建中,则查询二级。这里的 Bean 是"半成品"的早期引用,可能是一个普通对象,也可能是一个 AOP 代理对象。 - 三级缓存
singletonFactories:前两级都未命中,才轮到三级。这里存的是ObjectFactory。一旦通过工厂获取了早期引用,该引用会立刻升级到二级缓存,对应的工厂从三级缓存中清除。这保证了ObjectFactory.getObject()的getEarlyBeanReference逻辑对一个 Bean 只会执行一次。
- 一级缓存
- 设计原理映射 :此流程是 懒加载 与 单例性 的完美结合。
ObjectFactory的存在是"懒"的,即只有在发生循环依赖时才去真正执行getObject()。一旦执行,结果又被迅速提升到二级,保证了"单例性"(工厂的getObject只被调用一次)。 - 工程联系与关键结论 :
getSingleton(String beanName, boolean allowEarlyReference)中的allowEarlyReference参数是控制是否查询二、三级缓存的开关。在构造器注入的解析过程中,它是false,这就是构造器注入循环依赖无法解决的根本源码依据。
| 缓存级别 | Map 类型 | Key | Value | 写入时机 | 读取/移除时机 |
|---|---|---|---|---|---|
| 一级 (L1) | ConcurrentHashMap |
beanName | 完全就绪的 Bean 对象(或代理) | addSingleton 在 Bean 初始化完成后 |
发生循环依赖时,作为引用被注入 |
| 二级 (L2) | ConcurrentHashMap |
beanName | 早期暴露的 Bean 对象(或代理) | 从三级 ObjectFactory.getObject() 获取到早期引用后 |
被其他地方通过 getSingleton 获取;Bean 完成后会保留(实际代码中作为早期引用存在,但最终会被一级的成品覆盖,不过二级和三级会清理) |
| 三级 (L3) | HashMap |
beanName | 能产生 Bean 的 ObjectFactory |
addSingletonFactory 在 Bean 实例化后、属性填充前 |
getSingleton 调用 factory.getObject() 后,升级到 L2,自身被移除 |
特别强调 :这三级缓存仅为单例 Bean 服务。原型 Bean 的生命周期中,没有任何一个步骤会与这些缓存交互。
4 单例设值注入解决循环依赖的完整流程
这是 Spring 解决循环依赖最经典、最标准的路径。我们假设 Bean A (singleton) 和 Bean B (singleton) 通过 @Autowired 字段相互依赖。
4.1 完整流程序列图 (A ↔ B)
L1: miss, L2: miss, L3: hit! Cache-->>Container: 10. 返回"a"的ObjectFactory Container->>Factory: 11. getObject() -> getEarlyBeanReference(a_raw) Factory-->>Container: 12. 返回 a 的早期引用 (a_early_ref) Container->>Cache: 13. 将 a_early_ref 存入L2, 并从L3移除"a"的工厂 Container->>B_Instance: 14. 将 a_early_ref 注入 B.a 属性 Note over B_Instance: B now: a=a_early_ref, b=null Container->>B_Instance: 15. initializeBean(b): 执行初始化方法 Container->>Cache: 16. addSingleton("b", b) Note over Cache: B成品进入L1, L2/L3中B相关内容被清除 Note over Container, Cache: -- B 创建完毕, 返回 populateBean(a) -- Container->>A_Instance: 17. 将成品 B 注入 A.b 属性 Note over A_Instance: A now: a=null, b=B(成品) Container->>A_Instance: 18. initializeBean(a): 执行初始化方法 Container->>Cache: 19. addSingleton("a", a) Note over Cache: A成品(此时A.b引用着成品B)进入L1,
L2中的早期引用被替换/清除 Container-->>Client: 20. 返回完全就绪的 A
4.2 图表详解
- 图表主旨概括:此序列图完整演绎了基于设值注入的单例 Bean 循环依赖解决全过程,精确到每一步的缓存变化。
- 逐层/逐元素分解 :
- 步骤 2-3 (A 的实例化与暴露) :A 被
new出来后,仅仅是一个"空壳",但addSingletonFactory立刻将其以工厂的形式暴露到 L3 缓存。这是破解循环的关键第一步。 - 步骤 4-8 (A 触发 B 的创建):A 需要填充 B,容器去创建 B。B 同样经历了实例化、暴露 L3、属性填充。在填充 B 时,它需要 A。
- 步骤 9-14 (B 从缓存中找到并注入 A) :容器调用
getSingleton("a", true)。通过三级查询,在 L3 找到 A 的工厂,调用getObject生成 A 的早期引用,随后该引用被升级到 L2。B 使用这个早期引用来填充自己的a属性。此时,B 就变成了一个"持有 A 早期引用"的半成品。 - 步骤 15-16 (B 的完成):B 完成初始化和后续处理,作为一个完全就绪的"成品"被放入 L1 缓存。
- 步骤 17-19 (A 完成创建) :B 创建完成,流程回到 A 的
populateBean。A 拿到成品的 B 并注入。接着 A 也初始化完毕,成为"成品",进入 L1。
- 步骤 2-3 (A 的实例化与暴露) :A 被
- 设计原理映射 :这是 "基于中间状态的解耦" 的绝佳体现。通过
ObjectFactory和earlySingletonObjects,Spring 把"对象的完整初始化"这个原子操作,拆分成了"实例化"、"预暴露"、"属性填充"、"后置处理"等多个可中断、可介入的阶段,从而在两个相互依赖的对象间,建立起一个可共享的"承诺"或"欠条"。 - 工程联系与关键结论 :
- 此流程仅适用于单例 。步骤 3 和步骤 7 的
addSingletonFactory是一切的基础。 - 关键结论:A 最终持有的 B 是成品的 B,而 B 最初持有的 A 是 A 的早期引用。关键问题来了:当 A 最终完成初始化成为一个成品时,B 里持有的那个"A 的早期引用"会如何?答案是:B 持有的早期引用就是最终成品的引用(如果 A 不需要被代理),或者 B 持有的是 A 的代理对象(如果 A 需要被代理),这个代理对象内部最终会指向成品的 A。下文会详述 AOP 的影响。
- 此流程仅适用于单例 。步骤 3 和步骤 7 的
5 为什么需要 singletonFactories?AOP 代理的提前暴露
这就是三级缓存设计的精髓所在,也是征服 Spring 面试官的关键。问题直击要害:二级缓存存引用,三级缓存存工厂,为何不直接用二级?为什么非要多此一举?
答案:AOP (Aspect-Oriented Programming) 代理对象。
5.1 核心痛点:AOP 代理的"真假美猴王"
假设 A 和 B 循环依赖,并且 A 需要被 AOP 增强(比如 @Transactional)。
createBeanInstance:A 的普通对象(我们称之为A_Raw)被new出来。- 如果此时(在
addSingletonFactory中)我们直接将A_Raw放入二级缓存,那么 B 在填充时就会注入这个A_Raw。 initializeBean:A 的后置处理器(BeanPostProcessor)开始工作,AbstractAutoProxyCreator发现了 A 需要被增强,于是创建了一个 A 的代理对象(我们称之为A_Proxy)。- 最终,容器的一级缓存里存入的是
A_Proxy。这就出问题了:B 持有的是A_Raw,而整个容器和外部调用者拿到的是A_Proxy。一个容器里,同一个"概念上"的 Bean,出现了两个具有不同行为(一个有增强,一个没有)的实例。 这违背了单例原则,且 AOP 增强对 B 完全失效。
为了避免这个问题,就需要 ObjectFactory 和 getEarlyBeanReference 这队搭档。它们确保:只要 Bean 需要被代理,那么在"提前暴露"的那个瞬间,拿到的就必须已经是那个代理对象。
5.2 getEarlyBeanReference 调用链源码分析
java
// org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {
// ...
// 1. 实例化 Bean
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
final Object bean = instanceWrapper.getWrappedInstance();
// ...
// 2. 提前暴露 ObjectFactory (解决循环依赖的关键)
// 这里的 addSingletonFactory 是在 populateBean 之前就调用的
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
// 3. 属性填充 (populateBean)
populateBean(beanName, mbd, instanceWrapper);
// 4. 初始化 (initializeBean)
exposedObject = initializeBean(beanName, exposedObject, mbd);
// 5. 尝试获取早期引用,判断是否需要返回代理对象
if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
// ... 其他逻辑
}
}
return exposedObject;
}
getEarlyBeanReference 最终会调用到所有 SmartInstantiationAwareBeanPostProcessor 的 getEarlyBeanReference 方法,其中最关键的是 AOP 的核心处理器:
java
// org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator
public class AbstractAutoProxyCreator extends ProxyProcessorSupport
implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
// 将 (beanName, bean) 记录到 earlyProxyReferences 中,标记它已被提前处理过
if (!this.earlyProxyReferences.contains(cacheKey)) {
this.earlyProxyReferences.add(cacheKey);
}
// 重要:在这里就执行了创建代理的逻辑!
// wrapIfNecessary 会根据 Pointcut 匹配决定是否为当前 bean 创建代理
return wrapIfNecessary(bean, beanName);
}
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
// 如果这个 bean 已经被提前代理过了,就直接返回原始 bean,
// 避免在初始化后再次创建代理。
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
// 如果没有被提前处理过,则在初始化后常规创建代理
return wrapIfNecessary(bean, beanName);
}
}
return bean;
}
// ...
}
5.3 AOP 代理提前暴露的时序图
5.4 图表详解
- 图表主旨概括 :本图展示了当一个需要 AOP 增强的 Bean 处于循环依赖中时,
ObjectFactory和getEarlyBeanReference是如何协作,确保无论是提前暴露还是最终成品,都是同一个代理对象。 - 逐层/逐元素分解 :
- 步骤 8-12 :这是代理对象提前诞生的关键时刻。通过
ObjectFactory的懒加载能力,在依赖方 B 真正需要时,才调用getEarlyBeanReference。AOP 模块在此处检查并创建代理A_Proxy,并将其放入 L2。 - 步骤 13-15 :当 A 进行常规的
initializeBean流程时,AOP 处理器再次运转。但它在earlyProxyReferences中发现 A 已经被提前代理过了,于是 直接跳过了代理创建 ,返回原始的A_Raw。 - 步骤 16-17 :Spring 的最后检查机制发现,从 L2 拿到的早期引用 (
A_Proxy) 和initializeBean返回的对象 (A_Raw) 不一致,于是它正确地选择了将A_Proxy作为最终的成品放入一级缓存。
- 步骤 8-12 :这是代理对象提前诞生的关键时刻。通过
- 设计原理映射 :这应用了 "承诺链" (Promise Chain) 和 "单次执行" (Once-and-only-Once) 的设计思想。
ObjectFactory是一个延迟的承诺,getEarlyBeanReference是这个承诺的兑现动作,而earlyProxyReferences则保证了兑现动作仅发生一次。这完美地解耦了"提前暴露"和"最终初始化"两个阶段,并保证了代理对象的全局唯一性。 - 工程联系与关键结论 :
singletonFactories的存在,本质是为了给SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference提供一个执行入口,从而解决 AOP 代理的循环依赖问题。- 如果不存在 AOP 这类需要改变 Bean 最终引用形态的后置处理器,理论上二级缓存确实足够了(即直接将早期对象存入二级)。Spring 如此设计,是为了提供最为通用的、可扩展的解决方案。
6 构造器注入死锁分析
如果说设值注入的循环依赖是 Spring 通过精巧的三级缓存机制巧妙破解的难题,那么构造器注入的循环依赖就是一面无法逾越的墙。无论单例还是原型,在构造器循环依赖面前,结局都一样------无解的启动失败。
6.1 死锁的根源:实例化的原子性
构造器注入循环依赖失败的根本原因,在于 对象实例化过程本身的原子性 。当我们通过构造器创建对象时,JVM 层面的 newInstance 或 Constructor.newInstance() 调用必须首先获得所有构造器参数的值,才能完成对象的实例化。而在 Spring 容器中,这些参数值又是通过容器去解析和获取的。这就形成了一个经典的"鸡生蛋、蛋生鸡"困局。
让我们对比设值注入和构造器注入在 Bean 创建流程上的时序差异:
- 设值注入流程 :
实例化(new)→提前暴露到缓存→属性填充(设值)→初始化 - 构造器注入流程 :
解析构造器参数→获取参数对应的 Bean→实例化(new 带参构造器)→提前暴露到缓存→属性填充→初始化
关键差异一目了然:构造器注入必须先在容器中找到或创建构造器参数对应的 Bean,然后才能完成实例化。也就是说,"提前暴露"这个救命稻草,在构造器注入场景下根本来不及执行。
6.2 源码证据:ConstructorResolver.autowireConstructor
让我们通过源码来验证这一结论。当 Spring 处理构造器注入时,核心逻辑在 ConstructorResolver.autowireConstructor 方法中:
java
// org.springframework.beans.factory.support.ConstructorResolver
public BeanWrapper autowireConstructor(String beanName, RootBeanDefinition mbd,
@Nullable Constructor<?>[] chosenCtors, @Nullable Object[] explicitArgs) {
// ...
try {
// ... 省略构造器选择逻辑
// 1. 解析构造器参数。如果参数是 Bean 引用,则在此处调用 getBean
// argsToUse 数组的每一个元素,都是通过 resolveConstructorArguments 从容器中获取
Object[] argsToUse = resolveConstructorArguments(beanName, mbd, bw, constructorToUse, argsToResolve, true);
// 2. 参数就绪后,才调用 newInstance 创建对象
// 此时所有依赖必须已经解析完毕!
beanInstance = beanFactory.getInstantiationStrategy().instantiate(
mbd, beanName, beanFactory, constructorToUse, argsToUse);
// 3. 实例化完成后,设置 BeanWrapper
bw.setBeanInstance(beanInstance);
return bw;
}
// ...
}
// 参数解析的核心
private Object[] resolveConstructorArguments(String beanName, RootBeanDefinition mbd, BeanWrapper bw,
Constructor<?> constructor, Object[] argsToResolve, boolean fallback) {
// ...
for (int argIndex = 0; argIndex < argsToResolve.length; argIndex++) {
// 这里的 resolveAutowiredArgument 最终会调用 beanFactory.getBean()
// 也就是说,在 Bean A 的构造器被真正调用之前,容器已经在尝试 getBean("B") 了
Object resolvedArg = resolveAutowiredArgument(
methodParam, beanName, autowiredBeanNames, converter, fallback);
resolvedArgs[argIndex] = resolvedArg;
}
return resolvedArgs;
}
源码解读:
- 参数解析先行 :
resolveConstructorArguments是整个过程的关键。它的作用是将构造器声明中的每一个参数,转换成实际可以传入的 Bean 实例。对于@Autowired标注的构造器参数,这个方法会同步调用beanFactory.getBean()去获取依赖的 Bean。 - 实例化后置 :只有当所有的构造器参数都成功解析(即所有依赖的 Bean 都已创建或可获取)后,
instantiate方法才会被调用。此时,对象的空壳才第一次被创建出来。 addSingletonFactory的时机错过 :回到我们在第 5 章中分析的doCreateBean方法。addSingletonFactory是在createBeanInstance返回之后才被调用的。而createBeanInstance内部就包含了整个构造器注入的流程。这意味着,在构造器注入场景下,整个解析和创建依赖 Bean 的递归过程,全部发生在addSingletonFactory之前。
6.3 构造器注入循环依赖的死锁序列图
让我们通过序列图直观地展示单例 Bean A 和 B 通过构造器互相依赖时的死锁过程,并与设值注入的成功流程做对比。
尚未被 new 出来,更未调用 addSingletonFactory Container->>Container: 6. 发现 singletonsCurrentlyInCreation 已包含 "a" Container-->>Container: 7. 抛出 BeanCurrentlyInCreationException ! Note over Container: 死锁!启动失败
6.4 图表详解
- 图表主旨概括 :本序列图展示了构造器注入循环依赖的经典死锁场景,强调了创建流程永远走不到
addSingletonFactory这一步,从而与设值注入的成功流程形成鲜明对比。 - 逐层/逐元素分解 :
- 步骤 2-3 (A 创建的开始与中断) :容器开始创 建 A,但在
createBeanInstance阶段,为了调用 A 的构造器,必须先去获取参数 B。A 的创建过程被"挂起",转而进入 B 的创建。 - 步骤 4-5 (B 创建时同样中断) :对称地,B 在创建时,为了调用自己的构造器,又必须去获取 A。此时,容器尝试通过
getSingleton在缓存中查找 A。 - 步骤 6-7 (死锁判决) :缓存查询全部未命中。因为 A 的创建流程被卡在
createBeanInstance里,根本没有机会执行到addSingletonFactory来暴露自己。最终,Spring 通过singletonsCurrentlyInCreation发现 A 已经在一个创建线程中,形成了重入,于是抛出异常。
- 步骤 2-3 (A 创建的开始与中断) :容器开始创 建 A,但在
- 设计原理映射 :这是一个典型的 "资源获取即初始化" (RAII) 的失败案例。构造器注入要求 Bean 在"出生"那一刻就具备完整的依赖,这剥夺了 Spring 容器管理它"中间状态"的机会。
- 工程联系与关键结论 :
- 此死锁对单例和原型都成立 。因为无论是单例还是原型,
createBeanInstance都是它们生命周期的第一个步骤,addSingletonFactory永远在其之后。 - 关键结论:构造器注入的循环依赖是无法通过任何缓存技巧解决的。解决它的唯一方法是改变依赖注入的方式(改为设值注入)或打破循环。
- 此死锁对单例和原型都成立 。因为无论是单例还是原型,
6.5 内联示例:构造器注入死锁 (单例与原型)
下面的代码将分别演示单例和原型 Bean 在构造器循环依赖下的表现。
java
// ========= 场景1: 单例构造器循环依赖 =========
@Component
class SingletonA {
private final SingletonB b;
@Autowired // 构造器注入
public SingletonA(SingletonB b) {
this.b = b;
}
}
@Component
class SingletonB {
private final SingletonA a;
@Autowired // 构造器注入
public SingletonB(SingletonA a) {
this.a = a;
}
}
// ========= 场景2: 原型构造器循环依赖 =========
@Component
@Scope("prototype")
class PrototypeC {
private final PrototypeD d;
@Autowired
public PrototypeC(PrototypeD d) {
this.d = d;
}
}
@Component
@Scope("prototype")
class PrototypeD {
private final PrototypeC c;
@Autowired
public PrototypeD(PrototypeC c) {
this.c = c;
}
}
// ========= 启动代码 =========
// 两个场景都会导致 ApplicationContext 创建失败,抛出 BeanCurrentlyInCreationException。
// 错误信息中会明确提示 "Is there an unresolvable circular reference?"
// 单例和原型的表现一致,都是无法启动。
7 @Lazy 破解循环依赖的原理
面对构造器循环依赖这个"死局",Spring 提供了一个"非暴力不合作"的破解工具:@Lazy 注解。
7.1 @Lazy 的核心机制:延迟代理
@Lazy 的原理并非解决了循环依赖,而是 "绕过了" 循环依赖。它通过在依赖方注入一个代理对象,将真正依赖 Bean 的创建和注入时机,延迟到代理对象上的某个方法被第一次调用的那一刻。
这个代理对象是"轻量级"的,它在创建时不需要真实的依赖 Bean 存在。容器可以轻松地创建它,并将其注入到需要它的 Bean 中。这样,两个 Bean 的实例化流程就都成功完成,循环依赖的链条被代理对象这个"缓冲垫"给切断了。
7.2 源码分析:ContextAnnotationAutowireCandidateResolver
@Lazy 在注入点的处理逻辑,主要由 ContextAnnotationAutowireCandidateResolver 完成。
java
// org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver
public class ContextAnnotationAutowireCandidateResolver extends QualifierAnnotationAutowireCandidateResolver {
@Override
public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) {
// 1. 检查注入点是否标注了 @Lazy
if (!isLazy(descriptor)) {
return null;
}
// 2. 如果标注了,则创建一个延迟解析代理
// 注意,这里并没有去真正获取 Bean,而是返回了一个代理对象
return buildLazyResolutionProxy(descriptor, beanName);
}
protected Object buildLazyResolutionProxy(final DependencyDescriptor descriptor, final @Nullable String beanName) {
// ... 创建 ProxyFactory 或使用 JdkDynamicAopProxy
// 3. 这个代理对象在每次方法调用时,会执行以下逻辑:
// a. 检查内部是否已持有真实 Bean 引用
// b. 如果没有,调用 beanFactory.getBean(beanName) 获取真实 Bean
// c. 将方法调用委托给真实 Bean
// 这样一来,真正的 getBean 操作被延迟到了方法调用时
return proxyInstance;
}
}
源码解读:
- 条件检查 :
getLazyResolutionProxyIfNecessary方法在处理@Autowired注入点时被调用。它会检查注入点(构造器参数、字段、Setter 方法)是否标注了@Lazy。 - 代理创建 :如果标注了,它不会调用
beanFactory.getBean(),而是调用buildLazyResolutionProxy创建一个代理。这个代理实现了与依赖类型相同的接口。 - 延迟机制 :代理对象内部持有
BeanFactory的引用和beanName。只有当代理对象上的方法被真正调用时,它才会第一次触发getBean(beanName)来获取真实的目标 Bean,并将后续调用委托给它。这个代理的创建和注入完全不需要目标 Bean 的存在,因此可以无缝绕开实例化阶段的死锁。
7.3 @Lazy 的效果与代价
- 对单例和原型都有效 :
- 单例 Bean :代理对象会在第一次方法调用时,通过
getBean获取目标单例 Bean。之后,该代理对象会一直持有这个单例引用,后续调用不会再查容器。 - 原型 Bean :代理对象在每次方法调用时,都会通过
getBean获取一个全新的目标原型 Bean 实例。这符合原型 Bean 的语义。
- 单例 Bean :代理对象会在第一次方法调用时,通过
- 代价与注意事项 :
- 性能开销:每次通过代理对象进行方法调用时,都会多一层间接调用。对于单例 Bean,这个开销只在第一次调用时发生;对于原型 Bean,每次调用都有该开销。
- 类型暴露:由于代理是基于接口的(JDK 动态代理)或基于类的(CGLIB,但较少用于此),如果依赖是具体类且没有实现接口,可能需要额外的 CGLIB 配置。
- 语义复杂性 :
@Lazy改变了 Bean 的可用时机,增加了系统的隐性复杂度。在排查问题时,代理对象的存在可能会让调用栈看起来不那么直观。 - 并非银弹 :
@Lazy应该被视为一种"最后的手段"。优先考虑重构代码来消除不必要的循环依赖,或者使用设值注入。
7.4 内联示例:使用 @Lazy 解决构造器循环依赖
java
// ========= 使用 @Lazy 之前的死锁代码 =========
@Component
class Alpha {
private final Beta beta;
public Alpha(@Autowired Beta beta) { // 构造器循环依赖,启动失败
this.beta = beta;
}
}
@Component
class Beta {
private final Alpha alpha;
public Beta(@Autowired Alpha alpha) {
this.alpha = alpha;
}
}
// ========= 使用 @Lazy 之后的解决方案 =========
@Component
class AlphaFixed {
private final BetaFixed beta;
// 在注入点添加 @Lazy 注解
public AlphaFixed(@Autowired @Lazy BetaFixed beta) {
this.beta = beta;
System.out.println("AlphaFixed 构造完成,beta 是一个代理: " + beta.getClass());
}
public void callBeta() {
beta.sayHello();
}
}
@Component
class BetaFixed {
// 只需在循环链的某一端加上 @Lazy 即可
private final AlphaFixed alpha;
public BetaFixed(@Autowired AlphaFixed alpha) { // 这边不需要 @Lazy
this.alpha = alpha;
}
public void sayHello() {
System.out.println("Hello from BetaFixed!");
}
}
// 在此配置下,应用可以正常启动。AlphaFixed 中的 beta 实例是一个延迟代理,
// 直到 alpha.callBeta() 被调用时,代理才会去容器中获取真正的 BetaFixed Bean。
8 生产事故排查专题
理论最终要为实践服务。以下三个真实世界中的事故案例,将展示循环依赖是如何从代码中的微妙错误演变为严重的线上故障的。
8.1 事故一:原型 Bean 循环依赖导致 OOM
- 现象 :某电商订单服务在晚高峰期间突然响应变慢,随后 Full GC 频繁,最终堆内存溢出(
java.lang.OutOfMemoryError: Java heap space),服务宕机。严重时宕机后重启,几分钟内再次 OOM。 - 排查 :
- 堆转储分析 :运维拿到 Heap Dump 文件,用 MAT (Memory Analyzer Tool) 分析。发现
com.example.order.PriceCalculator和com.example.order.DiscountEngine这两个类的实例数量异常庞大,各自有数十万个实例,占用了堆内存的绝大部分。 - 代码审查 :检查这两个 Bean 的定义,发现它们都被注解为
@Scope("prototype")。进一步查看,PriceCalculator内部@Autowired了DiscountEngine,而DiscountEngine也@Autowired了PriceCalculator。 - 调用链追踪 :发现一个核心的
@Service单例 Bean (OrderService),在其方法中通过applicationContext.getBean(PriceCalculator.class)来获取新的计算器实例,为每个订单执行价格计算。
- 堆转储分析 :运维拿到 Heap Dump 文件,用 MAT (Memory Analyzer Tool) 分析。发现
- 根因 :
- 循环 :
PriceCalculator(原型) ⇄DiscountEngine(原型)。 - 触发 :每次调用
OrderService的方法,都会触发getBean(PriceCalculator.class)。 - 膨胀 :由于是原型 Bean,容器无法缓存早期引用。虽然
prototypesCurrentlyInCreation能检测到最直接的循环(即PriceCalculator创建时直接触发另一个PriceCalculator创建),但在这个场景中,循环是A->B->A。当容器创建PriceCalculator,注入DiscountEngine时,它去创建DiscountEngine;创建DiscountEngine注入PriceCalculator时,isPrototypeCurrentlyInCreation("priceCalculator")为true,理应抛出异常。 - 异常被掩盖 :在某些 Spring 版本或复杂调用中,如果这个
getBean调用不在一个简单的循环里,而是被其他 BeanPostProcessor 或其他逻辑包裹,这个异常可能被捕获或转化为一个延迟错误,或者由于多层嵌套调用,prototypesCurrentlyInCreation的移除时机存在延迟,导致检测失败,实例还是在不断创建直到栈溢出或内存耗尽。更常见的情况是,这种循环是在一次高并发请求下,不断有新的线程进入循环创建的死胡同,每个线程都在堆上留下一堆"半成品"实例,最终撑爆内存。
- 循环 :
- 解决 :
- 紧急处理 :重启服务,并临时通过配置中心将这两个 Bean 的作用域改为
@Scope("singleton"),立竿见影地稳定了内存和GC。 - 根本解决:重构代码。将价格计算和折扣计算逻辑从有状态的 Spring Bean 中剥离出来,变成无状态的工具类或策略模式,不再作为原型 Bean 注入,从根本上消除循环依赖。
- 备选方案 :如果必须保留原型,使用
@Lookup方法注入(详见后续篇章)来获取依赖,打破直接的@Autowired注入。
- 紧急处理 :重启服务,并临时通过配置中心将这两个 Bean 的作用域改为
- 最佳实践 :
- 原型 Bean 应保持简单,避免互相依赖。它们是"用完即弃"的,复杂的依赖网会使它们的行为变得极其危险。
- 仔细评估原型 Bean 的生命周期和持有者 。谁负责管理原型 Bean 的实例?高并发场景下,任何一次不经意的
getBean都可能被放大为性能或内存问题。
8.2 事故二:@Async + 循环依赖导致双重代理与 ClassCastException
- 现象 :应用启动正常,但在调用
PaymentService的process()方法时,抛出java.lang.ClassCastException: com.sun.proxy.$Proxy123 cannot be cast to com.example.service.PaymentServiceImpl。 - 排查 :
- 异常定位 :错误发生在一个注入点,代码是
@Autowired private PaymentServiceImpl paymentService;。 - Bean 定义检查 :
PaymentServiceImpl类上有@Service注解,并且有一个方法标记了@Async。- 该类与
OrderServiceImpl存在循环依赖。
- 代理类型分析 :
@Async注解会使 Spring 创建一个PaymentServiceImpl的代理。- 默认情况下,如果
PaymentServiceImpl实现了某个接口,Spring 会优先使用 JDK 动态代理(创建一个$Proxy),其类型是接口的子类型,而不是PaymentServiceImpl的子类。 - 但在循环依赖中,当
OrderServiceImpl被创建并需要注入PaymentServiceImpl时,Spring 为了提前暴露,通过getEarlyBeanReference创建了PaymentService代理。 - 最终,容器中
PaymentService这个 Bean 是一个 JDK 动态代理对象,它无法强制转换为PaymentServiceImpl这个具体类。
- 异常定位 :错误发生在一个注入点,代码是
- 根因 :
- AOP 代理类型选择 :当目标类有接口时,
@Async的AsyncAnnotationBeanPostProcessor默认采用 JDK Proxy。 - 注入点类型不匹配 :开发者使用
@Autowired private PaymentServiceImpl paymentService;,强制要求注入具体类的实例,这与容器中实际的 JDK Proxy 对象类型冲突。 - 循环依赖加剧暴露:循环依赖迫使 AOP 代理提前创建,如果启动时没有问题,这个问题在初始化后依然会暴露,但循环依赖中的早期暴露可能会让一些日志显得更加混乱。
- AOP 代理类型选择 :当目标类有接口时,
- 解决 :
- 方案一(推荐) :依赖倒置,面向接口编程。将注入点改为
@Autowired private PaymentService paymentService;。 - 方案二 :强制
@Async使用 CGLIB 代理。在application.properties中设置spring.aop.proxy-target-class=true。这会使得PaymentService的代理是PaymentServiceImpl的一个 CGLIB 子类,强制转换就不会有问题。
- 方案一(推荐) :依赖倒置,面向接口编程。将注入点改为
- 最佳实践 :
- 始终面向接口编程。这是 Java 和 Spring 的最佳实践,能避免绝大部分代理相关的类型转换问题。
- 谨慎使用
proxy-target-class=true。虽然方便,但它会强制所有代理都走 CGLIB,引入了额外的库依赖和微小的性能开销。
8.3 事故三:循环依赖 Bean 的 @PostConstruct 中访问对方方法导致 NPE
- 现象 :应用启动时,在
BeanA的@PostConstruct方法中调用beanB.doSomething(),有一定概率抛出NullPointerException,但又不总是出现。 - 排查 :
- 代码审查 :
BeanA和BeanB均是单例,通过设值注入互相依赖。BeanA的@PostConstruct方法中有this.beanB.doSomething()的调用。 - 场景还原 :这描述了我们在第4章中详细分析的时序问题。当 Spring 创建
BeanA,在注入beanB时创建BeanB。BeanB注入了beanA的早期引用 ,然后完成自己的初始化(包括@PostConstruct)。然后BeanA注入了成品beanB,接着执行自己的@PostConstruct。 - 关键问题 :
BeanA的@PostConstruct中引用的beanB已经是完全初始化好的成品 Bean,通常不会为 null。那么 NPE 从何而来?答案是beanB的某个内部状态可能还不完整。
- 代码审查 :
- 根因 :
- 状态不一致 :虽然
beanB对象本身不为 null,并且它的@PostConstruct已执行完毕,但它依赖的beanA引用指向的还是一个"早期引用",一个还没有执行@PostConstruct的BeanA。如果BeanB的doSomething()方法依赖于BeanA的某个在@PostConstruct中才初始化的状态(例如,加载到缓存中的数据、初始化的连接池等),那么BeanA此时调用beanB.doSomething(),就可能访问到BeanB内部一个"半生不熟"的BeanA,从而间接导致错误或 NPE。
- 状态不一致 :虽然
- 解决 :
- 打破循环:这是最彻底的解决方案。
- 分离初始化逻辑 :将必须在对方完全就绪后执行的逻辑,移到
ApplicationListener<ContextRefreshedEvent>或SmartLifecycle.start()中,确保在所有 Bean 都完全初始化后再执行。 - 使用
@DependsOn:在某些场景下,可以通过@DependsOn保证初始化顺序,但它不能完全解决循环依赖中的相互访问问题。
- 最佳实践 :
@PostConstruct方法应该是自包含的 。避免在@PostConstruct中调用其他 Bean 的方法,尤其是那些可能存在循环依赖的 Bean。初始化逻辑的相互调用是滋生复杂 Bug 的温床。
9 面试高频专题
本专题与正文严格分离,旨在为读者提供一份应对 Spring 循环依赖面试的"终极题库"。
9.1 基础认知
1. 什么是循环依赖?Spring 能解决哪些类型的循环依赖?
- 标准回答 :循环依赖是两个或多个 Bean 互相持有对方引用形成的闭环。Spring 只 能解决单例(Singleton)Bean 通过设值(Setter/Field)注入形成的循环依赖。构造器注入和原型(Prototype)作用域的循环依赖无法解决。
- 多角度追问 :
- 原型为什么不行? → 追问容器对原型 Bean 生命周期的管理差异,指向
prototypesCurrentlyInCreation和addSingletonFactory的缺失。 - 构造器为什么不行? → 追问
createBeanInstance和addSingletonFactory的时序,以及resolutionConstructorArguments中的死锁点。 - 如果我混合作用域呢?(如单例依赖原型) → 原型作为单例的依赖是可以的,因为单例只创建一次,它持有的原型引用在
populateBean时获取一次新的即可。问题在于原型依赖单例 通常也没问题,但 原型依赖原型 就会出问题。
- 原型为什么不行? → 追问容器对原型 Bean 生命周期的管理差异,指向
- 加分回答 :能结合 2x2 矩阵进行清晰分类,并指出 Spring 在设计上通过
prototypesCurrentlyInCreation对原型循环依赖做了"快速失败"的检查。
2. prototype 作用域的 Bean 能解决循环依赖吗?为什么?
- 标准回答 :不能。因为容器不持有原型 Bean 的引用,不会将其放入任何缓存(
singletonFactories等),因此无法提前暴露。每当发生循环请求时,要么被prototypesCurrentlyInCreation检测到并抛出BeanCurrentlyInCreationException,要么在复杂调用中绕开检测导致实例无限创建,最终 OOM。 - 多角度追问 :
prototypesCurrentlyInCreation的原理? → ThreadLocal 变量,记录当前线程正在创建的原型 Bean 名称。- 和
singletonsCurrentlyInCreation的区别? → 一个针对原型,用于快速失败/检测;一个针对单例,是三级缓存查询的"开关"。 - 有没有办法绕过? → 有,如果循环链中有单例作为桥梁,可能使检测失效,导致实例膨胀。
- 加分回答:能画出原型循环依赖的序列图,并解释其与 OOM 的关联。
9.2 缓存机制
3. Spring 为什么要使用三级缓存?二级缓存不够吗?每一级缓存的作用是什么?
- 标准回答 :三级缓存的存在是为了解决 AOP 代理对象需要提前暴露 的问题。一级缓存
singletonObjects存成品,二级缓存earlySingletonObjects存早期引用,三级缓存singletonFactories存能产生早期引用的工厂。如果没有三级缓存的ObjectFactory,就无法在"需要提前暴露"的时刻调用getEarlyBeanReference来生成代理,导致最终容器内出现普通对象和代理对象两个版本。 - 多角度追问 :
- 如果没有 AOP,是不是二级就够了? → 是的,理论上直接将早期引用放入二级可以。Spring 的设计是为了通用性和扩展性。
ObjectFactory的getObject会被调用几次? → 至多一次。一旦调用,其生成的对象被升入二级缓存,工厂本身从三级缓存移除。- 这个设计模式叫什么? → 类似于"懒加载"和"单例注册"模式的结合。
- 加分回答 :能结合
AbstractAutoProxyCreator.getEarlyBeanReference和earlyProxyReferences的源码,解释代理对象是如何保证唯一性的。
4. 三级缓存分别是什么类型?它们的 Key 和 Value 分别是什么?
- 标准回答 :三者都是
Map<String, ...>。singletonObjects(ConcurrentHashMap): Key=beanName, Value=完全就绪的成品 Bean。earlySingletonObjects(ConcurrentHashMap): Key=beanName, Value=提前暴露的早期 Bean 引用(可能是普通对象或代理对象)。singletonFactories(HashMap): Key=beanName, Value=ObjectFactory<?>,一个能生成早期引用(调用getEarlyBeanReference)的工厂。
- 多角度追问 :
- 为什么一级和二级是
ConcurrentHashMap,三级是HashMap? → 一级、二级是全局共享,有并发读写需求;三级的操作严格在同步块或单线程的 Bean 创建流程中,且生命周期极短,因此无需线程安全。 ObjectFactory是一个接口,它的实现在哪? → 以 Lambda 形式在doCreateBean中传入。- Value 为
Object和ObjectFactory的区别是什么? →Object是一个固化的结果,ObjectFactory是一次可以延迟执行的逻辑。
- 为什么一级和二级是
- 加分回答 :能解释第三级用
HashMap并用synchronized锁保护,体现了 Spring 对性能的极致追求。
5. singletonFactories 中为什么必须用工厂而不是直接存 Bean?与 AOP 的关联是什么?
- 标准回答 :如果直接存原始对象,当发生循环依赖且该 Bean 需要 AOP 增强时,被注入的依赖方将拿到一个没有被增强的原始对象 。而容器最终会存放一个增强后的代理对象 。这就导致了同一个 Bean 在容器内存在两个行为不同的版本 。使用
ObjectFactory,可以在真正需要提前暴露引用时,调用getEarlyBeanReference,确保在这个时刻就直接创建出代理,从而保证全局唯一性。 - 多角度追问 :
getEarlyBeanReference只创建代 理吗? → 不只是 AOP,任何SmartInstantiationAwareBeanPostProcessor都可能在此阶段修改 Bean。- 如何在
initializeBean阶段避免重复创建代理? → 通过earlyProxyReferences标记。 - 如果不解决这个"真假美猴王"问题,会发生什么? → AOP 增强对内部依赖方失效,比如事务不起作用。
- 加分回答 :能举出
@Async和@Transactional的代理同时存在的复杂案例。
9.3 核心流程
6. 设值注入解决单例循环依赖的完整流程是什么?从 doCreateBean 到最终 Bean 就绪的每一步。
- 标准回答 :详见第 4 章的 20 步序列图和说明。核心是:A 实例化 → A 暴露
ObjectFactory到 L3 → A 填充属性时发现需要 B → B 实例化 → B 暴露工厂到 L3 → B 填充属性时需要 A →getBean("A")从 L3 获取 A 的工厂并生成 A 的早期引用(升级到 L2) → B 注入 A 的早期引用并完成初始化成为成品(进入 L1) → A 注入 B 的成品并完成初始化成为成品(最终替换/升级到 L1)。 - 多角度追问 :
- A 最终持有的是 B 的什么状态?B 呢? → A 持有成品的 B,B 持有 A 的早期引用(可能被代理)。
- 如果 A 在这个过程中需要 AOP,流程有何不同? → 在 B 获取 A 的早期引用时,获取到的就是 A 的代理对象。
addSingleton对各级缓存做了什么? → 将最终成品放入 L1,并将 beanName 从 L2 和 L3 中移除。
- 加分回答 :能写出
getSingleton(String beanName, boolean allowEarlyReference)的伪代码,并画出所有缓存的状态迁移表。
7. getEarlyBeanReference 在循环依赖中起什么作用?它什么时候被调用?
- 标准回答 :作用是提前创建 Bean 的代理对象 ,以确保在循环依赖场景下,依赖方拿到的就是正确的增强后的 Bean,而不是原始对象。它在一个 Bean 的其他 Bean "需要" 它的早期引用时被调用,具体路径是:
getBean(A)->populateBean(A)->getBean(B)->populateBean(B)->getSingleton(A, true)->singletonFactory.getObject()->getEarlyBeanReference。 - 多角度追问 :
- 谁实现了它? →
AbstractAutoProxyCreator等SmartInstantiationAwareBeanPostProcessor的实现类。 - 它返回的对象是什么? → 可能是原始对象,也可能是 JDK Proxy 或 CGLIB Proxy。
- 它和
postProcessAfterInitialization如何协调? →getEarlyBeanReference会记下标记,postProcessAfterInitialization检查标记后跳过二次代理。
- 谁实现了它? →
- 加分回答 :能解释
SmartInstantiationAwareBeanPostProcessor接口中getEarlyBeanReference与predictBeanType的配合,用于提前预测 Bean 类型。
9.4 棘手问题
8. 如果一个 Bean 需要 @Async 增强并存在循环依赖,会发生什么?为什么可能出现双重代理?
- 标准回答 :可能导致"双重代理"。在循环依赖场景中,一个 Bean 可能首先通过
getEarlyBeanReference被@Async的 Advisor 代理一次。然后,在初始化阶段,它可能再次被@Transactional或其他 Advisor 代理。如果处理不当,会出现代理套代理的情况。Spring 通过earlyProxyReferences来避免同一个Advisor的重复代理,但不同Advisor(如@Async和@Transactional)可能会叠加。更常见的问题是,由于代理的顺序和类型,可能导致最终的 Bean 是$Proxy(JDK proxy),在强转成具体类时抛出ClassCastException。 - 多角度追问 :
- 双重代理的具体表现是什么? → 调用栈中出现两层代理的拦截,或者类型转换错误。
- 如何解决 @Async 导致的 ClassCastException? → 改为接口注入,或设置
proxy-target-class=true。 - Spring 如何保障代理单一性? →
earlyProxyReferences机制,以及wrapIfNecessary的幂等性设计。
- 加分回答 :能结合
AbstractAutoProxyCreator.getEarlyBeanReference和earlyProxyReferences源码,清晰解释为什么同一个 AOP 增强不会重复创建代理。
9. 构造器注入为什么无法解决循环依赖?单例和原型在此场景下表现一致吗?
- 标准回答 :是的,表现一致,都无法解决。因为实例化必须在依赖解析之后。
ConstructorResolver.autowireConstructor会先调用resolveConstructorArguments去容器中getBean获取依赖,然后才用newInstance完成实例化。而暴露到缓存的addSingletonFactory在createBeanInstance之后,因此永远没有机会暴露。 - 多角度追问 :
- 在
autowireConstructor的过程中,尝试去getSingleton会发生什么? → 查询缓存全空,抛BeanCurrentlyInCreationException。 - 如果我用
@Lazy呢? → 那就不是解决,而是绕过了。@Lazy让注入的参数变成一个代理,不再需要立即getBean。 - Spring 官方推荐构造器注入,这岂不是矛盾? → 官方推荐用于"必须的依赖",这会导致更早暴露配置问题。循环依赖本身就是不推荐的设计,应优先重构。
- 在
- 加分回答 :能从 JVM 的
Constructor.newInstance的原子性角度解释,说明这是 Java 语言层面的限制。
10. 原型 Bean 循环依赖一定会抛 BeanCurrentlyInCreationException 吗?有没有例外?
- 标准回答 :不一定。在单线程的简单循环中(A依赖B,B依赖A),一定会抛出。但在复杂的、涉及多个 Bean 或多线程的场景下,可能会有例外。比如,如果循环链中有一个单例 Bean 作为"缓冲",或者请求在线程池中被异步处理,
prototypesCurrentlyInCreation这个 ThreadLocal 可能会失效或检测不到,从而导致实例无限创建,最终 OOM。 - 多角度追问 :
prototypesCurrentlyInCreation的实现细节? → 是一个ThreadLocal<NamedThreadLocal<Set<String>>>。- 在哪些情况下单例 Bean 会成为"缓冲"? → 一个单例 Bean 持有原型 A,原型 A 持有原型 B,原型 B 又依赖原型 A。
- 如何主动避免? → 架构上禁止原型 Bean 互相依赖。
- 加分回答:能通过一个具体的高并发案例,演示原型循环依赖如何绕开检测并最终导致 OOM。
9.5 高级应用与排查
11. prototypesCurrentlyInCreation 的作用是什么?与 singletonsCurrentlyInCreation 有何不同?
- 标准回答 :
prototypesCurrentlyInCreation:基于ThreadLocal的Set<String>,用于快速失败。当尝试重复创建同一个原型 Bean 时,立刻抛出异常,防止无限递归。singletonsCurrentlyInCreation:全局Set<String>,用于逻辑分流 。它的存在是getSingleton(beanName, true)继续查询二级、三级缓存的先决条件。只有当一个单例 Bean "正在创建中",它才有资格从二、三级缓存中获取早期引用。
- 多角度追问 :
- 为什么一个用 ThreadLocal,一个用全局 Set? → 原型 Bean 的创建是"线程私有"的活动,单例 Bean 的缓存是全局共享的状态。
singletonsCurrentlyInCreation在getSingleton中的判断位置? → 在一级缓存查询之后,二级缓存查询之前。- 如果把这个检查去掉会怎样? → 可能导致向正常的单例 Bean 错误地返回一个"半成品"引用。
- 加分回答 :能画出
getSingleton方法内部的完整流程图,并标注这两个 Set 的作用点。
12. @Lazy 是如何破解循环依赖的?它的原理和代价是什么?对单例和原型都有效吗?
- 标准回答 :原理是注入一个延迟代理 。
ContextAnnotationAutowireCandidateResolver在解析注入点时,如果发现@Lazy,就不再调用getBean,而是直接创建一个轻量级代理。这个代理在首次方法调用时,才会去容器中获取真正的 Bean。这"绕过了"依赖在构造时的解析死锁。对单例和原型都有效,但对原型的语义是每次方法调用都获取新实例。 - 多角度追问 :
- 代价? → 性能开销(间接调用)、类型问题(默认JDK Proxy)、代码语义复杂化。
@Lazy标注在类上和标注在注入点有何区别? → 标注在注入点更精准,只用于解决该点的循环依赖;标注在类上会影响该 Bean 所有地方的创建。- 它的代理和 AOP 的代理有关系吗? → 有,它复用了 Spring 的 AOP 基础设施(
ProxyFactory)。
- 加分回答 :能解读
buildLazyResolutionProxy的关键源码。
13. 循环依赖中,B 持有的 A 的早期引用会随着 A 完成初始化而自动更新吗?为什么?
- 标准回答 :不会"更新"引用的内存地址,但其行为最终会一致 。如果 A 不需要被代理,B 持有的就是 A 的原始对象引用,当 A 完成填充和初始化后,B 也就自然地能访问到 A 的完整状态。如果 A 需要被代理,B 持有的就是 A 的代理对象引用。A 的原始对象在内部会被代理对象委托,所以 B 通过代理对象调用的,最终也是完全就绪的 A。因此,
b.getA() == a(引用相等) 始终成立,无需额外更新。 - 多角度追问 :
- 如果是代理对象,B 持有代理,容器持有代理,A 的原始对象去哪了? → 原始对象通常被代理对象内部持有(targetSource)。
- 这有什么潜在风险? → 如果在 A 的
@PostConstruct之前,B 持有 A 的早期引用并调用了 A 的方法,而此时 A 的某些状态尚未初始化,可能会出错。 - 这和"自动更新"有什么区别? → 引用的对象没有变,变的是对象内部的状态。
- 加分回答:能结合 Java 的引用传递机制和动态代理的委托原理进行解释。
14. 三级缓存中的 Bean 何时从二级缓存升级到一级缓存?addSingleton 方法做了什么?
- 标准回答 :Bean 完成
initializeBean后,走完所有BeanPostProcessor,成为成品时,addSingleton(beanName, singletonObject)被调用。这个方法会:1. 将成品 Bean 放入一级缓存 (singletonObjects)。 2. 从三级缓存 (singletonFactories) 中移除。 3. 从二级缓存 (earlySingletonObjects) 中移除。 4. 将 beanName 加入registeredSingletons集合。至此,该 Bean 的早期引用"升级"完成。 - 多角度追问 :
- 如果 Bean 被提前暴露过,最终成品和早期引用是同一个对象吗? → 如果不需要代理,是同一个;如果需要代理,成品是代理,早期引用也是代理,所以对外界来说它们就是"同一个"。
addSingleton方法是同步的吗? → 是的,synchronized (this.singletonObjects)。- 为什么从二级和三级移除? → 清理废弃状态,防止内存泄漏,并保证后续获取直接从一级拿成品。
- 加分回答 :能解释
addSingleton的同步块设计对并发安全的意义。
9.6 故障排查与设计
15. 如何排查 BeanCurrentlyInCreationException?如何通过堆栈判断是构造器循环依赖还是原型循环依赖?
- 标准回答 :重点看异常堆栈。
- 构造器循环依赖 :堆栈会显示
ConstructorResolver.autowireConstructor->AbstractBeanFactory.getBean->AbstractBeanFactory.doGetBean->DefaultSingletonBeanRegistry.getSingleton。核心特征是请求的 Bean 是单例 ,并且卡在createBeanInstance之中。 - 原型循环依赖 :堆栈会显示
AbstractBeanFactory.doGetBean中的beforePrototypeCreation检查点。核心特征是请求的 Bean 是原型。
- 构造器循环依赖 :堆栈会显示
- 多角度追问 :
- 错误信息
Is there an unresolvable circular reference?每次都一样吗? → 是的,这是 Spring 专门为循环依赖设计的提示。 - 如果一个 Bean 既是单例又有原型循环依赖怎么办? → 排查具体是哪个 Bean 在创建时抛的错,位于调用链的哪个位置。
- 除了循环依赖,还有其他原因吗? → 极少,通常是这两个。
- 错误信息
- 加分回答 :能直接根据
BeanCurrentlyInCreationException和Requested bean is currently in creation信息,结合 Bean 的作用域,迅速定位到循环依赖的双方。
16. (系统设计题) 设计一个轻量级 IOC 容器,要求能检测和解决单例 Bean 的循环依赖,并能检测原型 Bean 的循环依赖并明确报错,在 AOP 增强场景下保证代理对象的唯一性。请给出核心 getBean 伪代码及缓存结构设计。
- 标准回答(伪代码):
java
public class MyIoCContainer {
// 缓存结构
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(); // L1: 成品
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(); // L2: 早期引用
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(); // L3: 工厂
private final Set<String> singletonsCurrentlyInCreation = ConcurrentHashMap.newKeySet();
private final ThreadLocal<Set<String>> prototypesCurrentlyInCreation = ThreadLocal.withInitial(HashSet::new);
public <T> T getBean(String beanName, Class<T> clazz) {
BeanDefinition bd = getBeanDefinition(beanName);
if (bd.isSingleton()) {
return (T) getSingletonBean(beanName, bd);
} else {
return (T) getPrototypeBean(beanName, bd);
}
}
private Object getSingletonBean(String beanName, BeanDefinition bd) {
// 1. 查一级缓存
Object singleton = this.singletonObjects.get(beanName);
if (singleton != null) return singleton;
// 2. 循环依赖检测
if (this.singletonsCurrentlyInCreation.contains(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
// 3. 开始创建
this.singletonsCurrentlyInCreation.add(beanName);
try {
// a. 实例化
Object rawInstance = createBeanInstance(bd);
// b. 提前暴露工厂 (L3) ------ 关键!
// 工厂内部调用 AOP 增强逻辑
addSingletonFactory(beanName, () -> applyAopIfNecessary(rawInstance, beanName));
// c. 属性填充
populateBean(rawInstance, bd);
// d. 初始化
Object finalInstance = initializeBean(rawInstance, beanName);
// e. 处理循环依赖下的代理唯一性问题
Object earlyRef = this.earlySingletonObjects.get(beanName);
if (earlyRef != null && finalInstance != rawInstance) {
finalInstance = earlyRef; // 保证全局使用代理
}
// f. 加入一级缓存,清理二三级
addSingleton(beanName, finalInstance);
return finalInstance;
} finally {
this.singletonsCurrentlyInCreation.remove(beanName);
}
}
// 在 populateBean 中注入依赖时,会再次调用 getBean
private Object resolveDependency(String depBeanName) {
Object depBean = getSingleton(depBeanName, true); // allowEarlyReference = true
if (depBean != null) return depBean;
// 如果未找到,进行常规创建
return getBean(depBeanName, Object.class);
}
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object o = this.singletonObjects.get(beanName);
if (o != null) return o;
// 只有正在创建中的单例 Bean 才允许查早期引用
if (!this.singletonsCurrentlyInCreation.contains(beanName)) return null;
// 查二级
o = this.earlySingletonObjects.get(beanName);
if (o != null) return o;
if (allowEarlyReference) {
// 查三级,获取工厂,生成早期引用,升级到二级
ObjectFactory<?> factory = this.singletonFactories.get(beanName);
if (factory != null) {
o = factory.getObject();
this.earlySingletonObjects.put(beanName, o);
this.singletonFactories.remove(beanName);
}
}
return o;
}
private Object getPrototypeBean(String beanName, BeanDefinition bd) {
// 原型循环依赖检测
if (this.prototypesCurrentlyInCreation.get().contains(beanName)) {
throw new BeanCurrentlyInCreationException("Prototype circular dependency: " + beanName);
}
this.prototypesCurrentlyInCreation.get().add(beanName);
try {
Object instance = createBeanInstance(bd);
// 【注意】原型 Bean 不暴露到任何缓存!
populateBean(instance, bd); // 此处若触发 getBean 调用原自己,会命中上面的检测,从而快速失败
return initializeBean(instance, beanName);
} finally {
this.prototypesCurrentlyInCreation.get().remove(beanName);
}
}
// ... 其他
}
- 多角度追问 :
- 为什么 AOP 代理创建要放在
ObjectFactory内部? → 为了保证"谁需要谁触发",并且只创建一次。 - 你的设计如何避免代理冲突? → 通过
earlyRef检查,确保最终进入一级缓存的是代理对象。 - 对于原型的检测,ThreadLocal 足够吗? → 对于单个线程内的原型重入是足够的,但对于跨线程的间接循环,无法完全预防,这是原型本身的特性决定的。
- 为什么 AOP 代理创建要放在
- 加分回答 :能讨论到此设计的"骨架"就是
DefaultSingletonBeanRegistry的精简版,并指出原型 Bean 的检测实际上是一种"尽力而为"的机制。
循环依赖解决机制速查表
| 作用域 (Scope) | 注入方式 (Injection Type) | 是否可解决 | 解决/失败机制 | 关键源码/缓存 | 典型事故 |
|---|---|---|---|---|---|
| Singleton | Setter/Field | ✅ 可解决 | 实例化后通过 addSingletonFactory 将 ObjectFactory 暴露到三级缓存,发生循环依赖时通过 getEarlyBeanReference 获取早期引用。 |
doCreateBean, addSingletonFactory, getSingleton, L1/L2/L3 |
1. @PostConstruct 中访问对方方法导致 NPE。 2. 组合 AOP 增强时出现代理类型转换异常。 |
| Singleton | Constructor | ❌ 不可解决 | ConstructorResolver.autowireConstructor 必须在构造前解析参数,此时 addSingletonFactory 尚未执行,形成死锁。 |
autowireConstructor, createBeanInstance |
应用启动立即失败,抛出 BeanCurrentlyInCreationException。 |
| Prototype | Setter/Field | ❌ 不可解决 | 容器不持有原型 Bean 引用,不调用 addSingletonFactory。直接循环依赖会被 prototypesCurrentlyInCreation 检测到并抛异常。复杂循环可能绕开检测,导致无限创建。 |
doGetBean, prototypesCurrentlyInCreation |
原型 Bean 循环依赖导致 OOM。每次请求创建新实例,快速耗尽堆内存。 |
| Prototype | Constructor | ❌ 不可解决 | 原因与单例构造器注入类似,叠加了原型 Bean 不暴露缓存的特性,双重无解。 | autowireConstructor, prototypesCurrentlyInCreation |
与单例构造器一样,启动或首次调用时失败。 |
延伸阅读
- 《Spring 揭秘》王福强 :第 9 章"Spring 容器的扩展点"对
BeanFactoryPostProcessor和BeanPostProcessor的剖析,是理解 AOP 代理与循环依赖交互的前置知识。 - Spring Framework 官方源码
DefaultSingletonBeanRegistry:Spring 中最精妙的类之一,本文的所有缓存分析都围绕其展开。阅读其源码注释是理解设计者意图的最佳途径。 - 《Expert One-on-One J2EE Development without EJB》 Rod Johnson:Spring 的设计哲学之源,能帮助你理解为何会有"三级缓存"而非其他方案。
- Spring 官方文档《Core Technologies》 :关于 Bean 的作用域、
@Lazy、AOP 代理的章节,是权威的理论参考。 - 《Effective Java》Joshua Bloch:关于"构建器(Builder)"和"依赖注入优于硬编码资源"的条目,有助于从设计模式角度理解为何要避免构造器循环依赖。