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

Spring是如何解决循环依赖问题的

时间:2023-03-16 21:55:46 科技观察

在关于Spring的采访中,我们经常会被问到一个问题,就是Spring是如何解决循环依赖问题的。这道题算得上是Spring面试的高频题了,因为如果不刻意去研究,相信即使看了源码,面试官也未必能一下子揣摩出其中的玄机。本文主要针对这个问题,从源码的角度讲解其实现原理。一、流程演示关于Springbean的创建,本质上就是对象的创建。既然是对象,读者一定要明白,一个完整的对象由两部分组成:当前对象的实例化和对象属性的实例化。在Spring中,对象的实例化是通过反射来实现的,对象的属性是在对象实例化后以一定的方式设置的。这个过程可以这样理解:理解了这一点,对循环依赖的理解就已经帮助了一大步。这里我们以两个类A和B为例进行说明,下面是A和B的声明:){this.a=a;}}你可以看到这里A和B各自将对方作为自己的全局属性。这里首先要说明的是,Spring是通过ApplicationContext.getBean()方法来实例化bean的。如果要获取的对象依赖于另一个对象,会先创建当前对象,然后递归调用ApplicationContext.getBean()方法获取依赖对象,最后将获取到的对象注入到当前对象中。这里我们以上面先初始化A对象实例为例。首先,Spring尝试通过ApplicationContext.getBean()方法获取A对象的实例。由于Spring容器中没有A对象实例,它会创建一个A对象,然后发现它依赖于B对象,所以它会尝试递归传递ApplicationContext。getBean()方法获取了B对象的实例,但是此时Spring容器中还没有B对象的实例,所以还是先创建了B对象的实例。读者需要注意这个时间点。此时A对象和B对象已经创建并存储在Spring容器中,但是A对象的属性b和B对象的属性a还没有设置。在之前的Spring中创建了B对象后,Spring发现B对象依赖了属性A,所以此时它还是会尝试递归调用ApplicationContext.getBean()方法获取A对象的实例,因为Spring中已经存在A对象的实例,虽然它只是一个半成品(其属性b还没有初始化),但它也是目标bean,所以会返回一个A对象的实例。此时设置了B对象的属性a,然后ApplicationContext.getBean()方法递归返回,即返回B对象的实例,并将该实例设置为A的属性b目的。这时注意A对象的属性b和B对象的属性a已经设置了目标对象的实例。读者可能会疑惑,之前给对象B设置属性a时,这个A类属性还是个半成品。但是需要注意的是,这个A是一个引用,本质上就是一开始实例化的A对象。在上述递归过程的最后,Spring将得到的B对象实例设置为A对象的属性b。这里的A对象其实和实例B中设置的半成品A对象是同一个对象,它的引用地址是一样的,这里的值是为A对象的b属性设置的,其实值为为半成品的a属性设置。下面我们通过一个流程图来解释这个过程:图中的getBean()表示调用Spring的ApplicationContext.getBean()方法,这个方法中的参数代表了我们要获取的目标对象。图中黑色箭头表示一开始方法调用的方向。最后,Spring中缓存的A对象返回后,表示递归调用返回,用绿色箭头表示。从图中我们可以清楚的看出,B对象的a属性就是第三步注入的半成品A对象,A对象的b属性就是第二步注入的成品B对象。这时,半成品A对象就变成了成品A对象,因为它的属性已经设置好了。2.源码解释关于Spring处理循环依赖的方式,我们可以通过上面的流程图很容易理解。需要注意的一点是Spring如何将生成的A对象标记为半成品,以及如何保存A对象。这里的标记工作由Spring使用ApplicationContext的属性SetsingletonsCurrentlyInCreation保存,半成品A对象由Map>singletonFactories保存,其中ObjectFactory是一个工厂对象,可以是通过调用其getObject()方法获取目标对象。AbstractBeanFactory.doGetBean()方法中获取对象的方法如下:protectedTdoGetBean(finalStringname,@NullablefinalClassrequiredType,@NullablefinalObject[]args,booleantypeCheckOnly)throwsBeansException{//尝试获取目标bean通过bean名对象,比如这里的A对象ObjectsharedInstance=getSingleton(beanName);//我们这里的目标对象都是单例if(mbd.isSingleton()){//这里我们尝试创建目标对象,并且第二个参数是一个ObjectFactory类型的对象,这里是用Java8的lamada//表达式写的,只要上面getSingleton()方法的返回值为空,就会调用这里的getSingleton()方法来创建//目标objectsharedInstance=getSingleton(beanName,()->{try{//尝试创建目标对象returncreateBean(beanName,mbd,args);}catch(BeansExceptionex){throwex;}});}return(T)bean;}这里的doGetBean()方法是一个非常关键的方法(其他代码在mi中省略ddle),上面主要有两步,第一步getSingleton()方法的作用是尝试从缓存中获取目标对象,如果没有,则尝试获取半成品目标对象;如果第一步没有得到目标对象的实例,则进入第二步,第二步的getSingleton()方法的作用是尝试创建目标对象,并注入它所依赖的对象的属性。这其实就是这里的主要逻辑。正如我们在上图中所指出的,doGetBean()方法在整个过程中会被调用3次。第一次调用时,会尝试获取A对象实例。这个时候用到第一个getSingleton()方法,由于已经创建的A对象没有成品或者半成品,所以这里获取的是null,然后调用第二个getSingleton()方法创建A对象的实例,然后doGetBean()方法被递归调用,试图将B对象的实例注入到A对象中。这个时候,由于Spring容器中没有B对象的成品或者半成品,所以还是会去到第二个getSingleton()方法,在这个方法中创建B对象的实例,创建是完全的。之后尝试去获取它所依赖的A的实例作为它的属性,所以还是会递归调用doGetBean()方法。这时候需要注意的是,因为已经有了半成品A对象的实例,所以这一次,再次尝试获取A对象的实例时,会用到第一个getSingleton()方法,在这将获得A对象的半成品实例。然后返回实例,注入到B对象的属性a中,此时B对象实例化完成。然后递归返回实例化后的B对象,再将实例注入到A对象中,从而得到一个成品A对象。这里我们可以读到上面第一个getSingleton()方法:@NullableprotectedObjectgetSingleton(StringbeanName,booleanallowEarlyReference){//尝试从缓存中获取成品的目标对象,如果存在则直接返回ObjectsingletonObject=this.singletonObjects.get(beanName);//如果缓存中不存在目标对象,则判断当前对象是否已经在创建过程中。在前面的解释中,在第一次尝试获取A对象的实例后//,A对象会被标记为正在创建,所以最后尝试获取A对象时,这里的if判断会为trueif(singletonObject==null&&isSingletonCurrentlyInCreation(beanName)){synchronized(this.singletonObjects){singletonObject=this.earlySingletonObjects.get(beanName);if(singletonObject==null&&allowEarlyReference){//这里的singletonFactories是一个Map,key是namebean,value是一个ObjectFactory类型的//对象,这里对于A和B,调用它的getObject()方法返回的图是A和B对象的实例,是否是半成品ObjectFactorysingletonFactory=this.singletonFactory.get(beanName);if(singletonFactory!=null){//获取目标对象的实例singletonObject=singletonFactory.getObject();this.earlySingletonObjects.put(beanName,singletonObject);this.singletonFactories.remove(beanName);}}}}returnssingletonObject;}这里会遇到一个问题就是如何实例化A的半成品实例,然后如何把它封装成一个ObjectFactory类型的对象,放到上面的singletonFactories属性里面这个主要是在上面的第二个getSingleton()方法中,最终会传入第二个参数,从而调用createBean()方法,该方法的最终调用委托给了另一个doCreateBean()方法,其中包含如下一段代码:比如这里实例化了A对象和B对象BeanWrapperinstanceWrapper=null;if(mbd.isSingleton()){instanceWrapper=this.factoryBeanInstanceCache.remove(beanName);}if(instanceWrapper==null){instanceWrapper=createBeanInstance(beanName,mbd,args);}//判断Spring是否配置了目标bean的提前暴露,即是否支持半成品bean的提前暴露booleanearlySingletonExposure=(mbd.isSingleton()&&this.allowCircularReferences&&isSingletonCurrentlyInCreation(beanName));if(earlySingletonExposure){//如果支持,会把当前生成的半成品bean放入singletonFactories中,这个singletonFactories//就是使用的singletonFactories属性首先是getSingleton()方法,也就是这里封装了半成品bean。而这里的getEarlyBeanReference()本质上就是第三个参数,直接会放上去,即//目标bean直接返回addSingletonFactory(beanName,()->getEarlyBeanReference(beanName,mbd,bean));}try{//实例初始化完成后,这里是判断当前bean是否依赖其他bean。如果是,//它将递归调用getBean()方法尝试获取目标beanpopulateBean(beanName,mbd,instanceWrapper);}catch(Throwableex){//省略...}returnexposedObject;}此时,Spring整个解决循环依赖问题的实现思路比较清晰。整体流程,读者只需要了解两点:Spring递归获取目标bean及其依赖的bean;Spring在实例化一个bean时,分两步进行,首先实例化目标bean,然后Injectproperties。结合这两点,也就是说,Spring在实例化一个bean的时候,首先递归实例化它所依赖的所有bean,直到一个bean不依赖其他bean,然后返回这个实例,然后反递归设置获取的bean作为每个上层bean的属性。3.小结本文首先通过图文讲解Spring是如何解决循环依赖问题的,然后从源码的角度详细讲解了Spring是如何实现各个bean的组装的。