#yyds干货盘点# Spring源码三千问为什么要用三级缓存来解决循环依赖问题?二级缓存行不行?一级缓存行不行?

Posted 老王学源码

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了#yyds干货盘点# Spring源码三千问为什么要用三级缓存来解决循环依赖问题?二级缓存行不行?一级缓存行不行?相关的知识,希望对你有一定的参考价值。

前言

前面的文章 中,我们分析了 Spring 是怎样使用三级缓存来解决循环依赖的。
使用三级缓存的关键问题是为了解决下面的场景:
当 AOP 代理 bean 被循环依赖时,需要通过第三级缓存 singletonFactories 提前获取到原始 bean 对应的 AOP 代理对象,从而将 AOP 代理 bean 的引用作为依赖注入到目标对象中。

那么,Spring 为什么要用三级缓存来解决循环依赖问题呢?如果只用二级缓存行不行?只用一级缓存呢?

版本约定

Spring 5.3.9 (通过 SpringBoot 2.5.3 间接引入的依赖)

正文

Spring 做为一个优秀的框架,在设计上肯定有很多的思考。所以,我们猜想如果减少缓存应该是不能达到目的的。
想要证明这个结论,我们只需要各举一个反例就行。

分析:如果只使用一级缓存

首先,我们分析一下,如果只用一级缓存行不行?
只使用一级缓存,也就是将所有的 bean 的实例都放在同一个 Map 容器中。其中就包括已经初始化好的 bean 和未初始化好的 bean。

bean 的创建过程分三个阶段:
1 创建实例 createBeanInstance
2 填充依赖 populateBean
3 initializeBean

假设 bean 是需要 AOP 增强的,那么最终放到缓存中的应该是一个代理 bean。而代理 bean 的产生是在 initializeBean(第三阶段) 的时候。所以,我们推导出:如果只使用一级缓存的话,缓存的插入应该放在 initializeBean 之后。

如果在 initializeBean 的时候记录缓存,那么碰到循环依赖的情况,需要在 populateBean(第二阶段) 的时候再去注入循环依赖的 bean,此时,缓存中是没有循环依赖的 bean 的,就会导致 bean 重新创建实例。
这样显然是不行的。

反例:
循环依赖场景:A-->B-->A
A 在 createBeanInstance 时,创建了 bean 实例,接着 populateBean 会填充依赖的 bean B,从而触发 B 的加载;
B 在 populateBean 时,发现要注入依赖的 bean A,先从缓存中获取 bean A,获取不到,就会重新创建 bean A。
这样违背了 bean 的单例性,所以只使用一级缓存是不行的。

理论上使用一级缓存是可以解决普通场景下的循环依赖的,因为对于普通场景,从始至终 bean 的对象引用始终都是不变的。  
但是,如果被循环依赖的 bean 是一个 AOP 增强的代理 bean 的话,bean 的原始引用和最终产生的 AOP 增强 bean 的引用是不一样的,一级缓存就搞不定了。  

疑问:如果在 createBeanInstance 之后就生成代理对象放入一级缓存呢?

我们或许会有疑问,如果不按 spring 原本的设计,我们在 bean 创建的第一步 createBeanInstance 之后就判断是否生成代理对象,并将要暴露的对象放入一级缓存是不是就可以解决所有场景的循环依赖问题呢?

分析
再利用上面的反例来分析一遍,放入一级缓存的 bean 与暴露到容器中的 bean (不管是否有代理)始终是同一个 bean,看似好像是没有问题的,好像是可以解决循环依赖的问题。
但是这里忽略了一个问题:bean 创建的第二步中 populateBean 的底层实现是将原始 bean 对象包装成 BeanWrapper,然后通过 BeanWrapper 利用反射设置值的。
如果在 populateBean 之前生成的是一个代理对象的话,就带来了另外一个问题 :jdk proxy 产生的代理对象是实现的目标类的接口,jdk proxy 的代理类通过 BeanWrapper 去利用反射设置值时会因为找不到相应的属性或者方法而报错。
所以,如果在 createBeanInstance 之后就生成代理对象放入一级缓存,也是行不通的。

分析:如果只使用二级缓存

如果使用二级缓存的话:
假设使用一级缓存来存放完全初始化好的 bean,使用二级缓存来存放未初始化好的 bean(即 bean 的早期引用)。
一级缓存在 initializeBean 之后进行存放,二级缓存在 createBeanInstance 之后进行记录。
这样的话,就需要在之前暴露三级缓存的地方,提前将二级缓存生成后放入 Map<String, Object> earlySingletonObjects
也就是说,如果 bean 是需要被 AOP 增强的,就需要提前将 AOP 代理 bean 生成。

理论上,这样处理是可以解决 AOP 代理 bean 被循环依赖的问题的,暂时没有想到反例来证明使用二级缓存不行。

Spring 为什么不用二级缓存来解决循环依赖问题?

Spring 原本的设计是,bean 的创建过程分三个阶段:
1 创建实例 createBeanInstance -- 创建出 bean 的原始对象
2 填充依赖 populateBean -- 利用反射,使用 BeanWrapper 来设置属性值
3 initializeBean -- 执行 bean 创建后的处理,包括 AOP 对象的产生
在没有循环依赖的场景下:第 1,2 步都是 bean 的原始对象,第 3 步 initializeBean 时,才会生成 AOP 代理对象。

