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

spring:如何解决循环依赖?

时间:2023-03-12 10:03:01 科技观察

1。是从同事抛出的一个问题说起的。最近项目组的一个同事遇到一个问题问我的意见,顿时引起了我的兴趣,因为我也是第一次遇到这个问题。平时以为自己对springcycle依赖问题理解的很好,直到遇到了这个和下面的问题,刷新了自己的理解。我们看一下当时出错的代码片段:@ServicepublicclassTestService1{@AutowiredprivateTestService2testService2;@Asyncpublicvoidtest1(){}}@ServicepublicclassTestService2{@AutowiredprivateTestService1testService1;publicvoidtest2(){}}这两段中定义了两个Service类代码:TestService1和TestService2,TestService1注入一个TestService2实例,TestService2注入一个TestService1实例,构成循环依赖。但是,这不是普通的循环依赖,因为在TestService1的test1方法中添加了@Async注解。大家猜猜程序启动后运行结果会怎样?org.springframework.beans.factory.BeanCurrentlyInCreationException:Errorcreatingbeanwithname'testService1':Beanwithname'testService1'hasbeeninjectedintootherbeans[testService2]initsrawversionaspartofacircularreference,buthaseventuallybeenwrapped.Thismeansthatsaidotherbeansdonotusethefinalversionofthebean.Thisisoftentheresultofover-eagertypematching-considerusing'getBeanNamesOfType'withthe'例如,allowEagerInit'标志关闭。报告错误。..原因是循环依赖。“不科学,spring不是号称可以解决循环依赖问题吗?怎么还会出现?”如果稍微调整一下上面的代码:@ServicepublicclassTestService1{@AutowiredprivateTestService2testService2;publicvoidtest1(){}}把@放在TestService1的test1方法上如果去掉Async注解,TestService1和TestService2都需要注入对方的实例,这也构成循环依赖。但是重启项目发现可以正常运行了。为什么是这样?带着这两个问题,让我们开始spring循环依赖的探索之旅。2.什么是循环依赖?循环依赖:说白了就是一个或多个对象实例之间存在着直接或间接的依赖关系,这种依赖关系构成了循环调用。第一种情况:自依赖直接依赖第二种情况:两个对象之间的直接依赖第三种情况:多个对象之间的间接依赖前两种情况的直接循环依赖比较直观,很好识别,但是第三种间接由于业务代码调用层次较深,循环依赖有时不易识别。3.循环依赖的N种场景spring中的循环依赖主要有以下几种场景:单例setter注入应该是spring中使用最多的注入方式,代码如下:@ServicepublicclassTestService1{@AutowiredprivateTestService2testService2;publicvoidtest1(){}}@ServicepublicclassTestService2{@AutowiredprivateTestService1testService1;publicvoidtest2(){}}这是一个经典的循环依赖,但是可以正常运行,得益于spring的内部机制,所以我们根本察觉不到它有问题,因为spring默默地为我们解决。spring内部有三级缓存:singletonObjects一级缓存,用于保存bean实例early的实例化、注入、初始化SingletonObjects二级缓存,用于保存实例化的bean实例singletonFactories三级缓存,用于保存bean的创建factories,以便以后的扩展有机会创建代理对象。下面用一张图来告诉大家spring是如何解决循环依赖的:图1细心的朋友可能会发现二级缓存在这种场景下并不是很有效。那么问题来了,为什么要使用二级缓存呢?试想一下,如果出现以下情况,我们该怎么办?@ServicepublicclassTestService1{@AutowiredprivateTestService2testService2;@AutowiredprivateTestService3testService3;publicvoidtest1(){}}@ServicepublicclassTestService2{@AutowiredprivateTestService1testService1;publicvoidtest2(){}}@ServicepublicclassTestService3{@AutowiredprivateTestService1testService1;publicvoidtest3(){}}TestService1依赖TestService2和TestService3,TestService2依赖TestService1,TestService3也依赖TestService1。按照上图的流程,可以将TestService1注入到TestService2中,从三级缓存中获取TestService1的实例。假设不使用二级缓存,将TestService1注入TestService3的流程如图:图2TestService1注入TestService3需要从三级缓存中获取实例,三级缓存不存储真正的实例对象,而是ObjectFactory对象。说白了就是两次从三级缓存中获取ObjectFactory对象,每次通过它创建的实例对象可能都不一样。这不是问题吗?为了解决这个问题,spring引入了二级缓存。在上图1中,TestService1对象的实例已经添加到二级缓存中,当TestService1注入TestService3时,只需要从二级缓存中获取对象即可。图3还有一个问题,为什么要把ObjectFactory对象加入三级缓存?不能直接保存实例对象吗?答:不能,因为如果要增强加入三级缓存的实例对象,直接使用实例对象是行不通的。对于这种情况,spring是怎么做到的呢?答案就在AbstractAutowireCapableBeanFactory类的doCreateBean方法的代码中:定义了一个匿名内部类,通过getEarlyBeanReference方法获取代理对象。其实底层是通过AbstractAutoProxyCreator类的getEarlyBeanReference来生成代理对象的。.多casesetter注入的注入方式偶尔会出现,尤其是在多线程场景下。具体代码如下:@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)@ServicepublicclassTestService1{@AutowiredprivateTestService2testService2;publicvoidtest1(){}}@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)@ServicepublicclassTestService2{@AutowiredprivateTestService1testService1;publicvoidtest2(){}}很多人说在这种情况,spring容器在启动的时候会报错,其实是错误的。很负责任的告诉大家,程序可以正常启动了。为什么?其实答案在AbstractApplicationContext类的refresh方法中就已经告诉了。它将调用finishBeanFactoryInitialization方法。该方法的作用是在spring容器启动时提前初始化一些bean。在该方法内部,调用了preInstantiateSingletons方法。可以清楚的看到红色标记:只有非抽象类、单例类、非懒加载类可以提前初始化。但是多实例,也就是SCOPE_PROTOTYPE类型的类,不是单例,不会提前初始化bean,所以程序可以正常启动。怎么让他提前初始化bean呢?定义一个单例类,注入TestService1@ServicepublicclassTestService3{@AutowiredprivateTestService1testService1;}进去,重启程序,执行结果:Requestedbeanscurrentlyincreation:Istheranunresolvablecircularreference?果然出现了循环依赖。注意:这种循环依赖问题无法解决,因为它没有使用缓存,每次都生成一个新的对象。构造函数注入这种注入方式现在已经很少见了,但是我们还是要了解一下,看下面的代码:是否存在无法解析的循环引用?存在循环依赖,为什么?从图中的流程可以看出,构造函数注入只是增加了三级缓存,并没有使用缓存,所以无法解决循环依赖问题。单例代理对象setter注入其实是一种常见的注入方式。比如在平时使用:@Async注解的场景下,会通过AOP自动生成代理对象。我同事的问题也是如此。@ServicepublicclassTestService1{@AutowiredprivateTestService2testService2;@Asyncpublicvoidtest1(){}}@ServicepublicclassTestService2{@AutowiredprivateTestService1testService1;publicvoidtest2(){}}前面了解到程序启动会报错,存在循环依赖:org.springframework.beans.factory.BeanCurrentlyInCreationanException:Erwname'testService1':Beanwithname'testService1'hasbeeninjectedintootherbeans[testService2]initsrawversionaspartofacircularreference,buthaseventuallybeenwrapped.Thismeansthatsaidotherbeansdonotusethefinalversionofthebean.Thisisoftentheresultofover-eagertypematching-considerusing'getBeanNamesOfType'withthe'allowEagerInit'flagturnedoff,forexample.为什么会循环依赖呢?答案就在下面这个张图中:说白了,bean初始化后,还有一步检查:二级缓存是否等于原始对象。由于和前面的流程无关,所以在前面的流程图中省略了,不过这里是重点,重点说一下:同事的问题刚好转到这段代码,发现二级缓存和原来的对象不相等,因此抛出循环依赖异常。如果此时将TestService1的名称改为:TestService6,其他都不变。@ServicepublicclassTestService6{@AutowiredprivateTestService2testService2;@Asyncpublicvoidtest1(){}}再次重启程序,神奇的搞定了。什么?为什么?这从springbean加载顺序开始。spring默认按照文件全路径递归查找,按路径+文件名排序,第一个先加载。所以TestService1先于TestService2加载,更改文件名后,TestService2先于TestService6加载。为什么可以在TestService6之前加载TestService2?答案如下图:这种情况下testService6中的二级缓存其实是空的,不需要和原来的对象进行判断,所以不会抛出循环依赖。DependsOn循环依赖还有一个比较特殊的场景。比如我们需要在实例化BeanA之前先实例化BeanB,这时候可以使用@DependsOn注解。@DependsOn(value="testService2")@ServicepublicclassTestService1{@AutowiredprivateTestService2testService2;publicvoidtest1(){}}@DependsOn(value="testService1")@ServicepublicclassTestService2{@AutowiredprivateTestService1testService1;publicvoidtest2(){}}程序启动后,执行结果:Circulardepends-onrelationshipbetween'testService2'and'testService1'在这个例子中,如果TestService1和TestService2不加@DependsOn注解是没有问题的,但是加了这个注解会造成循环依赖问题。为什么又是这个?答案就在AbstractBeanFactory类的doGetBean方法的这段代码中:它会检查dependsOn的实例是否存在循环依赖,如果存在循环依赖则抛出异常。4.如何解决循环依赖问题?如果项目中存在循环依赖问题,说明是spring默认无法解决的循环依赖。取决于项目的打印日志,属于哪种循环依赖。目前包括以下几种情况:通过生成代理对象产生循环依赖此类循环依赖问题的解决方案有很多,主要有:使用@Lazy注解,使用@DependsOn注解延迟加载,指定加载顺序,修改文件命名,并更改循环依赖类加载顺序使用@DependsOn生成的循环依赖,找到@DependsOn注解循环依赖的地方,强制其解决没有循环依赖的问题。循环依赖的多个实例这种类型的循环依赖问题可以通过将bean更改为单例来解决。构造函数循环依赖这种循环依赖问题可以通过使用@Lazy注解来解决。