spring如何解决循环依赖的
1、开篇
本节课会聊聊spring IOC如何解决循环依赖问题。包括如下内容:
-
什么是循环依赖
-
Spring IoC处理循环依赖的思路
-
处理循环依赖举例
2、什么是循环依赖
Spring IoC中的循环依赖其实就是循环引用,两个或者两个以上的 Bean 互相持有对方,最终形成闭环。如图1所示,如A依赖于B,B依赖于C,C又依赖于A。这样这样一个场景,初始化A的时候需要完成B的初始化,而完成B的初始化又需要完成C的初始化,最后C又依赖于A,如此这般A永远也无法完成初始化的操作。这种对象的相互依赖形成闭环的关系被称作循环依赖。
图1 循环依赖
在Spring IoC的使用场景中有两类循环依赖是无解的:
-
构造器的循环依赖:构造器要调用构造函数new 一个对象出来,而参数又依赖于另一个对象。创建类A依赖于类B,new 的时候去创建类B发现类B不存在就会出错拋出 BeanCurrentlyInCreationException 异常。
-
prototype 原型bean循环依赖:原型bean的初始化过程中不论是通过构造器参数循环依赖还是通过set方法产生的循环依赖也会抛出异常。
然而针对 singleton bean的循环依赖的场景可以通过三级缓存的方式解决。下面就根据该解决方案展开说明。
3、Spring IoC处理循环依赖的思路
在整理Spring IoC处理singleton bean循环依赖的思路之前先来复习一下bean的生命周期,其包括的三个步骤:
-
实例化:执行了bean的构造方法,bean中依赖的对象还未赋值
-
设置属性:给bean中依赖的对象赋值,若被依赖的对象尚未初始化,则先进行该对象的生命周期(递归)。
-
初始化:执行bean的初始化方法,回调方法等。
解决循环依赖的思路就藏在这三个步骤中,在实例化与设置属性两个步骤之间引入缓存机制,将已经创建好实例但是并没有设置属性的bean放到缓存里,缓存中是没有属性设置的实例对象。假设A对象和B对象互相依赖,A对象的创建需要引用到B对象,而B对象的创建也需要A对象。在创建A对象的时候可以将其放入到缓存中,当B对象创建的时候直接从缓存里引用A对象(此时的A对象只完成了实例化,没有进行设置属性的操作,因此不是完成的A对象,我们称之为半成品A对象),当B对象利用这个半成品的A对象完成实例创建以后(三个步骤都完成),再被A对象引用进去,则A对象也完成了创建。
上文提到的缓存在这里做一个解释,我们将其分为三级,每级缓存都起到不同的作用,如下表格所示:
-
一级缓存:用于存放完全初始化好的 bean,也就是完成三个步骤的bean,拿出来的bean是可以直接使用的。
-
二级缓存:存放原始的 bean 对象,此时的对象只进行了实例化但是没有填充属性,也就是我们所说的“半成品对象”,它的建立是用来解决循环依赖的。
-
三级缓存:用来存放 bean 工厂对象,这个工厂对象是用来产生bean对象的实例的。
源码 |
级别 |
描述 |
singletonObjects |
一级缓存 |
用于存放完全初始化好的 bean,从该缓存中取出的 bean 可以直接使用 |
earlySingletonObjects |
二级缓存 |
存放原始的 bean 对象(尚未填充属性),用于解决循环依赖 |
singletonFactories |
三级缓存 |
存放 bean 工厂对象,用于解决循环依赖 |
解决循环依赖的整个过程是:
先从一级缓存里取bean实例,如果没有对应的bean实例,二级缓存里取,如果二级缓存中也没有bean实例,singletonFactories三级缓存里获取。由于三级缓存存放着产生bean实例的工厂类,因此可以通过该工厂类产生bean实例。
这里可以调用工厂类暴露的getObject方法返回早期暴露对象引用,也是我们所说的半成品bean,也可以成为earlySingletonObject。并且将这个半成品bean放到二级缓存里,在三级缓存里删除该bean。什么时候这个半成品填充了属性以后,就被移动到一级缓存中,也就是被作为可以使用的已经完成初始化的实例bean了,处理循环依赖的过程宣告完毕。下面通过一个例子让大家更好理解这个思路。
4、处理循环依赖举例
根据上面的思路,这里假设A 和 B 互相依赖,如图2所示,在A 创建实例的时候使用了getBean方法,通过createBeanInstatnce方法对A进行实例化。此时的A只是被实例化出来了,并没有进行填充属性的操作,然后通过addSingletonFactory的方法将创建A的工厂类添加到三级缓存中。上面的思路中提到了这个放到三级缓存中的工厂类是用来生成bean实例用的。
接着往下,当通过populateBean填充实例A属性的时候发现,A依赖B。此时开始通过getBean方法创建B的实例,依旧通过createBeanInstatnce方法对B进行实例化,也把创建B实例的工厂类通过addSingletonFactory方法添加到三级缓存中。在使用populateBean方法填充B的属性时,发现B依赖A,此时通过getBean方法对A进行实例化。
这个时候就出现循环依赖的情况了,getBean方法先从一级缓存中获取 A 的实例,发现没有,再去二级缓存中找,还是找不到,没有办法只有找三级缓存中的A 实例创建工厂去创建A的实例。在前面的步骤中A 已经将工厂类通过addSingletonFactory方法存放到了三级缓存中,于是调用A的工厂类创造A的实例,并且将其放到二级缓存中返回给B 用来填充B的属性,当B完成属性填充以后产生了B的实例,返回给populateBean(A)使用,此时A获取了B的实例(完成属性填充的B实例)。
所以,A 也可以完成属性填充从而产生A 的初始化以后的实例并且将其放到一级缓存中。由于B之前使用的是A的实例是没有做属性填充的,也就是半成品的A实例,因此此时从一级缓存中获取成品的A实例完成B对象的初始化。
图2 A B 相互循环依赖,如何处理。
5、总结
本节课提出了Spring IoC遇到的循环依赖的问题,并且通过分析bean创建的过程和三级缓存技术,找到了解决singleton bean 循环依赖的办法,然后通过一个简单的循环依赖处理的例子加强对这一思路的理解。