循环依赖属于一个特殊的场景,如果在第 3 步 initializeBean 时才去生成 AOP 代理 bean 的话,那么在第 2 步 populateBean 注入循环依赖 bean 时就拿不到 AOP 代理 bean 进行注入。
所以,循环依赖打破了 AOP 代理 bean 生成的时机,需要在 populateBean 之前就生成 AOP 代理 bean。
而且,生成 AOP 代理需要执行 BeanPostProcessor,而 Spring 原本的设计是在第 3 步 initializeBean 时才去调用 BeanPostProcessor 的。
并不是每个 bean 都需要进行这样的处理,所以, Spring 没有直接在 createBeanInstance 之后直接生成 bean 的早期引用,而是将 bean 的原始对象包装成了一个 ObjectFactory 放到了三级缓存 Map<String, Object> earlySingletonObjects
当需要用到 bean 的早期引用的时候,才通过三级缓存 Map<String, ObjectFactory<?>> singletonFactories 来进行获取。

如果只使用二级缓存来解决循环依赖的话,那么每个 bean 的创建流程中都需要插入一个流程——创建 bean 的早期引用放入二级缓存。
其实,在真实的开发中,绝大部分的情况下都不涉及到循环依赖,而且 createBeanInstance --> populateBean --> initializeBean 这个流程也更加符合常理。

所以,猜想 Spring 不用二级缓存来解决循环依赖问题,是为了保证处理时清晰明了,bean 的创建就是三个阶段: createBeanInstance --> populateBean --> initializeBean
只有碰到 AOP 代理 bean 被循环依赖时的场景,才去特殊处理,提前生成 AOP 代理 bean

附:三级缓存的定义如下

// DefaultSingletonBeanRegistry.java

/** 一级缓存: Cache of singleton objects: bean name to bean instance. */
// 用于存放已经完全初始化好的 bean 
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** 二级缓存: Cache of early singleton objects: bean name to bean instance. */
// 用于存放三级缓存 ObjectFactory 获取到的对象(循环依赖时才会存放并使用)  
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

/** 三级缓存: Cache of singleton factories: bean name to ObjectFactory. */
// 用于存放 bean 的早期引用(都会存放,循环依赖时才会使用)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

辟谣:使用二级缓存解决不了 AOP 代理 bean 的循环依赖?

在网上有看到文章分析,说 Spring 必须要使用三级缓存是因为只用二级缓存解决不了 AOP 代理 bean 的循环依赖问题。
通过前面的分析,理论上来说,使用二级缓存是可以解决 AOP 代理 bean 的循环依赖的。只是 Spring 没有选择这样去实现。

AOP 代理的产生是在:bean 创建的第三个阶段 initializeBean 的时候,它会处理 @Async、@Schedule 的代理对象 和 @Around 等切入点表达增强的代理对象
AsyncAnnotationBeanPostProcessor ---> 处理 @Async
AnnotationAwareAspectJAutoProxyCreator -——> 处理 @Around 等 advisor 切入点表达式的 AOP 代理
(@Transactional 也归为 advisor 一类,它使用的是内置的 BeanFactoryTransactionAttributeSourceAdvisor)

当 AOP 代理 bean 被循环依赖时,AOP 代理的产生时机就会提前。Spring 会提前通过三级缓存 Map&lt;String, ObjectFactory&lt;?&gt;&gt; singletonFactories 来获取到 bean 的早期引用,这个早期引用就是 Spring 容器最终暴露的 bean 的引用。

小结

如果没有循环依赖的情况的话,一级缓存就可以搞定所有的情况,只需要在 bean 完全初始化好之后将其放入一级缓存即可。
但是一级缓存解决不了循环依赖的情况,所以,Spring 使用三级缓存来解决了循环依赖问题。

如果使用二级缓存的话,理论上是可行的,但是 Spring 选择了三级缓存来实现,让 bean 的创建流程更加符合常理,更加清晰明了。


如果本文对你有所帮助,欢迎点赞收藏

有关 Spring 源码方面的问题欢迎一起交流,备注:51cto (vx: Kevin-Wang001)


博主好课推荐:

课程 地址
Dubbo源码解读——通向高手之路 https://edu.51cto.com/course/23382.html
正则表达式基础与提升 https://edu.51cto.com/course/16391.html

以上是关于#yyds干货盘点# Spring源码三千问为什么要用三级缓存来解决循环依赖问题?二级缓存行不行?一级缓存行不行?的主要内容,如果未能解决你的问题,请参考以下文章

#yyds干货盘点# Spring源码三千问为什么要用三级缓存来解决循环依赖问题?二级缓存行不行?一级缓存行不行?

#yyds干货盘点# Spring源码三千问AdviceAdvisorAdvised都是什么接口?

#yyds干货盘点#Spring源码三千问Spring AOP 中 TargetSource 的作用及原理分析

#yyds干货盘点# Spring源码三千问Bean的Scope有哪些?scope=request是什么原理?

#yyds干货盘点# Spring源码三千问BeanDefinition详解——什么是 RootBeanDefinition?merged bean definition 又是什么鬼?

#yyds干货盘点# Spring源码三千问Spring动态代理:什么时候使用的 cglib,什么时候使用的是 jdk proxy?