当前位置: 首页 > 科技观察

深入源码分析,Spring是如何解决循环依赖的?

时间:2023-03-16 23:29:32 科技观察

大家好,我是楼仔!之前有粉丝问我楼哥,你的文章怎么不研究源码,我对这个挺感兴趣的。其实我不是很喜欢研究源码,热衷于理论和实操。不过转念一想,我已经写了很多系列文章,也可以发布一系列源码,满足不同爱好者的要求。让我们使用Spring。Spring是如何解决循环依赖的,网上的资料很多,但是感觉写的好的很少,尤其是源码解读方面。我会单独写一篇文章。这篇文章绝对肝!不用BB,放在文章目录下。一、基础知识1.1什么是循环依赖?一个或多个对象之间存在直接或间接的依赖关系,这种依赖关系构成循环调用,有以下三种方法。让我们来看一个简单的演示和基准测试“案例2”。@ServicepublicclassLouzai1{@AutowiredprivateLouzai2louzai2;publicvoidtest1(){}}@ServicepublicclassLouzai2{@AutowiredprivateLouzai1louzai1;publicvoidtest2(){}}这是一个经典的循环依赖,效果很好,后面我们会从源码的角度解读整体的执行过程。1.2三级缓存在解读源码流程之前,必须先了解一下spring内部三级缓存的逻辑,否则后面代码会乱码。一级缓存:singletonObjects,用于保存实例化、注入、初始化的bean实例;二级缓存:earlySingletonObjects,用于保存实例化的bean实例;三级缓存:singletonFactories,用来保存bean的创建工厂,以便后面有机会创建代理对象。这是核心,我们直接看源码:执行逻辑:先从“一级缓存”中查找对象,找到则返回,找不到则查找“二级缓存”;找到“二级缓存”,存在则返回,不存在则返回找到“三级缓存”;寻找“三级缓存”,如果找到了,获取对象,放入“二级缓存”,从“三级缓存”中移除。1.3原理执行过程我将“case2”的执行过程分解为以下3个步骤,是不是和“套娃”类似?整个执行逻辑如下:第一层,先获取A的Bean,没有则准备创建一个,然后将A的代理工厂放入“三级缓存”(这个A实际上是一个半成品,还没有加工完成,注入里面的属性),但是A依赖于B的创建,所以必须先创建B;在第二层,你要创建B,但是你发现B依赖于A,你需要先创建A;在第三层,去创建A,因为在第一层已经创建了A的代理工厂,直接从“三级缓存”中获取A的代理工厂,获取A的代理对象,放入“二级缓存”,清除“三级缓存”;回到第二层,现在有了A的代理对象,完美解决了对A的依赖(这里的A还是个半成品),B初始化成功;回到第一层,现在B初始化成功,完成A对象的属性注入,然后填充A的其他属性,A的其他步骤(包括AOP),完成A的完整初始化功能(这里,A是一个完整的Bean)。将A放入“L1缓存”。为什么要使用三级缓存?先看源码执行过程,后面会给出答案。2.源码解读注意:Spring版本是5.2.15.RELEASE,否则和我的代码不一样!!!以上知识其实网上都有。以下是我们的亮点。让你跟着娄子走一遍代码流程。2.1代码入口这里需要多跑几次,跳过前面的beanName,只看louzai1。2.2第一层进入doGetBean(),getSingleton()没有找到对象,进入创建Bean的逻辑。进入doCreateBean()后,调用addSingletonFactory()。将louzai1的工厂对象插入到三级缓存singletonFactories中。进入populateBean(),执行postProcessProperties(),这里是一个策略模式,找到下图中的策略对象。正式进入策略对应的方法。下面都是获取louzai1的成员对象,然后注入。进入doResolveDependency(),找到louzai1依赖的对象名。Louzai2需要获取louzai2的bean,就是AbstractBeanFactory的方法。正式拿到louzai2的bean。至此,第一层玩偶基本结束,因为louzai1依赖louzai2,现在进入第二层玩偶。2.3第二层获取louzai2的bean,从doGetBean()到doResolveDependency(),和第一层的逻辑完全一样,找到louzai2依赖的对象名louzai1。前面的流程全部省略,直接进入doResolveDependency()。正式拿到louzai1的bean。至此,第二层娃娃就结束了,因为louzai2依赖louzai1,所以我们进入第三层娃娃。2.4第三层获取louzai1的bean。在第一层和第二层,我们每次都是从getSingleton()中获取对象,但是因为louzai1和louzai2的三级缓存之前没有初始化,所以获取到的对象是空的。击中关键!击中关键!!击中关键!!!在第三层,由于三级缓存中有louzai1的数据,所以这里使用三级缓存中的工厂为louzai1创建一个代理对象,并塞入二级缓存中。这里获取到louzai1的代理对象,解决louzai2的依赖,返回第二层。2.5回到二楼回到二楼后,louzai2的初始化就完成了。这就是结局?二级缓存中的数据什么时候交给一级呢?别着急,看这里,记住在doGetBean()中,我们会通过createBean()创建一个louzai2的bean,当louzai2的bean创建成功后,我们会执行getSingleton(),它会处理louzai2的结果.当我们进入getSingleton()时,我们会看到下面的方法。这里是louzai2处理一级缓存和二级缓存的逻辑,清除二级缓存放入一级缓存。2.6返回第一层同2.5,louzai1初始化完成后,会清空louzai1的二级缓存,对象放入一级缓存。至此,所有流程结束,我们返回louzai1的对象。3、原理深入解读3.1为什么要有三级缓存?这是一道非常经典的面试题。我已经告诉你详细的执行过程,包括源码解读,但是没有告诉你为什么要用三级缓存?这就是重点!敲黑板!!!再说说“一级缓存”的作用。变量名为singletonObjects,结构为Map,是一个单例池。将初始化的对象放入其中,供其他线程使用。如果没有一级缓存,程序就无法保证Spring的单例特性。先放“二级缓存”,我们直接来看“三级缓存”的作用。变量名为singletonFactories,结构为Map>。“级别缓存”的作用其实就是一个用来存储对象的代理工厂。那么这个对象的代理工厂有什么作用呢?让我先给出答案。它的主要功能是存放半成品单例bean。目的是“打破循环”。也许你还不明白。让我在这里稍微解释一下。.让我们回到文章开头的例子。在创建A对象时,实例化的A对象会存放在“三级缓存”中。这个A其实是个半成品,因为A的依赖属性B的注入还没有完成,所以后面在初始化B的时候,这个时候B又想找A。这时候他需要从“三级缓存”中取出半成品A(这里的描述其实并不完全准确,因为不是直接取的。为了让大家更容易理解,我将首先如此描述),打破循环。那我再问一个问题,为什么“三级缓存”不直接存放半成品A,而是一个代理工厂呢?答案是因为AOP。在解释这个问题之前,我们先看一下这个代理工厂的源码,让大家有一个更清晰的认识。直接找到创建A对象时将实例化的A对象存入“三级缓存”的代码,直接使用前面两张截图。接下来我们主要看如何获取这个对象工厂,进入getEarlyBeanReference()方法。最后一张图太重要了,我们知道这个对象工厂的作用:如果A有AOP,就创建一个代理对象;如果A没有AOP,则返回原始对象。那么“二级缓存”的作用就很明确了,它用来存放对象工厂生成的对象,这个对象可能是原始对象,也可能是代理对象。再问一个问题,为什么要这样设计?杀不死二级缓存?让我们继续阅读。3.2我可以杀死二级缓存吗?@ServicepublicclassA{@AutowiredprivateBb;@Autowired私有Cc;publicvoidtest1(){}}@ServicepublicclassB{@AutowiredprivateAa;publicvoidtest2(){}}@ServicepublicclassC{@AutowiredprivateAa;publicvoidtest3(){}}根据上面的套娃逻辑,A需要找B和C,但是B需要找A,C也需要找A。如果A需要进行AOP,因为代理对象每次生成不同的对象,如果去掉二级缓存,就只有一级和三级缓存可用:当B找到A时,直接通过三级缓存对象A1的工厂生成代理对象。当C找到A时,直接通过三级缓存中工厂的代理对象生成对象A2。你看到问题了吗?你通过A的工厂代理对象生成了两个不同的对象A1和A2,所以为了避免这种问题,我们设置了一个二级缓存,保存A1,直接从二级缓存获取,没有需要生成一个新的代理对象。所以“二级缓存”的目的是为了避免因为AOP而创建多个对象,存放半成品的AOP单例bean。如果没有AOP,我们其实只需要1级和3级缓存就可以满足要求。4.写到最后,我们来回顾一下3级缓存的作用:一级缓存:为“Spring的单例属性”而生,是一个单例池,用来存放已经初始化好的单例Bean;二级缓存:为“解决AOP”而生,存放半成品的AOP单例bean;三级缓存:为“打破循环”而生,存放生成半成品单例bean的工厂方法。如果你能看懂我上面提到的这三点,恭喜你对Spring的循环依赖理解的非常透彻!关于循环依赖其实还有更多的知识。由于篇幅关系,就不写了。这篇文章的重点一方面是告诉你循环依赖的核心原理,另一方面是为了让你自己调试代码,运行运行过程很有趣。有的同学可能会问楼哥,你以前是不是经常看源码,调试了很久这个过程?我以前没见过多少开源代码。这个过程需要2.5小时的初步理论知识,然后debug了4.5个小时,基本上一切都通过了。最难的是三层娃娃,有点绕。这里也简单说说我阅读源码的心得:需要掌握基本的设计模式;在阅读源码之前,最好先找一些理论知识阅读;学会看英文评论,不会就用百度翻译;调试的时候一定要克制自己,不要纠缠于无用的细节,这才是最重要的。最难的是第4步,因为很多同学看Spring源码,每看一个方法就想多研究一下,所以很容易卡进去。这就需要学习有节制,有大局观,能够区分核心逻辑在哪里,至于怎么区分,可以先在网上找些资料,没有的话只能看代码了。