Spring 三级缓存(循环依赖)
Spring 之所以设计三级缓存,并不是单纯为了解决循环依赖,而是为了在不破坏 Bean 正常生命周期的前提下,按需、延迟地处理循环依赖问题。
☘ Spring 循环依赖
网上有很多关于三级缓存的面试,比如 Spring 怎么解决循环依赖的?然后会回答说三级缓存,紧接着就会进一步提问,为什么一定要三级缓存?二级缓存行不行?这篇文章所解释和网上差不多。
🤔 什么是循环依赖?
举个栗子 🌰
/**
* A 类,引入 B 类的属性 b
*/
public class A {
private B b;
}
/**
* B 类,引入 A 类的属性 a
*/
public class B {
private A a;
}
再看个简单的图:

像这样,创建 a 的时候需要依赖 b,那就创建 b,结果创建 b 的时候又需要依赖 a,那就创建 a,创建 a 的时候需要依赖 b,那就创建 b,结果创建 b 的时候又需要依赖 a ......
🙂 互相依赖何时了,死循环了吧? 👉 诺,这就是循环依赖!
循环依赖其实不算个问题或者错误,我们实际在开发的时候,也可能会用到。 再拿最开始的 A 和 B 来说,我们手动使用的时候会用以下方式:
A a = new A();
B b = new B();
b.setA(a);
a.setB(b);
其实这样就解决了循环依赖,功能上是没有问题的,但是为什么 Spring 要解决循环依赖?
🤔 为什么 Spring 要解决循环依赖?
首先简单了解下,我们用 Spring 框架,它帮我们做了什么事情?总结性来说,六字真言:IoC 和 AOP。
由于 Spring 解决循环依赖是考虑到 IoC 和 AOP 相关知识了,所以这里我先提一下。
由于本文主要的核心是 Spring 的循环依赖处理,所以不会对 IoC 和 AOP 做详细的说明,想了解以后有机会再说 😶
IoC,主要是将对象的创建、管理都交给了 Spring 来管理,能够解决对象之间的耦合问题,对开发人员来说也是省时省力的。
AOP,主要是在不改变原有业务逻辑情况下,增强横切逻辑代码,也是解耦合,避免横切逻辑代码重复;也是对 OOP 的延续、补充。
既然类的实例化都交给了 Spring 来管理了,那么循环依赖 Spring 肯定也要考虑到怎么去处理(怎么总觉得有点像是废话 😶)。
🤯 解决循环依赖的方式
参考我们能想到的肯定是手动处理的方式,先将对象都 new 出来,然后进行 set 属性值,而 Spring 也是通过这样的形式来处理的(你说巧不巧?其实一点都不巧 🙃,后面再说为什么),其实 Spring 管理 Bean 的实例化底层其实是由 反射 实现的。
而我们实例化的方式也有好多种,比如通过构造函数,一次性将属性赋值,像下面这样
// 假设有学生这个类
public class Student {
private int id;
private String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
}
// 通过构造器方式实例化并赋值
new Student(1, "Suremotoo");
但是使用构造器这样的方式,是无法解决循环依赖的!为什么不能呢?
我们还是以文中开头的 A 和 B 互相依赖来说, 要通过构造器的方式实现 A 的实例化,如下
new A(b);
😮 Wow,是不是发现问题了?要通过构造器的方式,首先要将属性值实例化出来啊!A 要依赖属性 b,就需要先将 B 实例化,可是 B 的实例化是不是还是需要依赖 A❗️ 这不就是文中开头描述的样子嘛,所以通过构造器的方式,Spring 也没有办法解决循环依赖。
我们使用 set 可以解决,那么 Spring 也使用 set 方式呢?答案是可以的。
既然底层是通过反射实现的,我们自己也用反射实现的话,大概思路是这样的(还是以 A 和 B 为例)
-
先实例化 A 类
-
再实例化 B 类
-
set B 类中的 a 属性
-
set A 类中的 b 属性
其实就是通过反射,实现以下代码
A a = new A();
B b = new B();
b.setA(a);
a.setB(b);
这里可以稍微说明一下,为什么这样可以?
A a = new A(),说明 A 只是实例化,还 未初始化
同理, B b = new B(),也只是实例化,并 未初始化
a.setB(b);, 对 a 的属性赋值,完成 a 的初始化
b.setA(a);, 对 b 的属性赋值,完成 b 的初始化
现在是不是有点感觉了,先把狗骗进来,再杀 😬
Spring 如何解决循环依赖问题
先上个通俗的答案解释,三级缓存, 更详细的解释在后面 进一步思考(在新标签页打开) 章节
/**
* 单例对象的缓存:bean 名称——bean 实例,即:所谓的单例池。
* 表示已经经历了完整生命周期的 Bean 对象
* <b> 第一级缓存 </b>
*/
Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/**
* 早期的单例对象的高速缓存:bean 名称——bean 实例。
* 表示 Bean 的生命周期还没走完(Bean 的属性还未填充)就把这个 Bean 存入该缓存中
* 也就是实例化但未初始化的 bean 放入该缓存里
* <b> 第二级缓存 </b>
*/
Map<String, Object> earlySingletonObjects = new HashMap<>(16);
/**
* 单例工厂的高速缓存:bean 名称——ObjectFactory。
* 表示存放生成 bean 的工厂
* <b> 第三级缓存 </b>
*/
Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
代码中注释可能不清晰,我再给贴一下三级缓存:
第一级缓存(也叫单例池):Map<String, Object> singletonObjects,存放已经经历了完整生命周期的 Bean 对象
第二级缓存:Map<String, Object> earlySingletonObjects,存放早期暴露出来的 Bean 对象,Bean 的生命周期未结束(属性还未填充完)
第三级缓存:Map<String, ObjectFactory<?>> singletonFactories,存放可以生成 Bean 的工厂
Spring 管理的 Bean 其实默认都是单例的,也就是说 Spring 将最终可以使用的 Bean 统一放入第一级缓存中,也就是 singletonObjects(单例池)里,以后凡是用到某个 Bean 了都从这里获取就行了。
仅使用一级缓存可以吗?
既然都从 singletonObjects 里获取,那么 仅仅使用这一个 singletonObjects ,可以吗?肯定不可以的。
首先 singletonObjects 存入的是完全初始化好的 Bean,可以拿来直接用的,如果我们直接将未初始化完的 Bean 放在 singletonObjects 里面,注意,这个未初始化完的 Bean 极有可能会被其他的类拿去用,它都没完事呢,就被拿去造了,肯定要出事啊!
我们以 A、B、C 举栗子 🌰
- 先实例化 A 类,叫 a
- 将 a 放入 singletonObjects 中(此时 a 中的 b 属性还是空的呢)
- C 类需要使用 A 类,去 singletonObjects 获取,且获取到了 a
- C 类使用 a,拿出 a 类的 b 属性,然后 NPE 了.
诺,出事了吧,这下就不是解决循环依赖的问题了,反而设计就不对了。
NPE 就是 NullPointerException
使用二级缓存可以吗?
再来回顾下循环依赖的问题:A→B→A→B......
说到底就是怎么打破这个循环,一级缓存不行,我们就再加一级,可以吗? 我们看个图

