Spring循环依赖
1.Bean实例化、循环依赖
1.1 什么是循环依赖?
如下有 A、B、C
三个类,可以看到发生了循环依赖:彼此互相依赖,导致各自都需要对方的依赖,形成依赖闭环。
但是我们会发现field属性注入、setter方法注入的循环依赖:即使发生了循环依赖,我们依然可以启动,使用并没有任何影响。
这种方式是我们最为常用的依赖注入方式,Spring会解决field属性注入、setter方法注入
的循环依赖。但是无法解决构造器注入的循环依赖。
Spring IoC
容器会在运行时检测到构造函数注入循环引用,并抛出
BeanCurrentlyInCreationException
。从而提醒你避免循环依赖。所以要避免构造函数注入,可以使用
setter 注入替代。根据官方文档说明,Spring 会自动解决基于 setter
注入的循环依赖。当然在咱们工作中现在都使用 @Autowired 注解来注入属性。 @Autowired
是通过反射进行赋值。
依赖注入的方式:
- 注解注入:@Autowire和@Resource:这种注解可以直接解决循环依赖问题,不需要额外处理
- 构造方法器注入:构造方法注入需要使用@Lazy 注解来作用于循环依赖的属性
- setter注入:setter注入也可以直接解决循环依赖问题,不需要额外处理
Spring只是解决了单例模式下属性依赖的循环问题;Spring为了解决单例的循环依赖问题,使用了三级缓存。
2.Spring的循环依赖
现在有循环依赖代码如下:
1 |
|
无论先创建ServiceA还是ServiceB时,都会发生循环依赖!
1 |
|
自己依赖自己其实也是循环依赖的一种
2.1 实例化与初始化
在介绍spring如何解决循环依赖前,我们必须先了解一个完整的对象其实包含两部分:当前对象实例化和对象属性的实例化:
类的实例化:
是指创建一个对象的过程。这个过程中会在堆中开辟内存,将一些非静态的方法,变量存放在里面。在程序执行的过程中,可以创建多个对象,既多次实例化。每次实例化都会开辟一块新的内存。(就是调用构造函数,生成一个对象。)
类的初始化:
是完成程序执行前的准备工作。在这个阶段,*静态的*(变量,方法,代码块)会被执行。同时在会开辟一块存储空间用来存放静态的数据。初始化只在类加载的时候执行一次(为变量设置值)
这个过程可以按照如下方式进行理解:
在Spring中,对象的实例化是通过反射实现的,而对象的属性则是在对象实例化之后通过一定的方式设置的。
2.2 解决循环依赖
在spring中,通过三级缓存来解决循环依赖的问题
1 |
|
「singletonObjects」:缓存某个 beanName 对应的经过了完整生命周期的bean;
「earlySingletonObjects」:缓存提前拿原始对象进行了 AOP 之后得到的代理对象,原始对象还没有进行属性注入和后续的 BeanPostProcesso r等生命周期;
「singletonFactories」:缓存的是一个 ObjectFactory ,主要用来去生成原始对象进行了 AOP之后得到的「代理对象」,在每个 Bean 的生成过程中,都会提前暴露一个工厂,这个工厂可能用到,也可能用不到,如果没有出现循环依赖依赖本 bean,那么这个工厂无用,本 bean 按照自己的生命周期执行,执行完后直接把本 bean 放入 singletonObjects 中即可,如果出现了循环依赖依赖了本 bean,则另外那个 bean 执行 ObjectFactory 提交得到一个 AOP 之后的代理对象(如果有 AOP 的话,如果无需 AOP ,则直接得到一个原始对象
spring容器的实现从根源上来看的话是通过BeanFactory
实现的,但是BeanFactory
只是一个接口类,真正作为一个可以独立使用的容器还是通过DeafultListableBeanFactory
实现的,DefaultListableBeanFactory
结构图解:
3.源码角度
3.1 DefaultListableBeanFactory的preInstantiateSingletons方法
1 |
|
最终都会调用getBean
方法,触发容器对Bean实例化和依赖注入过程。getBean
方法是在Beanfactory
中的一个接口,由AbstractBeanFactory
实现,执行doGetBean
方法,在Spring中,凡是以do开头的方法一般都是细节上的逻辑处理,也就是具体的实现代码。
1 |
|
在spring中,想要使用对象最终都会调用
AbstractBeanFactory
的getBean()
方法,进而执行doGetBean()
方法。在spring中,凡是以do开头的方法一般都是细节上的逻辑实现,所以在
doGetBean()
中执行了DefaultSingletonBeanRegistry
的getSingleton()
方法。
3.2 AbstractBeanFactory的doGetBean方法
1 |
|
3.3 DefaultSingletonBeanRegistry的getSingleton(String beanName, boolean allowEarlyReference)
1 |
|
这个方法做了这么一件事,
先到singletonObjects中获取,如果有表示实例化已经完成;
否则到earlySingletonObjects获取,如果有表示已经有bean,且存在循环依赖,将此bean作为属性注入了
否则到singletonFactories获取,如果存在循环依赖,且此属性是第一次被其他bean作为属性
3.4 DefaultSingletonBeanRegistry的getSingleton(String beanName, ObjectFactory<?> singletonFactory)
上面的代码主要就是执行一些条件判断,重要的是单例类的getSingleton()方法:
1 |
|
其中:
1 |
|
在这个getSingleton中,还会再去从一级缓存中获取一次,如果有则直接返回,没有则在beforeSingletonCreation
将要创建的对象存入
set 集合中。同理,还有afterSingletonCreation
,如下:
1 |
|
由此可知:创建的时候存在singletonsCurrentlyInCreation队列里,创建成功则被移除。
最后回到在3.4第一哥代码块中执行下列代码:
1 |
|
方法中进行Bean的创建,AbstractAutowireCapableBeanFactory中的createBean方法。
3.5 AbstractAutowireCapableBeanFactory的createBean方法
1 |
|
3.6 AbstractAutowireCapableBeanFactory的doCreateBean
主要是进行了一些判断处理,重要方法就是其中的doCreateBean,里面是执行创建对象的细节.
1 |
|
其中的addSingletonFactory方法:判断是否是循环引用,是的话执行下面方法;
1 |
|
消除此Bean在二级缓存里的缓存信息,将其包装成singletonFactory实例往三级缓存里添加,判断是否是循环引用,是的话需要添加到三级缓存中。
这里有一个重点就是Spring解决循环依赖的真相就在这一段源码中:在这里beanFactory被put进了singletonFactories,此时的bean只是完成了初始化构造的bean,还没有进行set或者注解注入的bean,是bean的一个中间状态,但是已经能被识别出来了,所以Spring此时将这个对象提前曝光出来让大家认识、使用。
在3.3 节中,getSingleton(String beanName, boolean allowEarlyReference) 方法中,如果
1 |
|
则:
1 |
|
4.Spring循环依赖解决
以上面为例子,A在DefaultListableBeanFactory
的preInstantiateSingletons
中,调用AbstractBeanFactory
的getBean方法,然后调用doGetBean方法,进入方法的时候会调用getSingleton方法,会进行如下判断:
- 先到singletonObjects中获取,如果有表示实例化已经完成;
- 到earlySingletonObjects获取,如果没有则找三级缓存,如果有表示已经有bean,且存在循环依赖,返回对象。
- 到singletonFactories获取,如果有则返回对象,且将对象存入二级缓存,三级缓存将此对象清除。
此处A在这三个缓存中都获取不到,然后就创建对象,在创建的时候还会再去从一级缓存中获取一次,如果有则直接返回,没有则先在beforeSingletonCreation
将要创建的对象存入
set
集合中,因此是不可重复的,然后执行AbstractAutowireCapableBeanFactory中的createBean方法中的doCreateBean方法,所有的对象都封装成BeanWrapper对象,通过BeanWrapper的.getWrappedInstance()得到具体的实例,然后再根据条件判断
1 |
|
是否从二级缓存移除,存入三级缓存,此处A已经存入三级缓存。
B的过程和 A 的一样,也是创建了三级缓存,然后去创建 C,同理,C也进入三级缓存,这时候三级缓存里面有它们三个的 singletonFactory 。C 也调用到 doGetBean 方法去获取 A.
填充属性ServiceA的时候,这时候能够从三级缓存中拿到半成品的ObjectFactory,拿到ObjectFactory对象后,调用ObjectFactory.getObject()方法最终会调用getEarlyBeanReference()方法,getEarlyBeanReference这个方法主要逻辑大概描述下如果bean被AOP切面代理则返回的是beanProxy对象,如果未被代理则返回的是原bean实例。
不过这次C调用到 Object sharedInstance = getSingleton(beanName); 的时候, A 已经存在了。这次调用虽然没有从一级缓存 (singletonObjects) 中获取到 A,但是 A 在创建中,所以进入判断在这里执行完之后,A 从三级缓存升级到二级缓存。
这里获取到的是 A 的引用,注意 A 这时候还没创建完成,只是引用。所以这里赋值的是 A 的引用。
填充属性的时候,spring会提前将已经实例化的bean通过ObjectFactory半成品暴露出去,为什么称为半成品是因为这时候的bean对象实例化,但是未进行属性填充,是一个不完整的bean实例对象,这时我们会发现能够拿到bean实例(属性未填充),然后从三级缓存移除,放到二级缓存earlySingletonObjects中,而此时C注入的是一个半成品的实例A对象,不过随着C初始化完成后,A会继续进行后续的初始化操作,最终C会注入的是一个完整的A实例,因为在内存中它们是同一个对象。
到这里 C 就创建完了。如上所示,C 创建完成之后,会执行addSingleton方法,然后会将 C 添加到一级缓存和已注册列表中,同时从二级三级缓存中删除 C。继续执行 B 和 A 的属性赋值以及后续的初始化流程。最后 B 和 A 都进入一级缓存。至此,循环依赖解决完毕。
对于“prototype”作用域bean, Spring 容器无法完成依赖注入,因为Spring 容器不进行缓存“prototype”作用域的bean ,因此无法提前暴露一个创建中的bean 。
5.半成品"Bean"
半成品的bean可能不太理解,我们可以看下面一段代码:
1 |
|
main方法:
1 |
|
如上,打印的地址是一样的,第一次打印的就是所谓的"半成品"。虽然它此时的属性没有赋值,但是它是可以使用(被别人引用)的半成品。在最终使用的时候,它经过一系列执行,注入的就是成熟的bean对象。
6.为什么要使用三级缓存,仅仅使用二级缓存行吗?
使用 earlySingletonObjects 和 singletonObjects 两级缓存,一个存放早期对象,一个存放初始化完成后的对象,也能实现同样的功能,singletonFactories 好像显得有些多此一举。其实不是的,对于普通对象,确实只要返回刚创建完的早期对象就好了,二级缓存已经可以解决循环依赖。
但如果A 的原始对象进行了 AOP 产生了一个代理对象,此时就会出现,对于 A 而言,如果仅有二级缓存,它的 Bean 对象其实应该是 AOP 之后的代理对象,而此时的B 的 a 属性对应的并不是 AOP 之后的代理对象,这就产生了冲突:B 依赖的 A 和最终的 A 不是同一个对象,因此才出现三级缓存, singletonFactories(三级缓存) 根据 beanName 得到一个 ObjectFactory ,然后执行 ObjectFactory ,也就是执行 getEarlyBeanReference 方法,此时会得到一个 A 原始对象经过 AOP 之后的代理对象,然后把该代理对象放入 earlySingletonObjects 中。后续为B所用,此时得到的A都是代理对象。
链接:https://segmentfault.com/a/1190000023647227
总结:
Spring 的设计原则是在 Bean 初始化完成之后才为其创建代理,如果只有二级缓存,这样违背了Spring设计原则。Spring结合AOP跟Bean的生命周期,是在Bean创建完全之后----通过AnnotationAwareAspectJAutoProxyCreator这个---后置处理器来完成的,在这个后置处理的postProcessAfterInitialization方法中对初始化后的Bean完成AOP代理。如果出现了循环依赖,那没有办法,只有给Bean先创建代理,但是没有出现循环依赖的情况下,设计之初就是让Bean在生命周期的最后一步完成代理而不是在实例化后就立马完成代理。 Spring 需要三级缓存的目的是为了在没有循环依赖的情况下,延迟代理对象的创建,使 Bean 的创建符合 Spring 的设计原则。
7.为什么Spring无法解决构造器的循环依赖?
在spring中,springIOC容器在运行时检测到构造函数注入循环依赖则直接抛出异常,因此要避免构造器的循环依赖而使用setter注入。spring也会自动解决基于setter注入的循环依赖。
Spring解决循环依赖依靠的是Bean的“中间态”这个概念,而这个中间态指的是已经实例化,但还没初始化的状态。而构造器是完成实例化的,所以构造器的循环依赖Spring并没有解决,bean都是需要可以先被实例化才可以的,所以这也就是为什么构造器依赖可能会失败的原因。假如A构造器依赖B,因为实例化A需要先调用A的构造函数,发现依赖B,那么需要去初始化B,但是B也依赖A,不管B是通过构造器注入还是setter注入,此时由于A没有被实例化,没有放入三级缓存,所以B无法被初始化,所以spring会直接报错。反之,如果A通过setter注入的话,那么则可以通过构造函数先实例化,放入缓存,然后再填充属性,这样的话不管B是通过setter还是构造器注入A,都能在缓存中获取到,于是可以初始化。
解决办法:
Spring构造器注入循环依赖的解决方案是@Lazy,其基本思路是:对于强依赖的对象,一开始并不注入对象本身,而是注入其代理对象,以便顺利完成实例的构造,形成一个完整的对象,这样与其它应用层对象就不会形成互相依赖的关系;当需要调用真实对象的方法时,通过TargetSource去拿到真实的对象[DefaultListableBeanFactory#doResolveDependency],然后通过反射完成调用
8.什么情况下循环依赖可以被处理?
首先要明确一点,Spring解决循环依赖是有前置条件的
- 出现循环依赖的Bean必须要是单例
- 依赖注入的方式不能全是构造器注入的方式(很多博客上说,只能解决setter方法的循环依赖,这是错误的)
其中第一点应该很好理解,第二点:不能全是构造器注入是什么意思呢?我们还是用代码说话
1 |
|
在上面的例子中,A中注入B的方式是通过构造器,B中注入A的方式也是通过构造器,这个时候循环依赖是无法被解决,如果你的项目中有两个这样相互依赖的Bean,在启动时就会报出以下错误:
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
如下四种情况的循环依赖测试
依赖情况 | 依赖注入方式 | 循环依赖是否被解决 |
---|---|---|
AB相互依赖(循环依赖) | 均采用setter方法注入 | 是 |
AB相互依赖(循环依赖) | 均采用属性自动注入 | 是 |
AB相互依赖(循环依赖) | 均采用构造器注入 | 否 |
AB相互依赖(循环依赖) | A中注入B的方式为setter方法,B中注入A的方式为构造器 | 是 |
AB相互依赖(循环依赖) | B中注入A的方式为setter方法,A中注入B的方式为构造器 | 否 |
不是只有在setter方法注入的情况下循环依赖才能被解决,即使存在构造器注入的场景下,循环依赖依然被可以被正常处理掉。
所以日常所说的只解决setter注入方式是错误的。spring解决循环依赖是不能全是构造器注入的方式。
为什么在下表中的第四种情况的循环依赖能被解决,而第五种情况不能被解决呢?
是因为Spring在创建Bean时默认会根据自然排序进行创建,所以A会先于B进行创建,A会首先暴露出来。然后才回去注入B,B再注入A的时候,就可以通过三级缓存拿到A了。此时的A是一个实例化的一个中间状态的Bean.
反之:如果使用如果A先使用构造器,在注入的时候,他会去找B,B再注入A,可此时A并没有暴露(因为还没有实例化成功),也就失败了。
9.Read more
:lollipop::https://developer.aliyun.com/article/766880
:lollipop::https://juejin.cn/post/6985337310472568839
:lollipop::https://segmentfault.com/a/1190000023647227
:lollipop::https://blog.csdn.net/weixin_48777366/article/details/123645686
:lollipop::https://pdai.tech/md/spring/spring-x-framework-ioc-source-3.html
:lollipop::https://www.zhihu.com/question/438247718/answer/1730527725
:lollipop::https://juejin.cn/post/6844903843596107790
博客说明
文章所涉及的资料来自互联网整理和个人总结,意在于个人学习和经验汇总,不用于任何的商业用途。如有侵权,请联系本人删除。谢谢!