【Spring源码三千问】为什么要用三级缓存来解决循环依赖问题?二级缓存行不行?一级缓存行不行?

前言

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

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

凡事多问几个为什么,理解问题会更加透彻

版本约定

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

正文

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

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

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

已经初始化好的 bean: 指经过了 bean 创建的三个阶段之后的 bean 对象
未初始化好的 bean : 指经过了 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. */
// 用于存放 bean 的早期引用对象(循环依赖时才会存放并使用)  
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

/** 三级缓存: Cache of singleton factories: bean name to ObjectFactory. */
// 用于存放 bean 对应的 ObjectFactory(都会存放,循环依赖时才会使用)
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 会提前通过三级缓存 singletonFactories 来获取到 bean 的早期引用,这个早期引用就是 Spring 容器最终暴露的 bean 的引用。

小结

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

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


SpringIoC源码视频讲解:

课程地址
SpringIoC源码解读由浅入深https://edu.51cto.com/sd/68e86

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

源码测试工程下载:
老王读Spring IoC源码分析&测试代码下载
老王读Spring AOP源码分析&测试代码下载

公众号后台回复:下载IoC 或者 下载AOP 可以免费下载源码测试工程…

阅读更多文章,请关注公众号: 老王学源码
gzh


系列博文:
【老王读Spring IoC-0】Spring IoC 引入
【老王读Spring IoC-1】IoC 之控制反转引入
【老王读Spring IoC-2】IoC 之 BeanDefinition 扫描注册
【老王读Spring IoC-3】Spring bean 的创建过程
【老王读Spring IoC-4】IoC 之依赖注入原理
【老王读Spring IoC-5】Spring IoC 小结——控制反转、依赖注入

相关阅读:
【Spring源码三千问】@Resource 与 @Autowired 的区别
【Spring源码三千问】bean name 的生成规则
【Spring源码三千问】BeanDefinition详细分析
【Spring源码三千问】Spring 是怎样解决循环依赖问题的?
【Spring源码三千问】哪些循环依赖问题Spring解决不了?
【Spring源码三千问】@Lazy为什么可以解决特殊的循环依赖问题?

  • 14
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 17
    评论
Spring框架中,三级缓存是用于解决循环依赖问题的一种机制。三级缓存分别是singletonObjects、earlySingletonObjects和singletonFactories。 1. singletonObjects:该缓存用于存储完全初始化完成的单例对象。在对象创建完成后,会将其放入此缓存中。 2. earlySingletonObjects:该缓存用于存储早期曝光的未完全初始化的单例对象。在对象的创建过程中,如果发生循环依赖,会将正在创建的对象放入此缓存中。 3. singletonFactories:该缓存用于存储用于创建单例对象的工厂对象。当对象创建过程中发生循环依赖时,会将正在创建对象的工厂对象放入此缓存中。 这三个Map在Spring中的异同如下: - 异同点:它们都是用于缓存单例对象的,用于解决循环依赖问题。 - 不同点:singletonObjects用于存储完全初始化完成的单例对象,而earlySingletonObjects和singletonFactories则分别用于存储未完全初始化的单例对象和创建单例对象的工厂对象。 循环依赖是指两个或多个Bean之间相互依赖形成了一个闭环,无法正确地创建这些Bean实例。例如,A依赖B,B又依赖A,就形成了循环依赖。在Spring中,循环依赖是一种容器级别的问题。 我了解Spring源码中的循环依赖处理机制,Spring通过使用三级缓存和提前曝光的方式来解决循环依赖问题。在对象创建过程中,当检测到循环依赖时,会从缓存中获取早期曝光的对象,从而避免了循环依赖带来的问题。 检测是否存在循环依赖可以通过构造器循环依赖来实现,当一个Bean在创建过程中多次被构造器引用,则说明存在循环依赖Spring会抛出BeanCurrentlyInCreationException异常来表示循环依赖的存在。 在实际开发中,循环依赖是一个常见的问题。当Bean之间的依赖关系设计不当或者配置错误时,就可能出现循环依赖的异常。在这种情况下,我们需要仔细检查Bean的依赖关系,并进行适当的调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老王学源码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值