图中的缓存就是二级缓存
看完图,可能还会有疑惑,A 没初始化完成放入了缓存,那么 B 用的岂不是就是未完成的 A,是这样的没错! 在整个过程当中,A 是只有 1 个,而 B 那里的 A 只是 A 的引用,所以后面 A 完成了初始化,B 中的 A 自然也就完成了。这里就是文中前面提到的手动 setA,setB 那里,我再贴一下代码:
A a = new A();
B b = new B();
b.setA(a); // 这里设置 b 的属性 a,其实就是 a 的引用
a.setB(b); // 这里设置 a 的属性 b,此时的 b 已经完成了初始化,设置完 a 的属性, a 也就完成了初始化,那么对应的 b 也就完成了初始化
分析到这里呢,我们就会发现二级缓存就解决了循环依赖的问题了,可是为什么还要三级缓存呢? 这里就要说说 Spring 中 Bean 的生命周期。
Spring 中 Bean 的管理

要明白 Spring 中的循环依赖,首先得了解下 Spring 中 Bean 的 生命周期(在新标签页打开)。
Spring 中 Bean 的生命周期,指的就是 Bean 从创建到销毁的一系列生命活动。
那么由 Spring 来管理 Bean,要经过的主要步骤有:
-
Spring 根据开发人员的配置,扫描哪些类由 Spring 来管理,并为每个类生成一个 BeanDefintion,里面封装了类的一些信息,如全限定类名、哪些属性、是否单例等等
-
根据 BeanDefintion 的信息,通过反射,去实例化 Bean(此时就是 实例化但未初始化 的 Bean)
-
填充上述未初始化对象中的属性(依赖注入)
-
如果上述未初始化对象中的方法被 AOP 了,那么就需要生成代理类(也叫包装类)
-
最后将完成初始化的对象存入缓存中(此处缓存 Spring 里叫: singletonObjects),下次用从缓存获取 ok 了
如果没有涉及到 AOP,那么第四步就没有生成代理类,将第三步完成属性填充的对象存入缓存中。
二级缓存有什么问题?
如果 Bean 没有 AOP,那么用二级缓存其实没有什么问题的,一旦有上述生命周期中第四步,就会导致的一个问题。因为 AOP 处理后,往往是需要生成代理对象的,代理对象和原来的对象根本就不是 1 个对象。
以二级缓存的场景来说,假设 A 类的某个方法会被 AOP,过程就是这样的:

