从源码角度聊一聊Spring中的循环依赖以及为何要使用三级缓存

114 阅读23分钟

一、开篇

相信Spring的循环依赖在面试中属于经典问题了,前段时间由于领导太忙,安排我去面试,这也是我个人比较爱问的问题之一。下面将从源码角度来分析这个问题,也希望能够帮助大家以后在面试中回答这个问题时对答如流,同时也是给自己记一份笔记,后续要用时方便查阅,文章可能会有点长,主要想照顾一下没怎么了解过Spring源码的童鞋,有基础的大佬可以快速的挑重点看或略过。那么在聊这个问题之前呢,我想先把Spring启动的大致流程讲一下,提前做一些铺垫,让大家有个大概的印象便于后面讲这个问题,建议对Spring启动流程不太熟的童鞋仔细看一下,尤其是旁边黄色的备注内容,下面是我画的一张简图: Spring启动流程.png 可以看到不管我们用xml还是用纯注解启动,最终都会调到AbstractApplicationContext的refresh()方法,这个方法的重要程度不言而喻,往下就是一个模板方法模式,这13个方法是我们学习Spring源码必须要掌握的。当然版本不同,可能看到的内容略微有点出入,因为Spring也是在做迭代的,我这里的版本是5.0.2.RELEASE,但是不管怎么迭代,这个整体流程是不变的。

关于如何学习Spring源码,讲一些个人心得,对于刚开始学习Spring源码的初学者来说,Spring的源码体系很庞大,第一点就是一定要学会抓大放小,先了解其整体流程,千万不要一直跟进到一些细枝末节里去,很容易就走不出来了,不知道自己跟到哪了,然后就产生畏难情绪直接不看了。只有当我们掌握了其整体流程后,才知道这一步走到哪了,一些不重要的代码完全可以跳过,Spring源代码命名是很规范的,很多时候看名字就能大概猜出这个方法在干什么,在我们熟悉其整体流程后,再深究里面的细节,这样会事半功倍。第二点看它给的注释信息,注释信息都写得很详细,对我们理解有帮助,然后就是初始化一些类的时候,关注其父类的初始化,因为很多时候一些属性都是在初始化父类时就赋值了,不看父类的话都不知道这个属性到底什么时候有值的。第三点关注类的继承体系,Spring中的类继承体系确实很复杂,但是一些关键类的继承体系一定要关注。当然,也不要求大家对每一行代码都要弄懂,说实话本人也不是每一行都懂,哈哈哈哈哈。以上就是我的一些个人小心得,整体启动流程大致如上图,画得比较简单,有不正确的地方还请各位大佬指正。尽管我们不学Spring源码,平时也能写好业务代码,但是对于工作3年以上的开发来说,我们每天都在用Spring,学习Spring源码有益无害,其中所涉及的设计模式以及很多优秀的开发思想都是值得我们借鉴的,要做到知其然而知其所以然。而且很多优秀的开源框架都是基于Spring扩展实现的,例如我们耳熟能详的SpringMVC,SpringBoot,SpringCloud等,当然说这个并不是让我们去重复造轮子,而是说当公司需要进行二次开发自研一些框架时,学习过Spring源码的优势就体现出来了。刚开始学习的时候晕车很正常,晕车就多看几遍,一定不要放弃,入门后就会发现柳暗花明又一村,推荐通过官方文档、视频、优秀书籍、博客文章等进行学习。最后重中之重,实践是检验真理的唯一标准。

二、前置知识

