Spring 三级缓存和循环依赖介绍
Spring 中的 三级缓存机制 是为了解决 构造函数注入(不推荐)和 setter 注入(常见)方式下的单例 Bean 的循环依赖问题。
💡 一、什么是循环依赖
循环依赖(Circular Dependency):两个或多个 Bean 相互依赖。
举例:
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
Spring 在创建 A
时需要先创建 B
,但 B
又依赖 A
,陷入循环。
🧠 二、Spring 如何解决循环依赖?
Spring 为了解决「构造函数注入之外的循环依赖」,引入了 三级缓存机制 来提前暴露对象引用:
🧱 三、三级缓存结构(DefaultSingletonBeanRegistry)
Spring 用以下三个 Map 来管理单例 Bean 的生命周期:
缓存名称 | 变量名 | 描述 |
---|---|---|
一级缓存(成品缓存) | singletonObjects |
存放已完全初始化的单例 Bean |
二级缓存(半成品缓存) | earlySingletonObjects |
存放早期曝光的半初始化 Bean(尚未完成依赖注入) |
三级缓存(工厂缓存) | singletonFactories |
存放创建早期 Bean 引用的 ObjectFactory (避免循环引用) |
🔁 四、创建 Bean 时的生命周期流程(含三级缓存)
以 A → B → A
的循环依赖为例,流程如下:
1. 创建 Bean A(A 依赖 B):
-
singletonObjects
查不到 A -
执行
createBean("a")
- 实例化对象
new A()
,但还没依赖注入 - 把
A
的ObjectFactory
放入singletonFactories
(三级缓存) - 下一步准备注入依赖,发现需要 B
- 实例化对象
2. 创建 Bean B(B 依赖 A):
-
singletonObjects
查不到 B -
执行
createBean("b")
- 实例化对象
new B()
,把 B 的ObjectFactory
放入三级缓存 - 注入依赖时发现需要 A
- 实例化对象
3. B 注入 A:
-
singletonObjects
查不到 A -
查找
earlySingletonObjects
,仍没有 -
查找
singletonFactories
(三级缓存):- 拿到 A 的工厂,创建早期引用并放入
earlySingletonObjects
(二级缓存)
- 拿到 A 的工厂,创建早期引用并放入
-
注入 A 成功,继续完成 B 的初始化
-
B 初始化完成后,放入
singletonObjects
,清除缓存中旧的
4. 回到 A:
- B 已注入完成,A 的依赖完成
- 初始化 A,放入
singletonObjects
❌ 五、为什么构造函数注入会失败?
构造函数注入会在实例化时就需要完整依赖,无法提前暴露引用,因此无法利用三级缓存机制。
@Component
public class A {
private final B b;
@Autowired
public A(B b) { this.b = b; } // 在实例化时就需要 b
}
在构造函数注入场景中,Spring 无法将 A
暴露给 B
,因而循环依赖无法解决。
🧩 六、三级缓存简化图示
singletonObjects ← 一级缓存:完全初始化的Bean
↑
earlySingletonObjects ← 二级缓存:半初始化Bean引用
↑
singletonFactories ← 三级缓存:创建Bean引用的工厂(lambda)
✅ 七、小结
项目 | 是否解决循环依赖 |
---|---|
setter注入 | ✅ 可以解决(通过三级缓存) |
构造器注入 | ❌ 不支持 |
@Scope("prototype") 原型模式 | ❌ 不支持(因为原型不走单例容器) |
📚 八、特殊情况
如果一个是构造器注入,一个是setter注入,会怎么样?
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
private A a;
@Autowired
public void setA(A a) {
this.a = a;
}
}
答案是:会报错
┌─────┐
| a (field public com.example.demo.B com.example.demo.A.b)
↑ ↓
| b defined in file class
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
无论是属性循环依赖,还是构造器循环依赖,Spring都会进行检测,并且在检测到循环依赖时,会抛出异常UnsatisfiedDependencyException异常。并给出提示信息。设置spring.main.allow-circular-references=true可以允许循环依赖。
允许循环依赖
我们让A是构造器注入,B是setter注入,然后设置spring.main.allow-circular-references=true,再运行
@Component
public class A {
private final B b; // 构造器注入
@Autowired
public A(B b) {
this.b = b;
} // 在实例化时就需要 b
}
@Component
public class B {
private A a;
@Autowired
public void setA(A a) {
this.a = a;
}
}
结果,还是报错。提示我们无法打破循环依赖。
```Java
┌─────┐
| a defined in file
↑ ↓
| b (field public com.example.demo.A com.example.demo.B.a)
└─────┘
Action:
Despite circular references being allowed, the dependency cycle between beans could not be broken. Update your application to remove the dependency cycle.
🔍 为什么构造器注入+setter注入仍然失败?
A
使用 构造器注入B
B
使用 setter注入A
- 开启了
allow-circular-references=true
关键点是:
Spring 的三级缓存机制只能在 “Bean 已经创建(
new
了对象),但还没完成依赖注入” 这个阶段,提前暴露引用给别人用。
但是——
❌ 构造器注入的对象还没办法放入三级缓存
Spring 不能提前暴露 A
的引用(因为还没创建出来),而创建 A
需要 B
,此时 Spring 会去创建 B
,但 B
又依赖 A
,而 A
还没法暴露出来,于是死循环发生了。
🔁 再还原一遍整个流程
- Spring 创建
A
,它构造器需要B
- 去创建
B
B
是 setter 注入A
- 于是先去获取
A
的引用 - 发现
A
还没有被创建(构造器注入阶段,不能提前暴露) - ❌ 报错:依赖无法解析,循环引用未被打破
反过来
如果A是setter注入,B是构造器注入,会怎么样?
不会报错,可以解决循环依赖问题。
- Spring 开始创建
A
(A
是无参构造 + setter 注入B
) A
实例通过无参构造器创建出来了 ✅- Spring 立即将
A
的 ObjectFactory 放入 三级缓存singletonFactories
- 准备注入依赖:A 需要注入 B → 进入创建
B
- 创建
B
时,发现其 构造器参数需要A
- Spring 获取
A
的引用 → 进入getSingleton("a")
- 一级缓存没有 → 二级缓存没有 → 从三级缓存中获取 A 的 ObjectFactory,并调用之
- 得到早期 A 的引用,放入二级缓存
earlySingletonObjects
- 使用这个 A 实例成功构造
B
✅ - B 初始化完成,放入一级缓存
- 回到 A 的注入流程,将完整的 B 注入到 A 中
- A 也完成初始化,放入一级缓存 ✅
🧾 总结
Spring 为了解决 单例 Bean 的循环依赖问题,设计了 三级缓存机制,分别是:
singletonObjects
:一级缓存,已完全初始化的 Bean;earlySingletonObjects
:二级缓存,正在初始化过程中的早期引用;singletonFactories
:三级缓存,用于生成早期引用的工厂(lambda 形式)。
只有当 Bean 是通过无参构造器创建(即不是构造器注入),并且注入方式为 setter 或字段注入 时,Spring 才能在创建阶段将其 ObjectFactory 放入三级缓存,从而打破依赖环路。
✅ 可解决的循环依赖:
- 两边都是 setter / 字段注入 ✅
- 一方构造器注入,另一方 setter 注入,并且「构造器依赖方是后被创建的一方」✅
❌ 无法解决的循环依赖:
- 构造器注入 → 构造器注入 ❌
- 构造器注入在前,setter 注入在后 ❌
Spring 能解决一定范围内的循环依赖问题,但并非鼓励使用循环依赖。构造器注入无法通过缓存机制绕过依赖链,是一种明确的设计信号:“该依赖关系设计可能存在耦合过深的问题”。
因此,在日常开发中,应尽量避免循环依赖,或通过以下方式解耦:
- 拆分 Bean,提取中间服务;
- 使用懒加载
@Lazy
延迟注入;