Spring源码分析-循环依赖

导语
  前面提到了实例化Bean其实是一个复杂的过程,而在这个过程中比较难以理解的就是循环依赖的问题,下面就先来看看什么是循环依赖

什么是循环依赖?

 &emps;循环依赖就是循环引用对象,也就是说两个或者两个以上的对象Bean之间相互之间持有对方,例如说 A对象以B对象作为属性,B对象以C对象为属性,C对象以A对象作为属性,这样的话他们对象之间形成了一个循环调用,方法之间的调用也是循环调用。自己循环调用自己叫做递归。

  从递归的角度上来看,递归的结束时需要结束的条件,否则就会导致方法栈的溢出,当然,如果循环依赖没有结束的条件,那么就会导致死循环

Spring 是怎么解决循环依赖的?

  Spring容器循环包括构造器循环依赖和setter循环依赖,那么在Spring中是如何解决循环依赖的,首先来编写一个测试的类

public class TestA {
    private TestB testB;

    public TestA(TestB testB) {
        this.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 TestB(TestC testC) {
        this.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 TestC(TestA testA) {
        this.testA = testA;
    }

    public void c(){
        testA.a();
    }

    public TestA getTestA(){
        return testA;
    }

    public void setTestA(TestA testA){
        this.testA = testA;
    }
}


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

1、构造器循环依赖

  表示通过构造器注入构成循环依赖,这个依赖是无法解决的,只能是抛出BeanCurrentlyInCreationExceptio异常表示循环依赖。
  例如上面代码,在创建TestA的时候,构造器需要创建TestB类型,那么TestB的构造器,去创建TestB类型,发现又需要TestC类型,这个时候需要创建TestC类型的时候发现需要创建TestA类型。这样就形成了一个环。导致所有的Bean对象都无法创建。

  Spring容器将每一个正在创建的Bean标识符放在一个当前创建Bean的池中,Bean标识符在创建过程中将一直保持在这个池中,这样的话,如果在创建Bean的过程中发现自己已经在当前创建的Bean池中,将会抛出BeanCurrentlyInCreationException的异常表示循环依赖;对于创建完毕的Bean将从当前创建Bean池中进行清除。

1、创建配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <bean id="testA" class="com.core.charp2.demo04.TestA">
        <constructor-arg index="0" ref="testB"/>
    </bean>
    
    <bean id="testB" class="com.core.charp2.demo04.TestB">
        <constructor-arg index="0" ref="testC"/>
    </bean>
    
    <bean id="testC" class="com.core.charp2.demo04.TestC">
        <constructor-arg index="0" ref="testA"/>
    </bean>

</beans>

2、创建测试实例

public class Test {
    public static void main(String[] args) {
        try{
            new ClassPathXmlApplicationContext("classpath:demo4.xml");
        }catch (Exception e){
            e.printStackTrace();
        }


    }
}

异常处理
在这里插入图片描述

  从上面的分析可以看到

  • Spring 容器在创建testA的时候,首先去当前线程池中查找是否当前Bean正在被创建,如果没有发现,则继续准备其需要的构造器参数testB,并将testA标识符放到当前创建Bean池中。
  • Spring 容器在创建testB的时候,也需要进行同样的操作,看看在当前创建Bean池中是否找到对应的Bean正在被创建,如果没有发现,则继续需要创建构造器参数testC。
  • Spring 容器在创建testC的时候也进行了同样的操作,但是这个时候,在testC的构造函数中没有发现对应的TestA的Bean被创建,这个时候就出现了循环依赖没有被处理的场景,这个时候就会抛出一个BeanCurrentlyInCreationException异常。

2、setter循环依赖

  也就是说通过setter方法构成的循环依赖,对于setter注入构成的依赖是通过Spring提前暴露刚刚完成构造器注入但为完成其他步骤的步骤的Bean来完成,而且这种方式只能解决单例作用域的bean循环依赖。通过提前暴露一个单例工厂方法,从而让其他Bean能够引用到需要被创建的Bean对象。如下

在这里插入图片描述
具体步骤如下

  • 1、Spring容器创建单例testA的时候,首先根据无参构造器创建Bean,并且暴露一个ObjectFactory,用于返回一个提前暴露一个创建中的Bean,并将TestA表示放到当前创建Bean池中。然后进行setter注入testB
  • 2、Spring创建单例testB的时候,也是同样的操作,最终暴露一个ObjectFactory的创建中的Bean,然后进行标记
  • 3、Spring 创建单例testC的时候,前面的步骤都是与创建testA的步骤一样,但是在setter注入TestA的时候,由于提前暴露了一个ObjectFactory工厂,从而使用它可以返回一个提前暴露的一个创建中的testA。
  • 4、最后在依赖注入testB和testA,从而完成整个的setter注入。

3、prototype 范围的依赖处理

  对于prototype 作用域Bean,Spring 容器无法完成依赖注入,因为Spring 容器不进行缓存,prototype作用的Bean,因此无法提前暴露一个创建中的Bean。例如

1、创建配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="testA" class="com.core.charp2.demo04.TestA" scope="prototype">
    <property name="testB" ref="testB"/>
</bean>

<bean id="testB" class="com.core.charp2.demo04.TestB" scope="prototype">
    <property name="testC" ref="testC"/>
</bean>

<bean id="testC" class="com.core.charp2.demo04.TestC" scope="prototype">
    <property name="testA" ref="testA"/>
</bean>

</beans>

2、创建测试类

public class Test {
    public static void main(String[] args) {
        try{
            ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:demo5.xml");
            System.out.println(context.getBean("testA"));
        }catch (Exception e){
            e.printStackTrace();
        }

    }
}

在这里插入图片描述
  对于Singleton来说,可以通过setAllowCircularReferences(false);来禁用循环依赖,但是对于prototype来说,由于每次都需要创建一个不一样的Bean,所以说没有办法每次都来解决这个问题,有人会说每次都来通过提到的机制来实现不就可以了么。如果使用那种方式则上面的实现效率会很低。导致整个的JVM的资源消耗过大。为了解决性能的问题,我们在Spring源码中会看到很多的Map,这些Map就是用来在创建对象的过程中来对对象进行缓存,这样的话会减少在创建对象上面消耗的性能。从而提高效率。

总结

  从上的内容可以简单的了解Spring中是怎么出现循环依赖,以及如何解决循环依赖,在实际使用的过程中,Spring已经对循环依赖做了很多的优化工作,几乎是感觉不出来循环依赖带来的问题。但是在以后的分享中还是会对这块内容做深入的分析。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

nihui123

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值