首页 > 其他分享 >100%吃透Spring 的三级缓存

100%吃透Spring 的三级缓存

时间:2024-11-06 18:44:47浏览次数:6  
标签:缓存 beanA 对象 Spring 100% 方法 Bean 断点

在此之前,我们需要了解什么是spring的循环依赖,下面我引用一篇之前的文档

此处为语雀内容卡片,点击链接查看:https://www.yuque.com/u41175337/xy9eiy/egcll6gqml0ofb9a


然后带你从源码级别debug,一步一步带你探索Spring是如何通过三级缓存来解决循环依赖问题的

首先先创建两个类,分别是类A和类B,然后定义好get和set方法和无参数构造

下一步通过spring容器注入两个bean

ps:xml中bean的scope值不设置的时候默认是单例的,如果将scope值设置为prototype,则会抛出BeanCurrentlyCreationException,因为默认的singleton场景是支持循环依赖的,而原型模式不支持

接下来启动后会发现,A和B都被创建成功了,spring究竟是如何做到的?下面带你debug深入去探索底层原理

Spring内部的三级缓存

所谓三级缓存其实就spring用来解决循环依赖的三个Map

  • 一级缓存(singletonObjects),也叫单例池,他是用来存放已经经历了完整生命周期的Bean对象
  • 二级缓存(earlySingletonObjects),用于存放早期暴露出来的Bean对象,,Bean的生命周期未结束(属性未完全填充)
  • 三级缓存(Map<String,ObjectFactory<?>>) singletonFactories ),用于存放可以生成Bean的工厂

下面请跟着我通过打断点,一步一步的探索底层原理

首先第一个断点打在第八行的位置,然后以debug的方式启动

第一次步入,会跳到这个位置来,我们需要再步出一下

我们再步入,到了这个位置,发现这里有一个this,是一个构造方法,那就对了

继续步入

走到refresh这个方法,refresh这个方法在spring容器中是非常重要的,说白了我们第一个断点那一行代码的底子就是refresh,在面试考官的时候,会问你知不知道spring容器中的初始方法,refresh谈一下? 如果你一脸懵逼,那说明你根本没有看过底层源码

第二个断点打在refresh这一行,重新debug启动

步入refresh方法,会发现refresh中的代码很多,那么断点应该打在哪里呢?我们一步一步走

当代码走到这一行的时候,控制台打印出了东西,有这句话说明spring已经开始加载容器了

继续走,走到这一行,控制台又打印了一句,说明A、B这两个Bean要从配置文件中读进去了

当走到finishRefresh这个方法时,控制台一下子就打印出来了A、B被创建,那么这个finishRefresh上面那一句就是我们第三个落脚点

继续步入finishBeanFactoryInitialization这个方法,然后一步一步走

当走到这一行时,控制台打印了,那这个方法就是我们的第四个落脚点

步入到preInstanceSingleton这个方法,然后走到ArrayList时,会发现list集合中有两个对象,分别是我们的A和B,这就是要先形成容器,然后告诉容器有两个bean,然后把两个bean遍历出来放入容器中

继续往下走,走到这个getBean时打印了,这个getBean方法就是我们第五个落脚点

步入getBean方法,会发现其中有一个doGetBean方法,这也是一个考点,有的大厂面试官可能会问,你知不知道spring源码中以do开头的方法是什么意思?

如果你读过spring源码,那你一定能答出来。

听好,在spring源码中,凡是以do开头的方法,就是真真正正的业务逻辑方法

进入到doGetBean这个方法后,我们继续往下走,走到getSingleton这一行的lamda表达式,在这个位置打一个断点

进入getSingleton方法后,在这个位置打一个断点。

注意这个方法的名字,singletonObjects(一级缓存)!!!表示已经经历了完整生命周期的Bean对象

但是我现在还没有把A创建出来,所以往下走肯定为null

继续往下面走,会有一个mdb,这个mdb就是Bean对象,再往下走,会有一个if判断,它会判断这个Bean对象是否是单例的Bean,如果是单例Bean,那就直接拿,如果不是,则通过lambda表达式中的createBean方法创建一个Bean对象,因为我们的Bean是单例模式,所以会走里面的方法,所以断点打到这里

getSingleton这一行也打上一个断点,先步入getSingleton这个方法

往下走,在这个singletonObject这里打上断点

此时一进去,发现回到了createBean这个方法上!也就是说我们刚开始去容器里去拿这个B,结果没有,没有的话怎么办?这里就走到lambda表达式里,有点类似于回调

接下来我们进入到createBean方法中,往下走,找到doCreateBean这里打上断点

进入doCreateBean方法,往下走,在createBeanInstance这里停下,进入。继续往下走,回到原位,打上断点,目前我们这个包装类还为null

继续往下走,到这一步就算是初步的获取实例A

走到这一步时,做了一个boolean值的判断,首先判断这个对象是否为单例的Bean对象,然后判断这个对象是否允许循环引用,它的默认值就是true,如果满足的话,这个Bean对象就是一个早期被暴露出来的Bean对象

继续走,走到这里时,打一个断点,这里表示将这个Bean添加到单例工厂里

接下来我们进入这个方法,可以看到在同步代码块中有一个判断条件就是这个单例池中是否包含这个bean,这时并没有这个bean对象,所以是false,false再取反,就是true,所以进入到if分支代码,第一句的是调用的singletonFactories中的put方法,singletonFactories也就是三级缓存,就是说这个bean会被放入三级缓存中,第二句调用的就是earlySingletonObjects方法中的remove方法,将这个bean从二级缓存中删掉,但是我们现在二级缓存里根本没有这个bean,所以这里相当于是做了一个空删,最后一个就是注册bean,跟我们没关系,不管

