Spring 循环依赖与三级缓存 ⭐⭐⭐⭐⭐
面试频率:★★★★★
工作频率:★★★☆☆
⚡30 秒速记(复习只看这里)
🟢 一句话
Spring 通过三级缓存 + 提前暴露 Bean 引用,解决部分单例 Bean 的循环依赖问题。
例如:
A依赖B
B又依赖A
核心思想:
A还没完全创建完成
↓
先提供获取A提前引用的入口
↓
创建B时需要A
↓
提前拿到A
↓
B创建完成
↓
再注入A
↓
A创建完成
🟡 三级缓存
一级缓存 singletonObjects
= 创建完成的单例Bean
二级缓存 earlySingletonObjects
= 已经获取到的提前引用
三级缓存 singletonFactories
= 获取提前引用的ObjectFactory
速记:
一级:成品
二级:提前引用
三级:获取提前引用的工厂
🔴 最重要的流程
创建A
↓
实例化A
↓
三级缓存放入A的ObjectFactory
↓
A需要B
↓
创建B
↓
B需要A
↓
三级缓存获取A的提前引用
↓
A提前引用进入二级缓存
↓
A注入B
↓
B创建完成
↓
B注入A
↓
A创建完成
↓
进入一级缓存
📚 一、什么是循环依赖?
例如:
@Service
public class AService {
@Autowired
private BService bService;
}
B:
@Service
public class BService {
@Autowired
private AService aService;
}
关系:
A
↓
需要B
↓
B
↓
又需要A
这就是:
循环依赖。
二、为什么会出现死循环?
如果没有特殊处理:
创建A:
创建A
↓
A需要B
↓
创建B
↓
B需要A
↓
创建A
↓
A需要B
↓
创建B
↓
……
永远创建不完。
所以 Spring 必须想办法:
打破 A 等 B、B 又等 A 的循环。
三、Bean 创建不是一步完成的 ⭐⭐⭐⭐⭐
很多人容易认为:
new AService();
就代表 Bean 创建完成。
实际上 Spring 创建 Bean 大致经历:
实例化
↓
属性填充
↓
初始化
↓
Bean创建完成
例如:
AService a = new AService();
这时候:
A对象已经存在。
但是:
a.bService = null
因为:
B还没有注入。
所以:
对象已经实例化
≠
Bean已经完整创建
这就是 Spring 能解决循环依赖的关键。
四、什么叫提前暴露? ⭐⭐⭐⭐⭐
假设 Spring 正在创建 A。
先:
AService a = new AService();
此时:
A对象已经存在
但是
A还没有注入B
正常情况应该:
A全部创建完成
↓
再给别人使用
但是为了处理循环依赖:
Spring 会提前准备一个:
获取A提前引用的入口
如果后面创建 B 时:
B急着需要A
就可以:
提前拿到A
这就是:
提前暴露。
可以理解成:
A:
房子已经盖出来了
但是还没装修完成
↓
B现在急着要A
↓
先把钥匙给B
所以:
提前暴露并不是:
A已经完全创建完成
而是:
A已经实例化
但还没有完全初始化
五、三级缓存是什么? ⭐⭐⭐⭐⭐
Spring 单例 Bean 有三个重要缓存:
1. 一级缓存 singletonObjects
保存:
已经完全创建完成的单例Bean
例如:
singletonObjects
A → 完整A
B → 完整B
平时:
applicationContext.getBean("aService");
正常获取 Bean:
主要就是从这里拿。
2. 二级缓存 earlySingletonObjects
保存:
已经获取出来的提前引用
也就是:
Bean 还没有完全创建完成。
但是:
已经有人因为循环依赖需要它了。
例如:
earlySingletonObjects
A → A的提前引用
3. 三级缓存 singletonFactories
保存:
ObjectFactory
注意:
三级缓存里不是直接保存:
半成品A
而是保存:
获取 A 提前引用的工厂。
可以粗略理解:
singletonFactories
A → ObjectFactory
需要 A 时:
ObjectFactory
↓
获取A的提前引用
六、三级缓存怎么解决 A → B → A? ⭐⭐⭐⭐⭐
假设:
A依赖B
B依赖A
第一步:创建A
Spring:
AService a = new AService();
现在:
A已经实例化
但是:
A.bService = null
第二步:把A的ObjectFactory放入三级缓存
三级缓存:
A → ObjectFactory
意思:
如果后面有人提前需要 A,可以通过这个 ObjectFactory 获取 A 的提前引用。
第三步:给A注入B
Spring发现:
A需要B
但是:
B还不存在。
于是:
创建B
第四步:创建B
BService b = new BService();
然后:
Spring发现:
B需要A
问题来了:
A还没有创建完成。
怎么办?
开始查缓存。
第五步:查一级缓存
singletonObjects
有没有A?
没有 ❌
因为:
A还没创建完成。
第六步:查二级缓存
earlySingletonObjects
有没有A?
没有 ❌
因为:
A的提前引用还没有真正获取过。
第七步:查三级缓存
singletonFactories
发现:
A → ObjectFactory
有。
于是:
ObjectFactory
↓
获取A的提前引用
然后:
把这个提前引用放到:
二级缓存
变成:
earlySingletonObjects
A → A的提前引用
第八步:把A注入B
现在:
B终于拿到了A:
B.aService = A
所以:
B可以继续创建。
最终:
B创建完成
进入:
一级缓存
第九步:回去继续创建A
现在:
B已经创建完成。
所以:
A.bService = B
A也可以继续:
初始化
↓
创建完成
最终:
A进入:
一级缓存
七、完整流程图 ⭐⭐⭐⭐⭐
开始创建A
↓
new A()
↓
A已经实例化
↓
三级缓存放入A的ObjectFactory
↓
给A注入属性
↓
发现A需要B
↓
开始创建B
↓
new B()
↓
给B注入属性
↓
发现B需要A
↓
查一级缓存
↓
没有A
↓
查二级缓存
↓
没有A
↓
查三级缓存
↓
找到A的ObjectFactory
↓
获取A的提前引用
↓
放入二级缓存
↓
把A注入B
↓
B创建完成
↓
B进入一级缓存
↓
回到A
↓
把B注入A
↓
A创建完成
↓
A进入一级缓存
最终:
A → B
B → A
循环依赖解决。
八、为什么需要三级缓存?二级不够吗? ⭐⭐⭐⭐⭐
这是面试非常喜欢追问的。
如果没有 AOP:
理论上:
A实例化
↓
直接把A放二级缓存
也可以解决简单循环依赖。
但是:
Spring 还需要考虑:
AOP代理
例如:
@Transactional
public void save() {
}
A最终可能不是:
原始A
而是:
A的代理对象
↓
原始A
如果直接把原始A放到二级缓存:
可能出现:
B拿到:
原始A
其他地方拿到:
A的代理对象
这样:
可能出现 Bean 引用不一致。
所以 Spring 使用三级缓存:
A → ObjectFactory
真正有人提前需要 A 时:
ObjectFactory
↓
获取A的提前引用
↓
必要时处理AOP代理
↓
返回正确引用
所以:
三级缓存的重要作用之一,是在提前暴露 Bean 时,为 AOP 代理等处理保留机会。
九、三级缓存怎么记?
不要死背名字。
先记:
一级缓存
↓
成品仓库
二级缓存
↓
提前引用仓库
三级缓存
↓
获取提前引用的工厂
对应:
一级:
singletonObjects
二级:
earlySingletonObjects
三级:
singletonFactories
十、为什么构造器循环依赖解决不了? ⭐⭐⭐⭐⭐
例如:
public AService(BService bService) {
this.bService = bService;
}
B:
public BService(AService aService) {
this.aService = aService;
}
创建A:
new A(???)
↓
必须先有B
所以:
去创建B。
但是创建B:
new B(???)
↓
必须先有A
问题:
A还没有new出来
因此:
根本没有:
A的引用
可以提前暴露。
流程:
创建A
↓
需要B才能new A
↓
创建B
↓
需要A才能new B
↓
但是A还没创建
↓
死循环
所以:
构造器循环依赖通常无法通过三级缓存的提前暴露机制解决。
十一、为什么字段注入可以?
字段注入:
@Autowired
private BService bService;
可以:
先:
AService a = new AService();
再:
a.bService = b;
所以:
先实例化A
↓
A对象已经存在
↓
提前暴露A
↓
再给A注入B
有提前暴露的机会。
而构造器:
new A(b)
必须:
先有B。
所以:
没有提前暴露A的机会。
十二、Spring Boot 中要注意
即使理解了三级缓存:
也不要把循环依赖当成正常设计方式。
例如:
OrderService
↓
UserService
↓
OrderService
通常说明:
两个 Service:
职责可能耦合过重。
更好的方式可能是:
拆分公共逻辑
↓
新的Service
或者:
重新调整依赖关系。
🎤 面试回答(30秒)
Spring 主要通过三级缓存和提前暴露 Bean 引用来解决部分单例 Bean 的循环依赖。
Bean 创建分为实例化、属性填充和初始化等阶段。当 A 依赖 B、B 又依赖 A 时,Spring 在 A 实例化之后,会把获取 A 提前引用的 ObjectFactory 放入三级缓存。
创建 B 时如果需要 A,可以通过三级缓存获取 A 的提前引用,并放入二级缓存,然后完成 B 的创建,再把 B 注入 A,最终 A 和 B 创建完成后进入一级缓存。
三级缓存还为 AOP 代理等场景保留了处理提前引用的机会。
🎯 面试追问
Q1:什么是循环依赖?
答:
A依赖B
B又依赖A
Q2:Spring 怎么解决循环依赖?
答:
三级缓存
+
提前暴露Bean引用
Q3:什么叫提前暴露?
答:
Bean 已经实例化,但是还没有完全初始化时,提前提供它的引用给其他 Bean 使用。
Q4:三级缓存分别是什么?
答:
一级:
singletonObjects
完整Bean
二级:
earlySingletonObjects
提前引用
三级:
singletonFactories
获取提前引用的ObjectFactory
Q5:三级缓存里直接放半成品Bean吗?
答:
不是。
三级缓存保存的是:
ObjectFactory
通过它:
获取 Bean 的提前引用。
Q6:为什么需要三级缓存?
答:
为了在提前获取 Bean 引用时保留处理机会,例如 AOP 代理。
Q7:为什么构造器循环依赖解决不了?
答:
因为构造器必须先拿到依赖对象才能完成实例化,此时 Bean 自己都还没有 new 出来,没有引用可以提前暴露。
Q8:为什么字段注入可以解决部分循环依赖?
答:
因为可以先实例化 Bean,再进行属性注入,因此实例化后的 Bean 可以提前暴露。
⚠️ 工作踩坑
1. 主动设计循环依赖
虽然 Spring 在部分场景可以处理:
A → B → A
但是:
不代表这是好的设计。
循环依赖通常意味着:
Bean之间耦合过高
应该考虑重新拆分职责。
2. 认为所有循环依赖 Spring 都能解决
错误。
例如:
构造器循环依赖
通常无法通过提前暴露机制解决。
3. 认为三级缓存保存的是三个版本的Bean
错误。
三级缓存:
不是三个 Bean。
而是不同创建阶段使用的缓存:
一级:
完整Bean
二级:
提前引用
三级:
ObjectFactory
❓ 自测
-
什么是循环依赖?
-
Spring 为什么可以解决部分循环依赖?
-
什么叫提前暴露?
-
Bean 实例化完成是不是代表 Bean 已经创建完成?
-
Spring 三级缓存分别是什么?
-
一级缓存放什么?
-
二级缓存放什么?
-
三级缓存放什么?
-
为什么三级缓存不是直接放半成品 Bean?
-
A → B → A 的完整创建流程是什么?
-
为什么字段注入可以解决部分循环依赖?
-
为什么构造器循环依赖通常解决不了?
-
三级缓存和 AOP 有什么关系?