接下来我们言归正传,要聊循环依赖就要找到创建Bean的入口,也就是图中倒数第二步finishBeanFactoryInitialization()。其中的关键逻辑在图中右下角的紫色部分,也就是说创建一个Spring的Bean对象要经历如下几个过程:getBean()→doGetBean()→createBean()→doCreateBean()→createBeanInstance()→addSingletonFactory()→populateBean()→initializeBean()。真正干活的方法就是doCreateBean(),Spring中很多以do开头命名的方法,也就是真正干活的地方,这个方法里面主要又分为三步,第一步createBeanInstance()创建Bean实例,此阶段只是在堆中分配了一块内存空间,属性都是默认值,我们可以得到这个Bean的引用地址,然后addSingletonFactory()把该Bean包装为ObjectFactory存入到第三级缓存中,注意这里只是存进去,并没有发生调用。第二步populateBean()填充Bean属性值,@Autowired依赖注入就是在这一步进行的。第三步initializeBean()初始化Bean,其中会回调Aware方法,执行BeanPostProcessor的postProcessBeforeInitialization(),接着调用初始化方法,执行完初始化方法后,执行BeanPostProcessor的postProcessAfterInitialization()。我们可以看到在Bean的创建过程中,Bean的实例化和初始化是分开的,中间穿插着一个提前暴露到三级缓存和填充属性值阶段,那我们说的循环依赖也就是在填充属性这一步产生的,因为要构成循环依赖,无非就是A的属性值要注入B,B的属性值要注入A,形成一个环,3个或3个以上的Bean也是一样的,只要依赖关系形成一个环就称为循环依赖。

提到Spring的循环依赖,还有一个绕不开的就是三级缓存,当然只是我们为了区分习以为常这么叫而已,Spring的作者应该是不会这么叫的,其实三级缓存就是3个Map集合,只是里面存放的东西不一样而已,Spring源码中用到了很多Map集合做缓存,我们平时开发中也可以借鉴。那么这3级缓存是什么呢,我们来贴一下代码: 一级缓存中存放的是经历完整生命周期的单例Bean对象,二级缓存中存放的是早期Bean对象,也就是仅实例化还未进行populateBean()属性填充的Bean对象,此时属性值都是默认值,三级缓存中存放的是ObjectFactory,那么什么是ObjectFactory呢,我们可以打开它看一下,其实就是个函数式接口,最终会调用它的getObject()方法返回一个Bean实例: image.png 讲完三级缓存,还有一个Spring循环依赖绕不开的方法就是getSingleton(): 先从一级缓存中取,如果为空再从二级缓存中取,如果二级缓存也没有,且允许循环引用时就从三级缓存中取,调用getObject()返回Bean对象,我们在上面分析过,createBeanInstance()实例化Bean后,就会把该Bean包装为ObjectFactory存入到第三级缓存中,从二级缓存中删除,对应代码如下: 那我们接着来看下getEarlyBeanReference()到底做了什么,直接上代码: 从注释信息我们可以知道该方法会获得一个指定Bean的早期引用,通常用于解决循环依赖。如果我们忽略掉if分支中的代码块,这里就是直接把刚实例化完的Bean对象返回,中间代码是给后置处理器返回代理对象的,下面会讲到,也就是说我们如果是普通Bean对象之间发生循环依赖,当从第三级缓存取值时,会直接返回原来Bean对象。

三、源码分析

