spring源码阅读笔记09:循环依赖

2022-12-23,,,,

  前面的文章一直在研究Spring创建Bean的整个过程,创建一个bean是一个非常复杂的过程,而其中最难以理解的就是对循环依赖的处理,本文就来研究一下spring是如何处理循环依赖的。

1. 什么是循环依赖

  不管之前是否研究过循环依赖,这里先对这个知识做一点回顾。

  循环依赖就是循环引用,就是两个或者多个bean相互之间的持有对方,比如A引用B,B引用C,C引用A,则它们最终反映为一个环,参考下图:

  了解了什么是循环依赖之后,我们知道这是一种不可避免会出现的情况,那作为Bean容器的Spring又是怎么处理这一问题呢?我们接着往下看。

2. Spring如何处理循环依赖

  Spring容器循环依赖包括构造器循环依赖和setter循环依赖,那Spring容器又是如何解决循环依赖的呢?我们来测试一下,首先我们来定义循环引用类:

public class TestA{
private TestB testB; public void a(){
testB.b();
} public TestB getTestB(){
return testB;
} public void setTestB(TestB testB){
this.testB = testB;
}
} public class TestB{
private TestC testC; public void b(){
testC.c();
} public TestC getTestC(){
return testC;
} public void setTestC(TestC testC){
this.testC = testC;
}
} public class TestC{
private TestA testA; public void c(){
testA.a();
} public TestA getTestA(){
return testA;
} public void setTestA(TestA testA){
this.testA = testA;
}
}

  在Spring中将循环依赖的处理分成了3种情况:

2.1 构造器循环依赖处理

  这表示通过构造器注入构成的循环依赖,此依赖是无法解决的,只能抛出BeanCurrentlyInCreationException异常表示循环依赖。

  比如在创建TestA类时,构造器需要TestB类,那么将去创建TestB,在创建TestB类时又发现需要TestC类,则又去创建TestC,最终在创建TestC时发现又需要TestA,从而形成一个环,没办法创建。

  Spring容器将每一个正在创建的bean标识符放在一个“当前创建bean池”中,bean标识符在创建过程中将一直保持在这个池中,因此如果在创建bean的过程中发现自己已经在“当前创建bean池”里时,则抛出BeanCurrentlyInCreationException异常表示出现了循环依赖;而对于创建完毕的bean将从“当前创建bean池”中清除掉,这个“当前创建bean池”实际上是一个ConcurrentHashMap,即DefaultSingletonBeanRegistry中的singletonsCurrentlyInCreation。

  我们通过一个直观的测试用例来进行分析:

  xml配置如下:

    <bean id = "testA" class = "xxx.xxx">
<constructor-arg index = "0" ref = "testB"/>
</bean>
<bean id = "testB" class = "xxx.xxx">
<constructor-arg index = "0" ref = "testC"/>
</bean>
<bean id = "testC" class = "xxx.xxx">
<constructor-arg index = "0" ref = "testA"/>
</bean>

  创建测试用例:

public static void main(String[] args) {
try{
new ClassPathXmlApplicationContext("beans.xml");
}catch (Exception e){
e.printStackTrace();
}
}

  这个执行过程中会抛出异常BeanCurrentlyInCreationException,通过debug可以快速找到异常抛出的位置在getSingleton()方法中的beforeSingletonCreation():

protected void beforeSingletonCreation(String beanName) {
if (!this.inCreationCheckExclusions.containsKey(beanName) &&
this.singletonsCurrentlyInCreation.put(beanName, Boolean.TRUE) != null) {
throw new BeanCurrentlyInCreationException(beanName);
}
}

  由此可知,Spring在对构造器循环依赖的处理策略上是选择了直接抛异常,而且对循环依赖的判断是发生在加载单例时调用ObjectFactory的getObject()方法实例化bean之前。

2.2 setter循环依赖处理

  这个表示通过setter注入方式构成的循环依赖。对于setter注入造成的循环依赖Spring是通过提前暴露刚完成构造器注入但还未完成其他步骤(如setter注入)的bean来完成的,而且只能解决单例作用域的bean循环代码,我们这里来详细分析一下Spring是如何处理的。

  关于这部分的处理逻辑,在AbstractAutowireCapableBeanFactory的doCreateBean()方法中有一段代码,如下所示:

// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isDebugEnabled()) {
logger.debug("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
// 为避免后期循环依赖,可以在bean初始化完成前将创建实例的ObjectFactory加入工厂
addSingletonFactory(beanName, new ObjectFactory<Object>() {
public Object getObject() throws BeansException {
// 对bean再一次依赖引用,主要应用SmartInstantiationAwareBeanPostProcessor,
// 其中我们熟知的AOP就是在这里将advice动态织入bean中,若没有则直接返回bean,不做任何处理
return getEarlyBeanReference(beanName, mbd, bean);
}
});
}

  这段代码不是很复杂,但是如果是一开始看这段代码的时候不太容易理解其作用,因为仅仅从函数中去理解是很难弄懂其中的含义,这里需要从全局的角度去思考Spring的依赖解决办法才能更好理解。

earlySingletonExposure:从字面的意思理解就是是否提早曝光单例
mbd.isSingleton():是否是单例
this.allowCircularReference:是否允许循环依赖,在AbstractRefreshableApplicationContext中提供了设置函数,可以通过硬编码的方式进行设置或者可以通过自定义命名空间进行配置,硬编码的方式代码如下:

