Spring源码分析:Spring的循环依赖分析

栏目: Java · 发布时间: 5年前

引言

  1. 基于Spring5+
  2. 什么是循环依赖?
  3. 循环依赖有几种?
  4. Spring可以解决哪几种,为什么不能解决这几种?
  5. Spring是如何判断存在循环依赖的?

什么是循环依赖?

什么是循环依赖?我们都知道Spring最大的作用就是来替我们管理Bean的,当然也包括Bean的创建以及整个生命周期,但是有这么一种情况,假设有三个类A、B、C需要交给Spring来管理,但A实例的创建需要先有B实例,而B实例的创建需要先有C实例,C实例的创建需要先有A实例,这样三个类就自然形成了一个环状结构,如果用代码来表示,如下:

public class TestA {
    TestB testB;
    get;
    set;
}

public class TestB {
    TestC testC;
    get;
    set;
}

public class TestC {
    TestA testA;
    get;
    set;
}

这样,三个类就彼此形成了一个环状,那么Spring是如何来处理这样的状况呢?

循环依赖有几种?

有三种情况:

  1. 基于构造方法的循环依赖
  2. 基于setter构造的循环依赖(网上也叫field属性依赖)
  3. 基于prototype范围的依赖

Spring可以解决哪些循环依赖,为什么?

首先说一下结论:除了第二种Spring可以帮我们解决,其它两种都不能解决。我们知道Spring为我们完全实例化好一个Bean一定会经过一下三步:

  1. createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象。
  2. populateBean:填充属性,这一步主要是多bean的依赖属性进行填充。
  3. initializeBean:调用默认的或者自定义的init方法。

循环依赖的产生定会发生在步骤1和2中,因为1是利用构造方法,2是利用属性赋值。

基于构造方法的循环依赖

先说结论基于构造器的循环依赖Spring是无法解决的,是因为没有加入提前曝光的集合中,加入集合的条件是已经创建了Bean的包装对象,而构造注入的时候,并没有完成对象的创建,下面会有代码说明。

测试用例:

xml文件:

<bean id="testA" class="com.nmys.story.springCore.loop_dependency.loop01.LoopA">
    <constructor-arg index="0" ref="testB"/>
</bean>

<bean id="testB" class="com.nmys.story.springCore.loop_dependency.loop01.LoopB">
    <constructor-arg index="0" ref="testC"/>
</bean>

<bean id="testC" class="com.nmys.story.springCore.loop_dependency.loop01.LoopC">
    <constructor-arg index="0" ref="testA"/>
</bean>

测试类:

/**
 * description:测试通过有参构造方式注入产生的循环依赖问题
 * @author 70KG
 * @date 2018/12/21
 */
public class Test02 {

    @Test
    public void m1() {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test02.xml");
    }

}

分析上面代码:

  1. Spring容器创建testA的Bean实例,首先去"当前创建Bean池",查找是否当前Bean正在创建,如果没发现,则继续准备其需要的构造器参数testB,并将testA标识符放到"当前创建Bean池"。
  2. Spring容器创建testB的Bean实例,首先去"当前创建Bean池",查找是否当前Bean正在创建,如果没发现,则继续准备其需要的构造器参数testC,并将testB标识符放到"当前创建Bean池"。
  3. Spring容器创建testC的Bean实例,首先去"当前创建Bean池",查找是否当前Bean正在创建,如果没发现,则继续准备其需要的构造器参数testA,并将testC标识符放到"当前创建Bean池"。
  4. 到此为止Spring容器要去创建testA,但发现该Bean的标志符在"当前创建Bean池"中,表示了循环依赖,于是抛出BeanCurrentlyInCreationException异常。

其中"当前创建Bean池"就是一个Set集合,DefaultSingletonBeanRegistry类中beforeSingletonCreation方法,代码如下:

protected void beforeSingletonCreation(String beanName) {
    if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
        throw new BeanCurrentlyInCreationException(beanName);
    }
}

然后我们来到创建Bean实例的地方:

AbstractAutowireCapableBeanFactory类的543行,通过这个方法返回一个这个Bean的包装对象:

--> instanceWrapper = createBeanInstance(beanName, mbd, args);----> 进入这个方法

--> AbstractAutowireCapableBeanFactory类的1129行

// Need to determine the constructor...
// 需要确定构造函数,也就是说构造方法的循环依赖会在这儿return
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors != null ||
        mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR ||
        mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args))  {
    return autowireConstructor(beanName, mbd, ctors, args);
}

// No special handling: simply use no-arg constructor.
// 无需特殊处理,仅使用无参构造即可,setter的循环依赖会在这个地方return
return instantiateBean(beanName, mbd);

在上面代码中返回Bean的包装对象下面紧接着才是将这个对象曝光,也就是加入到SingletonFactory集合中,所以构造方法的循环引用,Spring是无法解决的,来到AbstractAutowireCapableBeanFactory的574行。

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

基于setter构造的循环依赖

首先说结论:Spring是可以为我们解决这样的依赖的,原理说白了就是用了缓存处理,也就是常说的提前曝光,为什么叫提前曝光呢?因为这个缓存中的Bean是一个还未进行赋值的Bean,仅仅是一个引用而已。

xml文件:

<bean id="testA" class="com.nmys.story.springCore.loop_dependency.loop01.LoopA">
    <property name="loopB" ref="testB"/>