了解完前置知识后,我们开始聊Spring是如何解决循环依赖问题的以及为何要使用三级缓存,二级缓存不行吗?这里先讲结论,Spring是通过提前曝光机制和缓存来解决循环依赖问题的。我们现在假设A依赖B,B依赖A,这里使用xml配置文件方式进行演示: 如上定义了两个Bean,一个是boyfriend,一个是girlfriend,boyfriend依赖girlfriend,girlfriend依赖boyfriend。我们知道加载完配置文件后会将BeanName存到beanDefinitionNames中,在第一张Spring启动流程图中有写到,而Spring中创建Bean就是通过遍历beanNames来挨个创建的,这里我把if (isFactoryBean(beanName))分支中的代码删掉了,看起来精简一点,因为我们这里是非FactoryBean,对应代码如下: image.png 我们从debug控制台可以看到执行到这里时,就两个Bean,而且是按xml配置文件中编写顺序加载的,因此会先创建boyfriend,就会执行我们的getBean(),然后执行doGetBean(),执行到doGetBean()我们会看到调用了一个getSingleton(),最终会调用我们上面讲的那个getSingleton()方法,从三级缓存中逐级去取,而且第二个参数allowEarlyReference传的true,记住这里传的是true,才会从一二三级缓存分别去取,后面有个地方传的是false,只会从一二级缓存取,对应代码如下: image.png 因为我们现在是第一次创建,取出来肯定为空,所以会走下面的分支,进入到createBean(),然后doCreateBean(),接着createBeanInstance()创建Bean实例,调用addSingletonFactory(beanName, ()→getEarlyBeanReference(beanName, mbd, bean)) 将创建的Bean包装为ObjectFactory提前曝光,存入到第三级缓存中,可以翻看上面的代码,至于为什么一定会走,那肯定是因为earlySingletonExposure为true,因为我们目前正在创建的Bean是单例的且正在被创建中且Spring中this.allowCircularReferences允许循环引用默认为true。接下来就是调用populateBean()为boyfriend填充属性,发现它需要一个女朋友girlfriend,但我们此时又还没有创建好的girlfriend,怎么办呢?Spring此时就会再次调用getBean(),没有那就创建嘛,具体的细节就不赘述了,里面跟进去也挺深的,大家先记住即可,感兴趣的可以自行研究。那么此时我们就进入到创建girlfriend环节,也是经历上面一系列处理过程,最终来到populateBean()为girlfriend填充属性,发现它需要一个男朋友boyfriend,也是调用getBean()到doGetBean(),再接着从缓存中取boyfriend,而且上面也说了传进去的allowEarlyReference为true,所以会尝试分别从一二三级缓存中取。此时一二级缓存中肯定是没有的,但是别忘了我们创建完boyfriend后将创建的boyfriend包装为ObjectFactory提前曝光,存入到了三级缓存中,而存进去的ObjectFactory是一个lambda表达式getEarlyBeanReference(beanName, mbd, bean),其中第3个参数就是我们创建好的boyfriend对象,因为此时boyfriend就是一个普通的Bean对象,所以我们这次肯定能从第3级缓存中取到boyfriend,上面有分析过如果是普通对象不需要代理的话直接返回原来刚实例化完存进来的对象。尽管此时的boyfriend是一个仅实例化完尚未给属性赋值的Bean,但是我们有它的引用地址就够了,单例Bean的引用地址不会再发生改变,此时将三级缓存中boyfriend删除,放入二级缓存中,详见上面的getSingleton()方法,最后将boyfriend返回,一路返回到给girlfriend属性赋值那里去,因为我们是从这里进来的嘛。此时girlfriend属性填充完后,继续执行initializeBean()初始化,一个经历完整生命周期的girlfriend对象就诞生了,然后一路返回调用addSingleton(): 将完整的girlfriend放入一级缓存中,从二三级缓存中将girlfriend删除。经历九九八十一难,终于创建好了一个Bean也就是girlfriend,那么此时代码应该返回到哪去呢,各位应该还记得我们是在给boyfriend填充属性时才去创建girlfriend调用getBean()的,所以最终会把创建好的girlfriend返回,这时boyfriend的属性就能用完整的girlfriend赋值了,接着boyfriend继续执行initializeBean()初始化,一个完整的boyfriend对象也创建好了,同时也会将完整的boyfriend放入一级缓存中,从二三级缓存中将boyfriend删除。然后我们循环体的第一次循环就结束了,第一次循环我们是创建boyfriend进去的,但因为boyfriend中依赖girlfriend,所以一并将girlfriend也创建好了,那么当第二次循环创建girlfriend时,就可以直接从一级缓存拿到了,因为我们创建完girlfriend已经存入到一级缓存中。至此,我们的boyfriend和girlfriend都创建成功了,循环依赖问题也解决了,我们看一下效果: 这里我们上一张流程图帮助大家理解,文字结合流程图一起看会更容易理解一点: 从结果来看循环依赖问题确实是解决了,也确实是通过提前曝光机制和缓存来解决循环依赖问题的。假如没有提前曝光机制和缓存,创建完boyfriend后,给boyfriend填充属性发现需要一个girlfriend,此时开始创建girlfriend,给girlfriend填充属性时发现需要一个boyfriend,此时boyfriend和girlfriend同时都在创建中,而又没有提前曝光机制和缓存,循环依赖问题就无解了,只能抛异常结束程序。那么我们在知道了Spring是通过提前曝光机制和缓存来解决循环依赖问题后,我们回到最开始的问题,为什么一定要用三级缓存呢,二级缓存不行吗?这里告诉大家,从我们这个案例来看,确实用二级缓存就可以解决循环依赖问题了,三级缓存直接返回原来实例化的Bean毫无意义。试想一下,我们在实例化boyfriend后,将该Bean存入二级缓存中,填充属性时依然创建girlfriend,给girlfriend填充属性时,也调用getBean()获取boyfriend,此时直接从二级缓存中就能取到boyfriend给到girlfriend赋值,然后girlfriend初始化,返回一个完整的girlfriend给到boyfriend填充属性,接着boyfriend执行initializeBean()初始化,最终也能创建两个完整的Bean对象解决循环依赖问题,这里大家可以自行下载Spring源码搭建一个可运行环境,尝试修改源代码进行验证,本人尝试后是可以的,当然场景只适用于普通Bean对象间的循环依赖。看到这里,我们也能解释为什么Spring解决不了构造器的循环依赖问题,因为提前曝光发生在实例化后,如果在实例化阶段通过构造器实例化Bean时就发生循环依赖,每一个Bean都无法实例化完进行提前曝光给其他Bean提前引用,也就无法解决循环依赖。原型Bean就更不用说了,每次都是new一个新对象,根本没有缓存的说法,想要解决循环依赖,提前曝光机制和缓存二者缺一不可,所以原型Bean发生循环依赖时最终会在循环依赖检测时抛异常结束程序。