- 生成 a 的实例,然后放入缓存,a 需要 b
- 再生成 b ,填充 b 的时候,需要 a,从缓存中取到了 a,完成 b 的初始化;
- 紧接着 a 把初始化好的 b 拿过来用,完成 a 的属性填充和初始化
- 由于 A 类涉及到了 AOP,再然后 a 要生成一个代理类,这里就叫:代理 a 吧
结果就是:a 最终的产物是代理 a,那 b 中其实也应该用代理 a,而现在 b 中用的却是原始的 a
代理 a 和原始的 a 不是一个对象,现在这就有问题了。
使用三级缓存如何解决?
二级缓存还是有问题,那就再加一层缓存,也就是第三级缓存:Map<String, ObjectFactory<?>> singletonFactories,在 bean 的生命周期中,创建完对象之后,就会构造一个这个对象对应的 ObjectFactory 存入 singletonFactories 中。
singletonFactories 中存的是某个 beanName 及对应的 ObjectFactory,这个 ObjectFactory 其实就是生成这个 Bean 的工厂。实际中,这个 ObjectFactory 是个 Lambda 表达式:() -> getEarlyBeanReference(beanName, mbd, bean),而且,这个表达式 并没有 执行。
那么 getEarlyBeanReference 具体做了什么事情?
核心就是两步:
第一步:根据 beanName 将它对应的实例化后且未初始化完的 Bean,存入 Map<Object, Object> earlyProxyReferences = new ConcurrentHashMap<>(16);
第二步:生成该 Bean 对应的代理类返回
这个 earlyProxyReferences 其实就是用于记录哪些 Bean 执行过 AOP,防止后期再次对 Bean 进行 AOP
那么 getEarlyBeanReference 什么时候被触发,什么时候执行?
在二级缓存示例中,填充 B 的属性时候,需要 A,然后去缓存中拿 A,此时先去第三级缓存中去取 A,如果存在,此时就执行 getEarlyBeanReference 函数,然后该函数就会返回 A 对应的代理对象。
后续再将该代理对象 放入第二级缓存 中,也就是 Map<String, Object> earlySingletonObjects 里。
为什么不放入第一级缓存呢?
此时就拿到的代理对象,也是未填充属性的,也就是仍然是未初始化完的对象。
如果直接放入第一级缓存,此时被其他类拿去使用,肯定有问题了。
那么什么时候放入第一级缓存?
这里需要再简单说下第二级缓存的作用,假如 A 经过第三级缓存,获得代理对象,这个代理对象仍然是未初始化完的!那么就暂时把这个代理对象放入第二级缓存,然后删除该代理对象原本在第三级缓存中的数据(确保后期不会每次都生成新的代理对象),后面其他类要用了 A,就去第二级缓存中找,就获取到了 A 的代理对象,而且都用的是同一个 A 的代理对象,这样后面只需要对这一个代理对象进行完善,其他引入该代理对象的类就都完善了。
再往后面,继续完成 A 的初始化,那么先判断 A 是否存在于 earlyProxyReferences 中, 存在就说明 A 已经经历过 AOP 了,就无须再次 AOP。那 A 的操作就转换从二级缓存中获取,把 A 的代理类拿出来,填充代理类的属性。
完成后再将 A 的代理对象加入到第一级缓存,再把它原本在第二级缓存中的数据删掉,确保后面还用到 A 的类,直接从第一级缓存中获取。
看个图理解下

