资讯专栏INFORMATION COLUMN

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

Cheng_Gang / 3209人阅读

摘要:我们知道为我们完全实例化好一个一定会经过一下三步实例化,其实也就是调用对象的构造方法实例化对象。循环依赖的产生定会发生在步骤和中,因为是利用构造方法,是利用属性赋值。

引言

基于Spring5+

什么是循环依赖?

循环依赖有几种?

Spring可以解决哪几种,为什么不能解决这几种?

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是如何来处理这样的状况呢?
循环依赖有几种?
有三种情况:

基于构造方法的循环依赖

基于setter构造的循环依赖(网上也叫field属性依赖)

基于prototype范围的依赖

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

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

createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象。

populateBean:填充属性,这一步主要是多bean的依赖属性进行填充。

initializeBean:调用默认的或者自定义的init方法。

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

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

测试用例:

xml文件:


    



    



    

测试类:

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

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

}

分析上面代码:

Spring容器创建testA的Bean实例,首先去"当前创建Bean池",查找是否当前Bean正在创建,如果没发现,则继续准备其需要的构造器参数testB,并将testA标识符放到"当前创建Bean池"。

Spring容器创建testB的Bean实例,首先去"当前创建Bean池",查找是否当前Bean正在创建,如果没发现,则继续准备其需要的构造器参数testC,并将testB标识符放到"当前创建Bean池"。

Spring容器创建testC的Bean实例,首先去"当前创建Bean池",查找是否当前Bean正在创建,如果没发现,则继续准备其需要的构造器参数testA,并将testC标识符放到"当前创建Bean池"。

到此为止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文件:


    



    



    

测试类:

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

代码分析:

Spring容器创建单例"loopA",首先根据无参构造创建Bean,并暴露到Map(singletonFactories)中,并将"loopA"标志符放到当前创建正在创建的Bean池(singletonsCurrentlyInCreation)中,然后进行setter注入"loopB"。

Spring容器创建单例"loopB",首先根据无参构造创建Bean,并暴露到Map(singletonFactories)中,并将"loopA"标志符放到当前创建正在创建的Bean池(singletonsCurrentlyInCreation)中,然后进行setter注入"loopC"。

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

最后依赖注入"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文件:


    



    



    

测试类:

/**
 * 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);

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73177.html

相关文章

  • Spring IOC 容器源码分析 - 循环依赖解决办法

    摘要:实例化时,发现又依赖于。一些缓存的介绍在进行源码分析前,我们先来看一组缓存的定义。可是看完源码后,我们似乎仍然不知道这些源码是如何解决循环依赖问题的。 1. 简介 本文,我们来看一下 Spring 是如何解决循环依赖问题的。在本篇文章中,我会首先向大家介绍一下什么是循环依赖。然后,进入源码分析阶段。为了更好的说明 Spring 解决循环依赖的办法,我将会从获取 bean 的方法getB...

    aikin 评论0 收藏0
  • Spring IOC 容器源码分析 - 获取单例 bean

    摘要:简介为了写容器源码分析系列的文章,我特地写了一篇容器的导读文章。在做完必要的准备工作后,从本文开始,正式开始进入源码分析的阶段。从缓存中获取单例。返回以上就是和两个方法的分析。 1. 简介 为了写 Spring IOC 容器源码分析系列的文章,我特地写了一篇 Spring IOC 容器的导读文章。在导读一文中,我介绍了 Spring 的一些特性以及阅读 Spring 源码的一些建议。在...

    lufficc 评论0 收藏0
  • Spring IOC 容器源码分析系列文章导读

    摘要:本文是容器源码分析系列文章的第一篇文章,将会着重介绍的一些使用方法和特性,为后续的源码分析文章做铺垫。我们可以通过这两个别名获取到这个实例,比如下面的测试代码测试结果如下本小节,我们来了解一下这个特性。 1. 简介 Spring 是一个轻量级的企业级应用开发框架,于 2004 年由 Rod Johnson 发布了 1.0 版本。经过十几年的迭代,现在的 Spring 框架已经非常成熟了...

    NSFish 评论0 收藏0
  • Spring IOC 容器源码分析 - 创建单例 bean 过程

    摘要:关于创建实例的过程,我将会分几篇文章进行分析。源码分析创建实例的入口在正式分析方法前,我们先来看看方法是在哪里被调用的。时,表明方法不存在,此时抛出异常。该变量用于表示是否提前暴露单例,用于解决循环依赖。 1. 简介 在上一篇文章中,我比较详细的分析了获取 bean 的方法,也就是getBean(String)的实现逻辑。对于已实例化好的单例 bean,getBean(String) ...

    mochixuan 评论0 收藏0
  • Spring-bean几种循环依赖方式

    摘要:如下图注意,这里不是函数的循环调用,是对象的相互依赖关系。因此如果在创建过程中发现自己已经在当前创建池里时将抛出异常表示循环依赖而对于创建完毕的将从当前创建池中清除掉。 showImg(https://segmentfault.com/img/bVbs5kw?w=339&h=193); 什么是循环依赖? 循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭...

    notebin 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<