</bean>

<bean id="testB" class="com.nmys.story.springCore.loop_dependency.loop01.LoopB">
    <property name="loopC" ref="testC"/>
</bean>

<bean id="testC" class="com.nmys.story.springCore.loop_dependency.loop01.LoopC">
    <property name="loopA" ref="testA"/>
</bean>

测试类:

/**
 * description:通过setter注入产生的循环依赖问题
 * @author 70KG
 */
public class Test03 {
    @Test
    public void m1() {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test03.xml");
    }
}

代码分析:

  1. Spring容器创建单例"loopA",首先根据无参构造创建Bean,并暴露到Map(singletonFactories)中,并将"loopA"标志符放到当前创建正在创建的Bean池(singletonsCurrentlyInCreation)中,然后进行setter注入"loopB"。
  2. Spring容器创建单例"loopB",首先根据无参构造创建Bean,并暴露到Map(singletonFactories)中,并将"loopA"标志符放到当前创建正在创建的Bean池(singletonsCurrentlyInCreation)中,然后进行setter注入"loopC"。
  3. Spring容器创建单例"loopC",首先根据无参构造创建Bean,并暴露到Map(singletonFactories)中,并将"loopA"标志符放到当前创建正在创建的Bean池(singletonsCurrentlyInCreation)中,然后进行setter注入"loopA"。在注入"loopA"的时候,由于提前暴露在singletonFactories集合中了,利用它就可以取到"loopA"正在创建的Bean对象。
  4. 最后依赖注入"testB","testA",完成setter注入。

查看控制台输出日志:

// 正在创建testA对象
Creating shared instance of singleton bean 'testA'
Creating instance of bean 'testA'
// 在缓存早期引用,目的是防止循环引用问题
Eagerly caching bean 'testA' to allow for resolving potential circular references
Creating shared instance of singleton bean 'testB'
Creating instance of bean 'testB'
Eagerly caching bean 'testB' to allow for resolving potential circular references
Creating shared instance of singleton bean 'testC'
Creating instance of bean 'testC'
Eagerly caching bean 'testC' to allow for resolving potential circular references
// 在创建testC的时候会去缓存中拿原来存储的testA,并返回,但此时的testA是一个不完全的对象,也就是尚未初始化
Returning eagerly cached instance of singleton bean 'testA' that is not fully initialized yet - a consequence of a circular reference
// 紧接着完成C的创建,顺便其它的也完成了
Finished creating instance of bean 'testC'
Finished creating instance of bean 'testB'
Finished creating instance of bean 'testA'
Returning cached instance of singleton bean 'testB'
Returning cached instance of singleton bean 'testC'

基于setter的循环依赖利用了提前曝光机制,这一步的关键代码,在AbstractAutowireCapableBeanFactory的574行,代码如下:

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

在加入SingletonFactory的前提是此Bean已经创建出来,才能够加入到这个Map集合中,也就是提前曝光,可以让别的Bean在初始化的时候从中拿到。否则是没有机会加入到Map中的。

基于prototype范围的依赖

首先说结论,对于多例情况下的循环依赖,是无法解决的,因为Spring容器不进行缓存,更无法提前暴露。

测试用例:

xml文件:

<bean id="testA" class="com.nmys.story.springCore.loop_dependency.loop01.LoopA" scope="prototype">
    <property name="loopB" ref="testB"/>
</bean>

<bean id="testB" class="com.nmys.story.springCore.loop_dependency.loop01.LoopB" scope="prototype">
    <property name="loopC" ref="testC"/>
</bean>

<bean id="testC" class="com.nmys.story.springCore.loop_dependency.loop01.LoopC" scope="prototype">
    <property name="loopA" ref="testA"/>
</bean>

测试类:

/**
 * description:通过setter注入产生的循环依赖问题
 * @author 70KG
 */
public class Test03 {

    @Test
    public void m1() {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test03.xml");
        LoopA loopA = context.getBean(LoopA.class);
        System.out.println(loopA);
    }

}

会抛出BeanCurrentlyInCreationException异常。

Spring是如何检测循环依赖

来到AbstractBeanFactory的246行,代码如下:

Object sharedInstance = getSingleton(beanName);

这一步是从缓存中获取以前创建的实例,如果发现存在,那么就存在循环依赖。

到此,全文完,自我感觉比其他的整理还算详细,如有疑问,请留言。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

另一个地球

另一个地球

[美]马克·格雷厄姆、威廉·H·达顿 / 胡泳、徐嫩羽 / 电子工业出版社 / 2015-10-1 / 78

互联网在日常工作和生活中扮演日益重要的角色,互联网将如何重塑社会?本书通过汇集有关互联网文化、经济、政治角色等问题的研究成果,提供了特定社会制度背景下解决这一问题的根本办法。 关于互联网的研究是蓬勃发展的崭新领域,牛津大学互联网研究院(OII)作为创新型的跨学科学院,自成立起就专注于互联网研究。牛津大学互联网研究院关于互联网+社会的系列讲座在一定程度上塑造了互联网+社会。本书内容基于不同学科......一起来看看 《另一个地球》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

URL 编码/解码
URL 编码/解码

URL 编码/解码

MD5 加密
MD5 加密

MD5 加密工具