Spring 为什么需要三个级别的缓存来解决循环依赖
Spring 的三级缓存是绕不过去的一个坎儿。面试也经常被问到。而网文大多都在讲 Spring 三级缓存的用途,而分析的很好的很少。
接下来整篇文章分析下:Spring 为什么要使用三级缓存解决循环依赖,而不是二级缓存或是一级缓存
导学
先来明白一下 Spring 实例化一个 Bean 的过程中几个重要概念
getBean
通过 getBean 方法获取单例 Bean,每个 bean 的创建都是从该方法开始的。
getSingleton
Spring 中最最重要的核心逻辑,没有之一。该方法依次从一级缓存、二级缓存、三级缓存中获取单例 bean 对象。
注意:如果从三级缓存中获取到对象之后,就会被立即移动到二级缓存。下面是源码:
三个级别缓存作用
一级缓存:存放最终的单例 Bean,里面所有的 Bean 都是直接能使用的。(这个大家一定都明白就不多说了)
三级缓存:存放一个工厂对象,这个工厂对象有一个 getObject 方法。工厂一般由 lambda 表达式组成,在工厂中主要完成了对 Aop 的代理。执行一些 Bean 的扩展逻辑。
二级缓存:没错,先介绍三级缓存是有目的的。二级缓存只有在 getSingleton(前文提到)方法中,才会把三级缓存获得的对象存入二级缓存。并且删除三级缓存中的工厂对象。至于为什么总结里会说。
循环依赖示例
废话不多说,先给两个类 Aoo 和 Boo,这两个类形成循环依赖,代码简单如下。
循环依赖执行流程
首先 Spring 会扫描指定的包,把所有标注 @Component 注解的类顺序的实例化。Spring 启动时,容器中没有任何 bean(当然是有一些内部 bean 的,但是这样说便于我们理解)
下面来理解下 Spring 实例化 Bean 的顺序:
启动时
检测到 Aoo 开始实例化 Aoo 对象
调用 doGetBean 方法实例化 Aoo
调用 getSingleton 方法,试图从三级缓存中依次获取 bean。但是第一次肯定都为空
因为缓存为空,所以程序继续往下走
程序调用 getSingleton 方法创建 Aoo 对象,并且该方法传入一个 lambda 表达式,这个表达式后面是会被放入三级缓存的。
我们来看下 getSingleton 方法
可以看到该方法中试图通过第二个参数,也就是上一步的 lambda 表达式创建 Aoo 对象,并且创建完成后把 Aoo 对象存入一级缓存。 此时一级缓存何时加入的我们清楚了。
接下来我们来看下这个 lambda 表达式,也即是 createBean 方法。而它又调用了 doCreateBean 方法。
第一步:创建一个 Aoo 的空壳对象,此时 Aoo 其中的属性还没有值。
第二步:把工厂存入三级缓存,缓存的 key 就是对象的名称 aoo,而 value 是一个工厂对象的 lambda 表达式,这个工厂对象会返回 Aoo 对象。这个工厂会执行 getEarlyBeanReference 方法,该方法中会完成 AOP 动态代理。需要重点说明一下,从三级缓存中取出的对象每次都是不一样的。因为它是每次代理生成的。
第三步:执行 populateBean 方法。为 Aoo 注入属性,Aoo 只有一个属性 Boo。Spring 在注入 Boo 属性的时候发现容器没有 Boo 对象。
第四步:从这一步就循环到文章的最开始了,接下来我用小写数字表示的步骤表示上文提到的步骤。回到 3 调用 doGetBean 方法实例化 Boo。
第五步:执行 4 调用 getSingleton 方法,试图从三级缓存中依次获取 bean。前面说过第一次肯定都为空,这次是第一次获取 Boo 对象肯定也还是空的。
第六步:依次执行 5,6,7,在 7 中会把 Boo 对象存入三级缓存中。 没错,任何一个 Bean 都会先放到三级缓存中。此时三级缓存中有两个 Bean 了,分别是 Aoo 和 Boo
第七步:在 7 中的 populateBean 方法开始给 Boo 注入属性了,Boo 只有一个属性 Aoo,Spring 在注入 Aoo 属性是会从容器中获取,也就是调用 getBean 方法,此时发现三级缓存中有 Aoo。就会从三级缓存中获取 Aoo 对象并给 Boo 的这个属性赋值。同时也会把 Aoo 对象从三级缓存移动到二级缓存中。此时一级缓存为空、二级缓存中有 Aoo 对象、三级缓存中有 Boo 对象。此时 Aoo、Boo 的状态还都是创建的过程中。
第八步:Boo 的属性已经完成,回到上面的 6 它会把 Boo 对象添加到一级缓存,并从三级缓存中移除(这儿没二级缓存啥事儿嘿嘿嘿)。 此时一级缓存中有一个对象 Boo、二级缓存中有 Aoo 对象、三级缓存为空。
第九步:既然 Boo 都已经在一级缓存当中了,那么接着第三步来说,此时 Aoo 的属性 Boo 也完成了赋值。此时 Aoo 也是一个完整对象了。但它此刻还在二级缓存当中。
第十步:在 7 执行完毕之后,回归到 6 的代码,执行 addSingleton 方法把 Aoo 从二级缓存移动到一级缓存当中。至此,依赖注入完毕。一级缓存中有 Aoo 对象和 Boo 对象。二级、三级缓存为空。
思考:为什么需要三个级别的缓存来解决循环依赖
现在来思考一下为什么一定要是三个级别的缓存呢?我们来删除二级缓存后看这个问题。下面我们就使用只有一级缓存和三级缓存这 2 个缓存来看下循环依赖的问题能不能解决。
还是 Aoo 和 Boo 两个类循环依赖
Spring 启动
从一级缓存和三级缓存中获取 Aoo,缓存中没有,则创建
创建 Aoo 的空壳对象,并把它和工厂对象放入三级缓存中。
对 Aoo 进行属性注入,发现 Boo 即不在一级缓存,也不在三级缓存。只能创建了
创建 Boo 对象
对 Boo 进行属性注入,发现三级缓存中有 Aoo 对象,直接从三级缓存中获取。
Boo 对象属性装配完成,把它从三级缓存移到一级缓存。
Aoo 对象属性装配完成,此时从三级缓存中移到一级缓存。
乍一看没啥问题是不是,其实不是的。问题出在第 6 步,和第 8 步。通过前面的讲解,一定要了解到,三级缓存中每次返回的对象都不一样。所以第 6 步和第 8 步如果都从三级缓存中获取 Aoo 对象, 这两步中的 Aoo 对象不是同一个,Spring 中的 Aoo 对象和 Boo 对象就会使这个样子
总结
首先不是说非要三级缓存机制才能解决循环依赖,一级缓存同样可以解决,把三级缓存代码平铺化就好了嘛,或者使用 JVM 指令,字节码等技术完成循环依赖,但你想一下,那样的话代码的可读性必然很低。所以第一个原因就是使用三级缓存解决循环依赖使得代码可读性非常好。
第二个原因是三级缓存中的工厂,每次 getObject 方法返回的实例不是同一个对象,所以需要二级缓存来缓存一下三级缓存生成的 bean,这样就保证了两个类的属性是环形依赖,不会破坏循环依赖。
作者:念念清晰
链接:https://juejin.cn/post/7200366809407651877
来源:稀土掘金
评论