上面的方法走完,这个BeanB也就完成了我们的doCreateBean

继续往下走,到这个populateBean方法打上断点,populate也就是填充的意思,这个方法就是在BeanB进行属性填充

进入populateBean这个方法,往下走,会看到有一个pvs,这个pvs就是表示这个BeanB需要一个属性填充,也就是BeanA

继续往下走,找到这个方法,打上断点,然后步入

步入方法后往下走,找到这个方法,打上断点,继续步入

步入方法后往下走,找到这个方法,打上断点,继续步入

往下走,找到getBean方法这里,打上断点

我们步入这个getBean方法,和之前一样,又是这个doGetBean,我们已经见过一次了

我们步入这个doGetBean,发现回到了之前断点的位置,之后创建BeanA的步骤就和BeanB一样了,我也就不再演示了,所有的断点都已经给你找了出来,只需要跟着这个断点一步一步走,就能梳理清楚spring底层是如何通过三级缓存来解决循环依赖这个问题

我们梳理一下整个流程

  1. 调用doGetBean()方法,想要取beanA,于是调用getSingleton()方法从缓存中查找beanA
  2. 在getSingleton()方法中,从一级缓存中查找,没有,返回null
  3. doGetBean()方法中获取到的beanA为null,于是走对应的处理逻辑,调用getSingleton()的重载方法(参数为ObjectFactory的)
  4. 在getSingleton()方法中,先将beanA_name添加到一个集合中,用于标记该bean正在创建中。然后回调匿名内部类的creatBean方法
  5. 进入AbstractAutowireCapableBeanFactory#doCreateBean,先反射调用构造器创建出beanA的实例,然后判断:是否为单例、是否允许提前暴露引用(对于单例一般为true)、是否正在创建中(即是否在第四步的集合中)。判断为true则将beanA添加到三级缓存中
  6. 对beanA进行属性填充,此时检测到beanA依赖于beanB,于是开始查找beanB
  7. 调用doGetBean()方法,和上面beanA的过程一样,到缓存中查找beanB,没有则创建,然后给beanB填充属性
  8. 此时beanB依赖于beanA,调用getSingleton()获取beanA,依次从一级、二级、三级缓存中找,此时从三级缓存中获取到beanA的创建工厂,通过创建工厂获取到singletonObject,此时这个singletonObject指向的就是上面在doCreateBean()方法中实例化的beanA
  9. 这样beanB就获取到了beanA的依赖,于是beanB顺利完成实例化,并将beanA从三级缓存移动到二级缓存中
  10. 随后beanA继续他的属性填充工作,此时也获取到了beanB,beanA也随之完成了创建,回到getSingleton()方法中继续向下执行,将beanA从二级缓存移动到一级缓存中|

Spring中Bean的创建过程其实可以分成两步,第一步叫做实例化,第二步叫做初始化。

实例化的过程只需要调用构造函数把对象创建出来并给他分配内存空间,而初始化则是给对象的属性进行赋值。

而Spring之所以可以解决循环依赖就是因为对象的初始化是可以延后的,也就是说,当我创建一个Bean A的时候,会先把这个对象实例化出来,然后再初始化其中的B属性。

而当一个对象只进行了实例化,但是还没有进行初始化时,我们称之为半成品对象。所以,所谓半成品对象,其实只是 bean 对象的一个空壳子,还没有进行属性注入和初始化。

当两个Bean在初始化过程中互相依赖的时候,如初始化A发现他依赖了B,继续去初始化B,发现他又依赖了A,那这时候怎么办呢?大致流程如下:

通过以上方式,就通过引入三级缓存,解决了循环依赖的问题,在上述流程执行完之后,A和B都被成功的完成了实例化和初始化。

扩展知识

Spring解决循环依赖一定需要三级缓存吗?

其实,使用二级缓存也能解决循环依赖的问题,但是如果完全依靠二级缓存解决循环依赖,意味着当我们依赖了一个代理类的时候,就需要在Bean实例化之后完成AOP代理。而在Spring的设计中,为了解耦Bean的初始化和代理,是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来在Bean生命周期的最后一步来完成AOP代理的。

但是,在Spring的初始化过程中,他是不知道哪些Bean可能有循环依赖的,那么,这时候Spring面临两个选择:

  1. 不管有没有循环依赖,都提前把代理对象创建出来,并将代理对象缓存起来,出现循环依赖时,其他对象直接就可以取到代理对象并注入。
  2. 不提前创建代理对象,在出现循环依赖时,再生成代理对象。这样在没有循环依赖的情况下,Bean就可以按着Spring设计原则的步骤来创建。

第一个方案看上去比较简单,只需要二级缓存就可以了。但是他也意味着,Spring需要在所有的bean的创建过程中就要先成代理对象再初始化;那么这就和spring的aop的设计原则(前文提到的:在Spring的设计中,为了解耦Bean的初始化和代理,是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来在Bean生命周期的最后一步来完成AOP代理的)是相悖的。

而Spring为了不破坏AOP的代理设计原则,则引入第三级缓存,在三级缓存中保存对象工厂,因为通过对象工厂我们可以在想要创建对象的时候直接获取对象。有了它,在后续发生循环依赖时,如果依赖的Bean被AOP代理,那么通过这个工厂获取到的就是代理后的对象,如果没有被AOP代理,那么这个工厂获取到的就是实例化的真实对象。

标签:缓存,beanA,对象,Spring,100%,方法,Bean,断点
From: https://blog.csdn.net/o0oho/article/details/143521141

相关文章