面试官:连Spring三级缓存都答不好,自己走还是我送你?

阅读 60

2022-04-26

最近一直在网上看各种的spring循环依赖的文章,感觉总是缺了那么一点点味道,没有直指spring循环依赖的核心,于是我便整理了一篇文章,从基础开始带领大家,彻底了解Spring循环依赖 。

首先我们看看什么是循环依赖?

所谓的循环依赖是指,A 依赖 B,B 又依赖 A,它们之间形成了循环依赖。或者是 A 依赖 B,B 依赖 C,C 又依赖A。它们之间的依赖关系如下:

Demo:        

如何解决循环依赖?

哪三级缓存?

以 BeanA 和 BeanB 两个类相互依赖为例

创建原始 bean 对象 

也就是所说的纯洁态Bean

假设 beanA 先被创建,创建后的原始对象为BeanA@1234,上面代码中的 bean 变量指向就是这个对象。

暴露早期引用

 该方法用于把早期对象包装成一个ObjectFactory 暴露到三级缓存中 用于将解决循环依赖...

beanA 指向的原始对象创建好后,就开始把指向原始对象的引用通过 ObjectFactory 暴露出去。getEarlyBeanReference 方法的第三个参数 bean 指向的正是 createBeanInstance 方法创建出原始 bean 对象 BeanA@1234。

解析依赖

还没有进行属性装配,自动注入的属性都是null

初始化好的Bean

populateBean 用于向 beanA 这个原始对象中填充属性,当它检测到 beanA 依赖于 beanB 时,会首先去实例化 beanB。beanB 在此方法处也会解析自己的依赖,当它检测到 beanA 这个依赖,于是调用 BeanFactroy.getBean("beanA") 这个方法,从容 器中获取 beanA。

获取早期引用

接着上面的步骤讲:

 1.populateBean 调用 BeanFactroy.getBean("beanA") 以获取 beanB 的依赖。 

2.getBean("beanB") 会先调用 getSingleton("beanA"),尝试从缓存中获取 beanA。此时由于 beanA 还没完全实例化好

 3.于是 this.singletonObjects.get("beanA") 返回 null。

 4.接着 this.earlySingletonObjects.get("beanA") 也返回空,因为 beanA 早期引用还没放入到这个缓存中。

 5.最后调用 singletonFactory.getObject() 返回 singletonObject,此时 singletonObject != null。singletonObject 指向 BeanA@1234,也就是 createBeanInstance 创建的原始对象。此时 beanB 获取到了这个原始对象的引用,beanB 就能顺利完成实例 化。beanB 完成实例化后,beanA 就能获取到 beanB 所指向的实例,beanA 随之也完成了实例化工作。由于 beanB.beanA 和 beanA 指向的是同一个对象 BeanA@1234,所以 beanB 中的 beanA 此时也处于可用状态了。

以上的过程对应下面的流程图:

为何需要三级缓存,而不是两级缓存?

为什么需要二级缓存? 

二级缓存只要是为了分离成熟Bean和纯净Bean(未注入属性)的存放, 防止多线程中在Bean还未创建完成时读取到的Bean时不完整的。所 以也是为了保证我们getBean是完整最终的Bean,不会出现不完整的情况。为什么需要三级缓存?我们都知道Bean的aop动态代理创建时在初始化之后,但是循环依赖的Bean如果使用了AOP。那无法等到解决完循环依赖再创建动态代 理, 因为这个时候已经注入属性。所以如果循环依赖的Bean使用了aop. 需要提前创建aop。

但是需要思考的是动态代理在哪创建?? 在实例化后直接创建?但是我们正常的Bean是在初始化创建啊。 所以可以加个判断如果是 循环依赖就实例化后调用,没有循环依赖就正常在初始化后调用。

怎么判断当前创建的bean是不是循环依赖?根据二级缓存判断?有就是循环依赖? 那这个判断怎么加?加载实例化后面行吗?且看:

实例化后 

 if(二级缓存有说明是循环依赖?){

 二级缓存=创建动态代理覆盖(判断当前bean是否被二级缓存命中); 

 } 

这样写可以吗?肯定不行啊, 因为实例化后始终会放入二级缓存中。所以这样写不管是不是循环依赖都会在实例化后创建动态代理。创建本身的时候没法判断自己是不是循环依赖,, 只有在B 引用A (不同bean的引用直接)下才能判断是不是循环依赖(比如B引用A,A 正在创建,那说明是循环依赖), 所以判断要卸载getSingleton中。

 假如A是proxy. 

 A创建Bean ‐‐>注入属性B‐‐>getBean(B)‐‐>创建B‐‐>注入属性A‐‐‐‐>getSingleton("a")之后写如下代码 

 if(二级缓存有说明是循环依赖?){ 

 // 在这里创建AOP代理吗? 

 二级缓存=创建动态代理覆盖(判断当前bean是否被二级缓存命中,没命中依然返回二级缓存); 

 } 

这里创建行吗?行!那要三级缓存干吗?如果是这样的依赖呢?

A被循环依赖了俩次或N次, 那要创建N次aop吗然后在里面判断有没有命中? 什么?你说根据二级缓存的对象判断?如果是动态代 理就不重复创建?逻辑太复杂了。毫无扩展性也太过于耦合。。如果希望循环依赖给程序员扩展呢?那程序员不一定就返回proxy 。

假如A是proxy.

除了这样没有更好的解决方案吗? 

能不能让二级缓存存储的bean无脑返回就行了(不管是普通的还是代理的,让这块逻辑分离?

 可 以。 

增加三级缓存,二级缓存先啥也不存。三级缓存 存一个函数接口, 动态代理还是普通bean的逻辑调用BeanPostProcessor 都放在这里面。只要调用了就存在二级缓存,无 脑返回就行。 大大减少业务逻辑复杂度 为什么Spring不能解决构造器的循环依赖?

 从流程图应该不难看出来,在Bean调用构造器实例化之前,一二三级缓存并没有Bean的任何相关信息,在 实例化之后才放入三级缓存中,因此当getBean的时候缓存并没有命中,这样就抛出了循环依赖的异常了。 

为什么多例Bean不能解决循环依赖?

我们自己手写了解决循环依赖的代码,可以看到,核心是利用一个map,来解决这个问题的,这个map就相当于缓存。为什么可以这么做,因为我们的bean是单例的,而且是字段注入(setter注入)的,单例意味着只需要创建一次对象,后面就可以从缓存 中取出来,字段注入,意味着我们无需调用构造方法进行注入。如果是原型bean,那么就意味着每次都要去创建对象,无法利用缓存;如果是构造方法注入,那么就意味着需要调用构造方法注入,也无法利用缓存。

循环依赖可以关闭吗?可以,

Spring提供了这个功能,我们需要这么写:

至此,我们能够非常清晰的了解到循环依赖了,同时,也能轻而易举的招架住面试官的各种提问了。

学废了吗?记得点个收藏+关注,资料持续更新中,目前全部都是免费送给大家,如果有需要,尽管拿走,点击名片即可领取

精彩评论(0)

0 0 举报