初步总结
说了这么多,总结下三级缓存:
第一级缓存(也叫单例池):Map<String, Object> singletonObjects,存放已经经历了完整生命周期的 Bean 对象
第二级缓存:Map<String, Object> earlySingletonObjects,存放早期暴露出来的 Bean 对象,Bean 的生命周期未结束(属性还未填充完),可能是代理对象,也可能是原始对象
第三级缓存:Map<String, ObjectFactory<?>> singletonFactories,存放可以生成 Bean 的工厂,工厂主要用来生成 Bean 的代理对象
👍特别推荐-三级缓存动画
Tip
下面这个动画比较简单,能让大家更形象地理解 Spring 的三级缓存是大致是怎么工作的,动画中有一些细节可能不太准确,但整体的流程和核心概念应该么问题。
里面包含了 4 个场景,可以切换查看每个场景的动画😉 一定要注意看 Step 描述。
🎉 附: 一个完整的 Spring 循环依赖时序图
时序图并不标准,但是方便大家去理解 🙃

进一步思考
如果看完前面的内容,那么对 Spring 的三级缓存已经有大致的了解,可能大部分面试也够用了,但实际上,我们可以进一步细细思考。
只用一级缓存真的不能实现吗?只用二级缓存真的不能实现吗? 第三级是延迟执行的函数,该函数要么返回原始对象,要么返回代理对象,那我们在放入第二级缓存的时候直接先执行该函数来提前获取对象,再放入缓存中,是不是也可以?
Spring Bean 的设计
我们再简单的说一下,在 Spring 的架构设计中,Bean 的生命周期被划分得非常清晰。
代理对象的创建,应当在 Bean 完成“初始化(Initialization)”之后进行。
生命周期时间线:
- 实例化(
Instantiation):new出来一个空对象。 - 属性填充(
Populate):注入@Autowired依赖。 - 初始化(
Initialization):执行@PostConstruct、afterPropertiesSet等方法。 - AOP 代理(
Post-Processing):原则上必须在这里! 如果需要代理,此时用代理对象替换原始对象。
为什么要这样设计
因为“初始化”阶段可能会对对象的状态进行重要的设置(比如开启资源、校验参数)。 AOP 生成的代理对象通常需要持有一个“状态完备”的目标对象(Target),如果在初始化之前就代理,代理持有的 Target 是一个“半成品”,这在架构上是不严谨的。
只用一级缓存行不行?
- 理论上:可以,在“无 AOP / 无 BeanPostProcessor、只考虑字段注入循环依赖”的理想模型里完全可行。
- 实际上:会把“解决循环依赖”这件事硬塞进正常 bean 创建主流程,破坏抽象,也让 AOP / 其他增强点变得非常别扭甚至不可控。
只用二级缓存行不行?
- 理论上:可以!
用二级缓存解决循环依赖,你必须在 实例化(第1步) 之后,立刻判断是否需要 AOP,如果需要,马上创建代理放进缓存。
- 所有 的 Bean,无论它是否涉及循环依赖,全都在 第 1 步 就执行,“无差别提前触发”,全都提前暴露。
- 这是一个“宁可错杀一千,不可放过一个”的方案,为了解决那 1% 的循环依赖场景,让 99% 正常的 Bean 都违背了生命周期原则。
回顾三级缓存
Spring 放入三级缓存的是一个 Factory(工厂),它 暂不执行 AOP,只是把“怎么做 AOP”的逻辑存起来,在看两个场景:
场景 A:普通 Bean(无循环依赖,占 99%)
- Factory 永远不会被调用。
- Bean 顺顺利利走到 第 4 步(初始化后)。
- 此时创建代理。
- 100% 遵循设计原则 ,完美遵循了生命周期。
场景 B:循环依赖 Bean(占 1%)
- 走到一半,发现必须要有 A 的引用才能继续,否则程序会死循环/报错,不得已,去调用三级缓存的 Factory。
- Factory 提前执行 AOP,生成代理给 B 用。
- 打破了原则,这里确实是在初始化前创建了代理。
Note
举个例子,A 依赖 B,B 依赖 A 和 C 和 D,C 和 D 又依赖 A,创建 A 的时候初始化需要 B,创建 B 的时候初始化需要 A,拿到 A 的 ObjectFactory 后调用接口方法获取对象,B 还需要 C 和 D,它们又需要去调用 A 的 ObjectFactory,所以就重复调用了 getObject 方法,其实只要 getEarlyBeanReference 方法实现保证同一个 beanName 返回同一个对象,就不需要第二级缓存。
但是,这又暴露了内部实现细节,假如我弄个 BeanPostProcessor 实现类,spring 也没有提示我要遵守上述约定,而我在 getEarlyBeanReference 方法里只是创建新对象返回,这就会导致 B 里面的是 A1,C 里面是 A2,D 里面是 A3,那完了,芭比 Q 了!
所以让实现者去做重复性判断是不可控的,很容易出现问题,于是乎引入了第二级缓存,当调用三级缓存里的对象工厂的 getObject 方法之后,spring 就会把返回值放入二级缓存,删除三级缓存,这样 C 和 D 取的就是二级缓存里的 A 对象,和 B 里的是同一个。
所以二级缓存和三级缓存其实是一套组合拳,不要拆成两个独立的东西去理解,出发点就不对。
网上其他网友的理解也很值得参考:
Note
我个人觉得这个最好理解的👇
感觉主要还是因为 Spring 开发者不希望循环依赖的处理影响了原本创建 bean 的流程,原本是在 bean 初始化之后,再通过后置处理来创建代理对象; 如果只使用一级缓存,那势必要在实例化 bean 后直接创建代理对象放入缓存,这样就仅仅因为循环依赖破坏了原本的流程,个人认为是没有必要的。
三级缓存的单例工厂可以保证原对象唯一但是代理对象无法保证,所以函数式接口触发要提前生成代理对象时,生成的代理对象需要缓存来保证唯一引用,所以有了二级缓存。
最终理解
Spring 之所以设计三级缓存,并不是单纯为了解决循环依赖,而是为了在不破坏 Bean 正常生命周期的前提下,按需、延迟地处理循环依赖问题。
Tip
具体来说:
- 一级缓存保存的是生命周期完成后的 singleton,语义上只允许完整对象存在;
- 三级缓存通过 ObjectFactory 延迟 early reference 的创建时机,只有在真正发生循环依赖时才触发;
- 二级缓存用于缓存已经生成的 early reference,保证在同一轮循环依赖中,对同一 Bean 的引用保持一致;
这种设计避免了“为了少数循环依赖场景,污染所有 Bean 的创建流程”,同时也保证了 singleton 身份、AOP 代理和生命周期的一致性。