关于Spring的两三事:再谈三级缓存(下)

人生苦短,不如养狗

作者:闲宇

公众号:Brucebat的伪技术鱼塘

一、前言

  在之前的文章当中我们分析了Spring是如何依赖三级缓存来处理只有一层循环依赖关系的Bean创建过程,但在这个过程中我们还是遗留了一个问题:为什么Spring要使用三级缓存,而不是使用二级缓存?下面我们将借助存在更加复杂的依赖关系的Bean创建过程来去分析推理这个问题的答案。

二、存在代理对象的循环依赖

  其实复杂依赖关系说到底不过以下四种:

  对于不存在代理对象的前两种情况来说,只需要弄清楚之前文章当中分析的流程以及抓住Spring在Bean创建过程中是同步(即按顺序)的这一点,那么还是非常好分析的。这里闲宇就不过多分析,就当做课后作业留给大家思考了[狗头]。

  而对于存在代理对象的后两种情况来说,这两种情况实际上属于一种情况。因为在Spring当中如果没有特殊处理的情况,那么只要类中存在被代理的情况,无论是整个类被代理还是某个方法被代理,那么依赖它的对象注入的一定是它的代理对象而不是原对象。所以不会存在有时是代理对象,有时是原对象的情况。那么我们还是按照上一篇文章的分析思路来分析一下Spring是如何处理存在代理对象的循环依赖,也即Bean A和Bean B均是被代理且相互依赖的情况。同样,这里闲宇依然只给出了流程中的关键方法,具体的代码仍然需要大家自行阅读源码。

  从上面的流程当中我们可以看到,整体流程上和只有纯粹的循环依赖的Bean创建过程并没有太大区别,唯一的区别就在于在这一次的Bean创建过程当中包含了Bean A和Bean B代理对象的创建。在上图中标注了以下两处生成代理对象的时机:

  • Bean A在第二次进入doGetBean方法尝试从getSingleton方法当中获取提前暴露的对象时。此时,Bean A的代理对象是通过放置在三级缓存singletonFactories当中的工厂对象生成的,具体调用了AbstractAutoProxyCreator#getEarlyBeanReference方法。需要注意的是,为了避免重复创建Bean A的代理对象,在这个方法当中使用了缓存earlyBeanReferences来保存提前暴露的代理对象;

  • Bean B在完成属性填充,调用initializeBean方法进行初始化操作时。此时属于正常的生成代理对象的流程,通过源码我们可以发现,实际生成代理对象实际上是在applyBeanPostProcessorsAfterInitialization方法,也即在整个对象构建的最后。注意,这里生成代理对象依然使用的是AbstractAutoProxyCreator类,所以在这里会通过earlyBeanReferences来检查当前Bean是否已经生成过代理对象了。对于首次生成代理对象的Bean B来说,这里的判断其实没有作用,但是对于已经生成了代理对象的Bean A来说则非常重要,通过这里的判断Spring可以避免重复生成Bean A的代理对象

  通过上面的流程我们还可以顺带回顾一下之前文章中提到的一个问题:为什么要在三级缓存中使用工厂对象而不是直接创建一个新的对象?这其中有一部分原因就在于需要生成代理对象。如果我们在填充属性之前就将Bean A的对象提前暴露给了三级缓存,那么在后续Bean B的填充过程中获取的对象Bean A就不是实际应该获取的代理对象而是原始对象,这和预期不符。而如果我们在填充属性之前就将Bean A的代理对象生成出来并放置在三级缓存中,这样一个行为又不符合Spring的依赖处理机制:依赖关系需要发生在原始对象(目标对象)身上而不是代理对象身上,而为了保证代理逻辑的正常工作,原始对象(目标对象)又可以引用所依赖对象的代理对象。提前生成代理对象就会使目标对象无法持有所依赖的Bean,而是让代理对象持有了。正因如此,在闲宇实际debug之后得到了这样一个图:

  Bean A最终生成的代理对象(也就是放置在一级缓存singletonObjects)实际上是不持有所依赖的Bean B,这其实也在一定程度上反映了代理模式的特点:代理对象只是对目标对象的“包装”,实际方法调用时,代理对象依然会委托给目标对象进行执行。而在debug的过程中我们也是可以看到Bean A本身是持有了Bean B的代理对象的。

三、所以,到底是否需要三级缓存?

  在分析完包含代理对象的循环依赖这样复杂的依赖关系之后,我们会发现即使是在这种复杂的情况下依然是可以不使用三级缓存而是使用二级缓存来去处理的。虽然网上会有非常多的相似的解释说提前暴露会有依赖错乱的问题、或者会存在开销问题、或者说不符合延迟创建的设计理念,但实际上这些说法逻辑上都存在着或多或少的问题,无法自洽。比如依赖错乱,在限定为单例且同步创建的条件下,这样一种情况是不会存在的。又比如开销问题,除非你不创建,否则这样一个开销你是永远免不了的。最后设计理念问题,如果真的是延迟创建,为什么Bean A代理对象的创建时机要早于Bean A本身填充、初始化完成的时机。

  如果硬要解释为什么非要搞出一个一级缓存来存储最终用于使用的Bean,闲宇大胆揣测可能是由于当初设计这套方案的大佬多少有点强迫症,一定要创建过程中的临时Bean和最终Bean存储位置区分开,过程中的临时缓存的Bean要全部清除,而最终使用的Bean则要放置在一个单独的地方存储。大概这样处理会显得更加优雅?哈哈哈,个人观点,不喜轻喷~~

四、总结

  花费了两章的时间终于分析完了Spring的三级缓存,虽然到最终也没有真正意义上解决为什么需要三级缓存这样一个问题,但是在学习分析的过程中我们也明晰了不少网络上可能存在谬误的观点。所以学习源码这件事情还是需要自己亲自阅读、分析、运行之后才知道到底对还是不对,毕竟代码是不会骗人。

  最后用一句陆游的话结尾:“纸上得来终觉浅,绝知此事要躬行。”祝大家身体健康,心想事成,早日财富自由~~

p.s. 附视频讲解