为什么要用三级缓存来解决循环依赖问题?二级缓存行不行?一级缓存行不行?

正文

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 之后就生成代理对象放入一级缓存,也是行不通的。

博主这里的报错我没有验证,但这里已经违背“如果只使用一级缓存的话,缓存的插入应该放在 initializeBean 之后”的原则了,这个假设就没意义了,因为在多线程环境下,实例化之后就存入缓存,属性可能没注入,大概率出现空指针。

当然结论虽重要,思路更珍贵!

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

如果使用二级缓存的话:
假设使用一级缓存来存放完全初始化好的 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 的创建流程更加符合常理,更加清晰明了。

模拟AOP场景下的循环依赖:

 public static void main(String[] args) {
        // 创建容器,读取配置,创建bean,     
        AnnotationConfigApplicationContext context
                = new AnnotationConfigApplicationContext("com.tiger.springbean");
        context.getBean("test");

    }




@Component
public class Test {

    @Autowired
    Test1 test1;

  
    public void demo() {

    }
}



@Component
@EnableAsync
public class Test1 {

    @Autowired
    private Test test;

    @Async
    public void demo() {

    }
}

   绿色标识并不精确,普通AOP代理(普通的 AOP proxy 类型指:通过用户自定义的 @Aspect 切面生成的代理 bean,区别于 @Async 标记的类产生的 AOP 代理)提前生成了代理对象,解决了循环依赖,并不是蓝色代码处才生成,而是红色代码调三级缓存生成,而 @Async生成的代理是在蓝色代码初始化这里,所以@Async可能因为其他bean引入的缓存是原始引用,而它自己却初始化之后暴露了代理从而导致bean存在两种不同的引用而抛出异常。把 @Async换到Test就可以模拟出循环依赖异常的场景。

可以下载源码修改绿色部分代码,直接生成代理对象,然后放进二级缓存。源码下载地址:spring-framework

test1是cglib的代理对象,test是原始对象。 

// 依然从缓存中获取,注意这里第二个参数是false,也就是说只能从一级缓存、二级缓存中获取 
// 因为此时还未放入一级缓存,所以肯定是没有的,只能从二级缓存中获取 
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
  // 比较二级缓存和最终暴露的bean
   if (exposedObject == bean) {
      exposedObject = earlySingletonReference;
   } ......

为什么比较的是exposedObject == bean;而不是exposedObject ==earlySingletonReference?exposedObject是最终暴露在spring中的bean,earlySingletonReference是缓存中被其他bean引入的,它们不一致才说明存在异常了

这里纠结了不少时间,一直纳闷来这么个比较,分析一下发现其实这两就一个意思:

bean 原始引用

exposedObject 原始引用或代理

earlySingletonReference 原始引用或代理

那么判断exposedObject ==earlySingletonReference,存在四种情况:

判断为true两种情况,都为原始引用或都为代理,这时候啥都不用干,暴露的bean和别人引用的bean一致;

第三种情况,exposedObject是原始引用,earlySingletonReference是代理,那把earlySingletonReference赋值给exposedObject保持一致;

第四种情况,exposedObject是代理,但earlySingletonReference是原始引用,这个得去其他bean把原始引用改成代理,太麻烦了点,直接走下文抛异常

第三第四结合起来其实就是判断exposedObject == bean

————————————————
版权声明:本文为CSDN博主「老王学源码」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wang489687009/article/details/120655156

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值