Spring 用三级缓存解决 setter/field 注入的循环依赖。本文从"为什么需要三级"出发,逐步拆解每一级的职责、对象创建的完整时序,以及 AOP 代理场景下的特殊处理。面试高频题。
缩写速查:Spring 术语与缩写速查 · 相关笔记:Spring 容器启动流程源码分析 · SpringBoot 启动流程源码分析
目录
| 章节 | 说明 |
|---|---|
| 什么是循环依赖 | 问题定义与分类 |
| 三级缓存结构 | 三个 Map 的职责 |
| 解决流程详解 | A↔B 循环依赖的完整时序 |
| 为什么需要三级而不是两级 | 核心问题:AOP 代理的时机 |
| 无法解决的场景 | 构造器注入、prototype、@Async |
| 面试常见追问 | 高频变体问题 |
什么是循环依赖
@Component
class A {
@Autowired B b; // A 依赖 B
}
@Component
class B {
@Autowired A a; // B 依赖 A
}
Spring 创建 A 时需要注入 B,创建 B 时又需要注入 A,形成互相等待的死锁。
循环依赖的分类
| 类型 | 示例 | Spring 能否解决 | 原因 |
|---|---|---|---|
| setter / field 注入(单例) | @Autowired 字段注入 |
✅ 三级缓存解决 | 先实例化再注入,实例化后即可将 ObjectFactory 放入三级缓存,打破死锁 |
| 构造器注入 | @Autowired 构造器参数 |
❌ 无法解决 | 实例化时就需要依赖,构造器未执行完则无法放入缓存,死锁无法打破 |
| prototype 作用域 | @Scope("prototype") |
❌ 无法解决 | Spring 不缓存 prototype Bean,三级缓存依赖缓存机制,对 prototype 完全失效 |
@Async + 循环依赖 |
异步代理与循环依赖同时存在 | ❌ 抛出异常 | @Async 代理在 postProcessAfterInitialization 生成,晚于三级缓存暴露早期引用的时机,导致早期引用与最终代理不一致 |
三级缓存结构
Spring 在 DefaultSingletonBeanRegistry 中维护三个 Map:
// 一级缓存:完整的、可直接使用的 Bean
Map<String, Object> singletonObjects;
// 二级缓存:早期暴露的 Bean(已实例化,但属性注入/初始化未完成)
Map<String, Object> earlySingletonObjects;
// 三级缓存:Bean 工厂,调用时生成早期引用(可触发 AOP 代理)
Map<String, ObjectFactory<?>> singletonFactories;
| 缓存 | Key | Value | 何时写入 | 何时移除 |
|---|---|---|---|---|
三级 singletonFactories |
beanName | ObjectFactory<?> |
createBeanInstance 后,属性注入前 |
三级引用被取走时 |
二级 earlySingletonObjects |
beanName | 早期 Bean 引用(可能是代理) | 从三级取出时自动写入 | Bean 完全初始化后 |
一级 singletonObjects |
beanName | 完整 Bean | initializeBean 全部完成后 |
容器关闭时 |
解决流程详解
以 A 依赖 B,B 依赖 A 为例,完整时序与每步后的缓存状态:
| 步骤 | 主要操作 | 三级 singletonFactories | 二级 earlySingletonObjects | 一级 singletonObjects |
|---|---|---|---|---|
| ① 实例化 A | createBeanInstance(A) → addSingletonFactory("A", factory) |
A: factory | — | — |
| ② 注入 A→B,实例化 B | createBeanInstance(B) → addSingletonFactory("B", factory) |
A: factory B: factory |
— | — |
| ③ 注入 B→A,命中三级 | factory.getObject() 生成 earlyA(若需 AOP 则生成代理)earlyA 写入二级,A 从三级移除 |
~~A 已移除~~ B: factory |
A: earlyA | — |
| ④ B 初始化完成 | initializeBean(B) 完成B 写入一级,B 从三级移除 |
~~B 已移除~~ | A: earlyA | B: 完整 Bean |
| ⑤ A 初始化完成 | initializeBean(A) 完成A 写入一级,A 从二级移除 |
— | ~~A 已移除~~ | A: 完整 Bean B: 完整 Bean |
为什么需要三级而不是两级
如果去掉三级缓存,只用两级
假设实例化后直接把原始 Bean 放入二级缓存,时间线如下:
| 时刻 | 操作 | 一级缓存 | 二级缓存 | B 持有的 A |
|---|---|---|---|---|
| T1 | A 实例化(new A()) |
— | — | — |
| T2 | 原始 A 放入二级缓存 | — | 原始 A | — |
| T3 | 创建 B,B 需要注入 A | — | 原始 A | — |
| T4 | 从二级缓存取 A 注入给 B | — | 原始 A | 原始 A |
| T5 | B 创建完成 | — | 原始 A | 原始 A |
| T6 | A 初始化完成,AOP 生成代理 A | 代理 A | — | 原始 A |
| 最终 | 容器就绪 | 代理 A | — | 原始 A ≠ 代理 A ❌ |
B 持有原始 A,而容器中流通的是代理 A,两者不是同一对象,B 调用 a.method() 不走代理,事务/AOP 全部失效。
为什么不能事后把 B 持有的引用更新为代理 A?
虽然 Spring 知道哪些 Bean 依赖了 A(BeanDefinition 图完整),可以反射更新 b.a 字段,但有一个根本问题无法解决:
@Service
class B {
@Autowired
private A a;
@PostConstruct
public void init() {
this.localRef = a; // 初始化时把引用缓存到本地变量
}
private A localRef;
public void doSomething() {
localRef.method(); // 用的是 localRef,不是字段 a
}
}
Spring 只能更新 b.a 字段,但 localRef 已经是原始 A 的副本,追踪不到,更新不了。事后修补永远存在遗漏风险,不如从源头保证正确。
三级缓存的解法
三级缓存存的是 ObjectFactory<?> 而不是对象本身,在 B 取 A 的那一刻就提前生成代理:
// Spring 源码:addSingletonFactory 时注册的工厂
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
// getEarlyBeanReference → 触发 AbstractAutoProxyCreator
// → 如果 A 需要 AOP,此时提前生成代理 A 返回给 B
时间线对比:
| 时刻 | 操作 | 一级缓存 | 二级缓存 | 三级缓存 | B 持有的 A |
|---|---|---|---|---|---|
| T1 | A 实例化 | — | — | — | — |
| T2 | ObjectFactory 放入三级缓存 | — | — | () → 代理A |
— |
| T3 | 创建 B,B 需要注入 A | — | — | () → 代理A |
— |
| T4 | 调用工厂,提前生成代理 A,移入二级缓存 | — | 代理 A | — | — |
| T5 | 代理 A 注入给 B | — | 代理 A | — | 代理 A |
| T6 | B 创建完成 | — | 代理 A | — | 代理 A |
| T7 | A 初始化完成,二级缓存代理 A 晋升一级 | 代理 A | — | — | 代理 A |
| 最终 | 容器就绪 | 代理 A | — | — | 代理 A = 一级缓存 ✅ |
两种方案关键差异对比:
| T4 时 B 拿到的 A | 最终一级缓存的 A | 是否一致 | |
|---|---|---|---|
| 两级缓存 | 原始 A | 代理 A | ❌ 不一致,AOP 失效 |
| 三级缓存 | 代理 A | 代理 A | ✅ 一致 |
一句话总结
二级缓存能解决循环依赖,但无法保证 AOP 代理的唯一性。三级缓存通过
ObjectFactory在 B 取 A 的那一刻就提前生成代理,从源头保证全程只有一个版本流通,而非事后修补。
无法解决的场景
1. 构造器注入
@Component
class A {
A(B b) { } // 构造器注入
}
实例化时就需要 B,此时 A 还没有任何引用可以放入缓存,死锁无法打破。
解法:@Lazy 让其中一个延迟注入(注入代理,首次调用时才真正初始化):
@Component
class A {
A(@Lazy B b) { }
}
2. prototype 作用域
Spring 不缓存 prototype Bean(每次 getBean 创建新实例),三级缓存机制不适用。
3. @Async + 循环依赖
@Async 方法由 AsyncAnnotationBeanPostProcessor 在 postProcessAfterInitialization 阶段生成代理,晚于三级缓存暴露早期引用的时机,导致早期暴露的引用与最终代理不一致,Spring 检测到后直接抛出:
BeanCurrentlyInCreationException:
Bean with name 'xxx' has been injected into other beans in its raw version
as part of a circular reference, but has eventually been wrapped.
解法:重构设计消除循环依赖,或将 @Async 方法提取到独立的 Bean。
面试常见追问
Q:Spring 三级缓存中,二级缓存的作用是什么?
性能优化。如果多个 Bean 同时依赖 A,第一次从三级缓存取出后放入二级缓存,后续直接从二级缓存取,不需要重复调用
ObjectFactory.getObject()(避免重复创建代理)。
Q:为什么构造器注入不能解决循环依赖?
实例化 Bean 必须先调用构造器,调用构造器前无法将任何引用放入缓存。三级缓存的机制依赖"先实例化、再注入"的顺序,构造器注入打破了这个顺序。
Q:Spring Boot 默认允许循环依赖吗?
Spring Boot 2.6+ 默认禁止循环依赖,启动时报
BeanCurrentlyInCreationException。可通过spring.main.allow-circular-references=true开启,但官方不推荐——循环依赖通常意味着设计问题。
Q:@Lazy 是如何解决构造器循环依赖的?
@Lazy让 Spring 注入一个 CGLIB 代理对象(而非真实 Bean),代理对象在首次方法调用时才触发真正的 Bean 初始化。这样构造器能顺利完成,打破了死锁。
Q:循环依赖一定是坏的设计吗?
是的。循环依赖说明两个类互相耦合,职责边界模糊。解法通常是:提取公共依赖到第三个类、使用事件解耦、重新划分职责。
参考资料
评论 (0)
发表评论