ClassPathXmlApplicationContext bf = new ClassPathXmlApplicationContext("aspectTest.xml");
bf.setAllowBeanDefinitionOverriding(false);

isSingletonCurrentlyInCreation(beanName):该bean是否在创建中。在Spring中,会有一个专门的属性(类DefaultSingletonBeanRegistry中的singletonsCurrentlyInCreation)来记录bean的加载状态,在bean开始创建前会将beanName记录在属性中,在bean创建结束后会将beanName从属性中移除。我们跟随代码一路走来或许对这个属性的记录并没有多少印象,不经会拍脑门问这个状态是在哪里记录的呢?不同scope的记录位置并不一样,我们以singleton为例,在singleton下记录属性的函数是在DefaultSingletonBeanRegistry类的getSingleton(String beanName,ObjectFactory singletonFactory)函数中的beforeSingletonCreation(beanName)和afterSingletonCreation(beanName)中,在这两段函数中分别通过this.singlesCurrentlyInCreation.add(beanName)与this.singlesCurrentlyInCreation.remove(beanName)来进行状态的记录与移除。

  经过上面的分析可以知道变量earlySingletonExposure为是否是单例、是否允许循环依赖、是否对应的bean正在创建这三个条件的综合。当这3个条件都满足时会执行addSingletonFactory操作,那么加入SingletonFactory的作用又是什么呢?

  这里还是用一个最简单的AB循环依赖为例,类A中含有属性类B,而类B中又会含有属性类A,那么初始化beanA的过程如下图所示:

  上图展示了创建beanA的流程,图中我们看到,在创建A的时候首先会记录类A所对应的beanName,并将beanA的创建工厂加入缓存中,而在对A的属性填充也就是调用populate()方法的时候又会再一次的对B进行递归创建。同样的,因为在B中同样存在A属性,因此在实例化B时的populate()方法中又会再次地初始化A,也就是图形的最后,调用getBean(A)。关键就是在这里,在这个getBean()函数中并不是直接去实例化A,而是先去检测缓存中是否有已经创建好的对应bean,或者是否有已经创建好的ObjectFactory,而此时对于A的ObjectFactory我们早已创建好了,所以便不会再去向后执行,而是直接调用ObjectFactory去获取A。

  到这里基本可以理清Spring处理循环依赖的解决办法,这里再从代码层面总结一下:

  在创建bean的过程中,实例化bean结束之后,属性注入之前,有一段这样的代码(代码位置为AbstractAutowireCapableBeanFactory类中的doCreateBean()方法中bean实例化之后):

    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isDebugEnabled()) {
logger.debug("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
addSingletonFactory(beanName, new ObjectFactory<Object>() {
public Object getObject() throws BeansException {
return getEarlyBeanReference(beanName, mbd, bean);
}
});
}

  这段代码前面也说过,主要做的事情是在addSingletonFactory()方法中,即在必要的时候将创建bean的ObjectFactory添加到缓存中。再结合前面的例子来看,在第一次创建beanA时,这里是会将ObjectFactory加入到singletonFactories中,当创建beanB时,在对beanB的属性注入时又会调用getBean()去获取beanA,同样是前面说到过,会先去缓存获取beanA,这时候是可以获取到刚才放到缓存中的ObjectFactory的,这时候就会把实例化好但是还未完成属性注入的beanA找出来注入到beanB中去,这样就解决了循环依赖的问题,需要结合下面的代码细品一下。

protected <T> T doGetBean(
final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
throws BeansException { final String beanName = transformedBeanName(name);
Object bean; // Eagerly check singleton cache for manually registered singletons.
Object sharedInstance = getSingleton(beanName); ...
} protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

2.3 prototype范围的依赖处理

  对于"prototype"作用域的bean,Spring容器并不会对其进行缓存,因此无法提前暴露一个创建中的bean,所以也是通过抛出异常的方式来处理循环依赖,这里仍然是用一个demo来测试一下代码是在哪抛的异常。

  配置文件:

<bean id = "testA" class = "xxx" scope = "prototype">
<property name = "testB" ref = "testB"/>
</bean>
<bean id = "testB" class = "xxx" scope = "prototype">
<property name = "testC" ref = "testC"/>
</bean>
<bean id = "testC" class = "xxx" scope = "prototype">
<property name = "testA" ref = "testA"/>
</bean>

  测试代码:

public static void main(String[] args) {
try{
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
System.out.println(ctx.getBean("testA"));
}catch (Exception e){
e.printStackTrace();
}
}

  同样通过断点我们可以定位异常的抛出位置是在AbstractBeanFactory类的doGetBean方法中,在方法开始获取缓存失败之后(prototype不会加入到缓存中),会首先判断prototype的bean是否已创建,如果是就认为存在循环依赖,抛出BeanCurrentlyInCreationException异常。

if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}

3. 总结

  Spring中对于循环依赖的处理存在3中场景:

构造器循环依赖处理;
setter循环依赖处理;
prototype范围的依赖处理;

  其中对于构造器和prototype范围的循环依赖,Spring是直接抛出异常。而对于单例的setter循环依赖,Spring是通过在bean加载过程中提前将bean的ObjectFactory加入到singletonFactories这个缓存用的map中来解决循环依赖的。

spring源码阅读笔记09:循环依赖的相关教程结束。

《spring源码阅读笔记09:循环依赖.doc》

下载本文的Word格式文档,以方便收藏与打印。