那么现在压力来到了我们这边,为什么Spring需要再增加一个缓存来解决循环依赖问题呢?这是因为我们的案例过于简单,就是两个普通的Bean对象而已。这里先直接告诉大家答案,别忘了,Spring除了IoC控制反转之外,还有一个很重要的功能AOP,这个第三级缓存就是为了解决AOP动态代理的。怎么证实这个答案呢,接着翻代码,假设我们现在的boyfriend是一个需要被动态代理的对象,当我们在给girlfriend填充属性通过调用getBean()最终调用getSingleton()时,会从三级缓存中取出ObjectFactory也就是变量singletonFactory,忘记的童鞋往上翻下代码哈,此时它肯定不为null嘛,因为我们实例化后就往三级缓存中存了。接着就会调用它的getObject()方法,大家还记得存进去的ObjectFactory其实是一个lambda表达式吧,忘记的也可以往上找,所以当调用getObject()时,就会回调getEarlyBeanReference(beanName, mbd, bean)这个方法,这里怕大家忘记我们再贴一下代码: 当我们开启AOP功能时,一定会满足第一个if条件进入到循环,也会满足第二个if条件。有一个很重要的类AbstractAutoProxyCreator,相信了解过AOP的童鞋都会知道,它的继承关系是自身实现了SmartInstantiationAwareBeanPostProcessor,而SmartInstantiationAwareBPP又继承了InstantiationAwareBeanPostProcessor,所以这里都是满足条件的。那么AbstractAutoProxyCreator是什么时候创建的呢?其实是在解析AOP相关标签时加载的一种Spring需要的以internel开头的内部Bean,也就是Spring启动流程图中提到的loadBeanDefinitions()解析AOP标签时会去加载这个Bean的定义信息,加载到BeanDefinition后,后续就会注册AbstractAutoProxyCreator到容器中。了解完这个类是如何注册到IOC容器中后,我们继续回到代码,if条件都满足后,接下来就会调用getEarlyBeanReference(),这里我们可以从图中看到有三种实现: 其中下面两种实现都是直接返回原Bean对象,而且我们这里是讲AOP,所以直接来到和AOP相关的AbstractAutoProxyCreator#getEarlyBeanReference(): image.png 我们会看到将当前Bean缓存到了earlyProxyReferences中,接着调用了一个非常重要的方法wrapIfNecessary(),相信有了解过AOP的铁子们或多或少都听过这个方法,我们继续跟进去看一下,会看到一段很核心的逻辑: 从注释我们也能看到叫如果我们有通知的话就创建一个代理类,也就是里面的createProxy()方法,Spring的动态代理底层是基于Jdk动态代理和Cglib动态代理实现的,这里感兴趣的小伙伴可以自行研究,不是本文讨论的重点。到此为止,我们发现如果发生循环依赖的同时又需要AOP动态代理时,在从三级缓存获取Bean对象的时候,如果这个Bean需要被代理,就会产生一个代理类返回,所以为什么Spring需要三级缓存来解决循环依赖问题,就是因为Spring中的AOP动态代理。讲到这里,相信细心的童鞋会发现如果仅需要动态代理而没有循环依赖的话,就不会从三级缓存中提前引用从而产生代理类,那么此时代理类从哪里产生呢?我们平时在开发中,Controller层注入一个Service,而Service需要事务的话,这个Service一定会被Spring动态代理,所以注入的肯定是一个代理类,因为开启事务、提交事务、回滚事务、关闭事务这些增强逻辑都在代理类中,只是这个代理类中会存着一个target对象引用,也就是我们的原始的Service,也称为被代理对象或目标对象,当我们调用代理类中的方法时,就会调用target目标对象中的方法。回到刚才的问题,假设此时代码中没有循环依赖,无论是先创建controller还是service,都是顺序创建的,创建完也会放入三级缓存中,但是由于不存在循环依赖,就不会在填充属性时发生提前引用从而从三级缓存中产生代理类。那么Service的代理类到底在哪里产生的呢,此时就不得不提一嘴Spring的AOP代理时机了,一般是说两个,个人认为可以分为三个,大家理解就好,不用过于执着于数字:

1、自定义TargetSource,此时会在Bean的实例化前通过自定义代理逻辑返回代理类,后续的操作都不会走了。

2、没有循环依赖也就是正常情况下,因为没有循环依赖,也就是说在populateBean()填充属性阶段不会产生代理类了,那么只剩下最后一个initializeBean()初始化阶段。正如我们所想,Spring就是在initializeBean()中调用BeanPostProcessor的postProcessAfterInitialization()时产生的代理类。

3、就是我们刚刚在循环依赖结合Spring的AOP动态代理场景下,通过调用提前暴露到三级缓存中的ObjectFactory的getObject()方法回调getEarlyBeanReference()产生代理类。

关于第1点,因为实际用得比较少,了解即可,第3点我们已经讲过了,现在就是要关注第2点。我们直接上代码: 好巧不巧的是我们的AbstractAutoProxyCreator也是BeanPostProcessor接口的实现类之一,其中也重写了postProcessAfterInitialization方法: 不知道大家有没有一种似曾相识的感觉,这个wrapIfNecessary()好眼熟啊,是的,我们在前面讲循环依赖调用AbstractAutoProxyCreator#getEarlyBeanReference()方法时已经见过它了,跟进去里面会有一个很核心的方法createProxy(),也就是在这里产生代理类并返回的,大家可以往上翻下代码,这里就不贴出来了。

再抛一个问题,我们开始创建的是普通Bean对象,如果需要代理就会创建代理对象,那么这个代理对象是什么时候替换掉原始对象返回并添加到一级缓存中的呢?回到我们刚才的例子,boyfriend和girlfriend循环依赖并且boyfriend需要被动态代理,那么在给girlfriend填充属性获取boyfriend时会通过提前曝光到三级缓存中的ObjectFactory拿到boyfriend的代理类proxy-boyfriend,为了和原始boyfriend区分,我们叫它proxy-boyfriend。此时girlfriend填充属性注入的就是proxy-boyfriend,然后girlfriend完成初始化并将完整的girlfriend返回到给boyfriend填充属性,接着boyfriend执行initializeBean()初始化,因为我们上面讲过initializeBean()里面也是AOP产生代理类的一个时机点,那么此时boyfriend执行完initializeBean()应该返回代理类还是原始的boyfriend呢?答案就在我们上面分析的AbstractAutoProxyCreator的postProcessAfterInitialization()中,因为其他BeanPostProcessor接口的实现类都不会产生代理类,要么是直接返回原Bean要么是做了一些额外操作后返回原Bean。这里就不带大家看了,可以自行挨个点进去看一下。这里直接贴一下AbstractAutoProxyCreator中的代码吧: 这里的if判断,如果earlyProxyReferences不包含当前Bean的话才会进行AOP动态代理,那么我们这里的案例肯定是包含的,因为我们当前案例中存在循环依赖,在给girlfriend赋值时会提前引用boyfriend从三级缓存中通过ObjectFactoty的getObject()回调getEarlyBeanReference()产生代理类,同时把当前Bean也就是boyfriend缓存到earlyProxyReferences中,忘记的童鞋往上找一下讲AbstractAutoProxyCreator#getEarlyBeanReference()的地方。正因为存在循环依赖发生了提前引用,所以此时earlyProxyReferences是包含当前boyfriend的为true,取反后为false,直接跳过AOP代理返回当前boyfriend。也就是说即使boyfriend执行完初始化后还是返回原来的boyfriend,在这里并不会返回代理类。那我们继续往下找,把doCreateBean()中的代码精简一下贴出来: 可以看到,在执行populateBean()和initializeBean()之前将当前Bean也就是我们这里的boyfriend赋值给了一个相当于临时变量的exposedObject,在执行initializeBean()也是传的exposedObject,我们刚刚分析到此时boyfriend执行完initializeBean()返回的还是原始的boyfriend。继续往下走会看到一个if判断,这个条件是为true的,前面有讲,接着调用一个getSingleton(beanName, false),注意此时传的第二个入参是false,前面也有提到过要注意这个入参,所以此时就是相当于从一二级缓存取。这里我们的boyfriend因循环依赖在给girlfriend填充属性需要被提前引用从第三级缓存中产生了代理类,从而将代理类放入了二级缓存,从三级缓存中删除。忘记的童鞋可以往上找getSingleton()方法。因此此时是可以从二级缓存中拿到代理类proxy-boyfriend并赋值给earlySingletonReference的,因earlySingletonReference不为空,所以会接着判断exposedObject是否和当前Bean引用地址相等,很明显是相等的,上面分析过,此时的Bean和exposedObject都指向原始的boyfriend,接下来会将earlySingletonReference也就是boyfriend的代理类proxy-boyfriend赋值给exposedObject并最终返回注册到IOC容器一级缓存中,因此就是在这里完成了代理类替换原始Bean的操作。如果是那种没有循环依赖的场景下,Bean都是顺序创建的,就不会发生提前引用从三级缓存到二级缓存那一步,这里从一二级缓存中取出的earlySingletonReference为空,也就走不到后续的判断。

四、总结

最后总结一下,我们今天把Spring是如何解决循环依赖问题以及为何需要三级缓存来解决都过了一遍,如果仅仅是普通的Bean对象,其实用二级缓存就足以解决循环依赖问题,但是Spring中还有一个很重要的AOP动态代理功能,因此Spring作者在设计之初,就是利用提前曝光机制和三级缓存来解决循环依赖问题。相信把文中提到的这些问题都弄懂的话,应对Spring循环依赖这个问题应该是得心应手了。如果有不懂的童鞋,也不用慌,可以多看几遍或者直接私聊我,以上仅为个人观点,如有不正确的地方,还请各位大佬指正,在此表示感谢。有需要Spring启动流程ProcessOn文件的在公众号内回复Spring循环依赖,拿到后可自行修改。

写在最后,文章篇幅可能会有点臃肿,首先是全文不仅是讲Spring循环依赖问题,还引申出了其他几个相关的问题,本可以分篇写的,图省事以及趁热打铁直接写一起了。其次是为了照顾没有Spring源码基础的童鞋,很多地方写得很啰嗦,生怕写出来的东西读者看不懂,可能这也是很多新手作者的通病吧,希望各位大佬多多海涵。原创不易,如果这篇文章对您有所帮助,帮忙点个关注、点赞和在看,朋友圈随缘,毕竟是私人空间,公众号在下方,求关注,后续有文章都能第一时间看到,您的支持是我坚持写作最大的动力。求一键三连:关注、点赞、在看。如需转载请注明出处,谢谢。

公众号.jpg