Spring6 - (10) Bean 的循环依赖问题

image-20230206074352715

Spring6 -(10)Bean的循环依赖问题

1. 什么是Bean的循环依赖

什么是循环依赖?

A对象中有B属性。B对象中有A属性,我依赖你,你也依赖我。

比如:丈夫类Husband,妻子类Wife。Husband中有Wife的引用。Wife中有Husband的引用。

image-20230320141031406

Husband类:

package com.julissa.spring6.bean;

public class Husband {
    private String name;
    private Wife wife;
}

Wife类:

package com.julissa.spring6.bean;

public class Wife {
    private String name;
    private Husband husband;
}

2. singleton下的set注入产生的循环依赖

测试一下在singleton+setter的模式下产生的循环依赖,Spring是否能够解决?

第一步:创建Husband类

package com.julissa.spring6.bean;

public class Husband {
    private String name;
    private Wife wife;

    public void setName(String name) {
        this.name = name;
    }

    public void setWife(Wife wife) {
        this.wife = wife;
    }

    public String getName() {
        return name;
    }
    // toString()方法重写时需要注意:不能直接输出wife,输出wife.getName()。要不然会出现递归导致的栈内存溢出错误。
    @Override
    public String toString() {
        return "Husband{" +
                "name='" + name + '\'' +
                ", wife=" + wife.getName() +
                '}';
    }
}

第二步:创建Wife类

package com.julissa.spring6.bean;

public class Wife {
    private String name;
    private Husband husband;

    public void setHusband(Husband husband) {
        this.husband = husband;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
    // toString()方法重写时需要注意:不能直接输出husband,输出husband.getName()。要不然会出现递归导致的栈内存溢出错误。
    @Override
    public String toString() {
        return "Wife{" +
                "name='" + name + '\'' +
                ", husband=" + husband.getName() +
                '}';
    }
}

第三步:配置beans.xml

<?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="husband" class="com.julissa.spring6.bean.Husband" scope="singleton">
        <property name="name" value="tom"/>
        <property name="wife" ref="wife"/>
    </bean>

    <bean id="wife" class="com.julissa.spring6.bean.Wife" scope="singleton">
        <property name="name" value="marry"/>
        <property name="husband" ref="husband"/>
    </bean>
</beans>

第四步:编写测试程序

@Test
public void test(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");

    Husband husband = applicationContext.getBean("husband", Husband.class);
    System.out.println(husband);

    Wife wife = applicationContext.getBean("wife", Wife.class);
    System.out.println(wife);
}

第五步:运行测试程序

image-20230320144353068

总结:

  • singleton + setter 模式下的循环依赖是没有任何问题的

  • 主要原因是:在这种模式下Spring对Bean的管理主要分为清晰的两个阶段

    第一个阶段:在Spring容器加载的时候,实例化Bean,只要其中任意一个Bean实例化之后,马上进行“曝光”【不等属性赋值就曝光】

    第二个阶段:Bean“曝光”之后,在进行属性的赋值【调用set方法】

  • 核心解决方案是:实例化对象和对象的属性赋值分为两个阶段来完成的

  • 只有在scope属性是singleton的情况下,Spring才会采取提前“曝光”的措施

3. prototype下的set注入产生的循环依赖

再来测试一下:prototype+set注入的方式下,循环依赖会不会出现问题?

第一步:修改bean.xml,将scope属性改为prototype

<?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="husband" class="com.julissa.spring6.bean.Husband" scope="prototype">
        <property name="name" value="tom"/>
        <property name="wife" ref="wife"/>
    </bean>

    <bean id="wife" class="com.julissa.spring6.bean.Wife" scope="prototype">
        <property name="name" value="marry"/>
        <property name="husband" ref="husband"/>
    </bean>
</beans>

第二步:执行测试程序

image-20230320150238076

通过测试得知,当循环依赖的所有Bean的scope="prototype"的时候,产生的循环依赖,Spring是无法解决的,会出现BeanCurrentlyInCreationException异常。

测试一下,以上两个Bean,如果其中一个是singleton,另一个是prototype,是没有问题的。

为什么两个Bean都是prototype时会出错呢?

image-20230320150521357

4. singleton下的构造注入产生的循环依赖

测试一下singleton + 构造注入的方式下,spring是否能够解决这种循环依赖。

第一步:创建Husband类

package com.julissa.spring6.bean;

public class Husband {
    private String name;
    private Wife wife;

    public Husband(String name, Wife wife) {
        this.name = name;
        this.wife = wife;
    }

    public String getName() {
        return name;
    }
    // toString()方法重写时需要注意:不能直接输出wife,输出wife.getName()。要不然会出现递归导致的栈内存溢出错误。
    @Override
    public String toString() {
        return "Husband{" +
                "name='" + name + '\'' +
                ", wife=" + wife.getName() +
                '}';
    }
}

第二步:创建Wife类

package com.julissa.spring6.bean;

public class Wife {
    private String name;
    private Husband husband;

    public Wife(String name, Husband husband) {
        this.name = name;
        this.husband = husband;
    }

    public String getName() {
        return name;
    }
    // toString()方法重写时需要注意:不能直接输出husband,输出husband.getName()。要不然会出现递归导致的栈内存溢出错误。
    @Override
    public String toString() {
        return "Wife{" +
                "name='" + name + '\'' +
                ", husband=" + husband.getName() +
                '}';
    }
}

第三步:配置beans.xml

<?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="husband" class="com.julissa.spring6.bean.Husband" scope="singleton">
        <constructor-arg name="name" value="tom"/>
        <constructor-arg name="wife" ref="wife"/>
    </bean>

    <bean id="wife" class="com.julissa.spring6.bean.Wife" scope="singleton">
        <constructor-arg name="name" value="marry"/>
        <constructor-arg name="husband" ref="husband"/>
    </bean>
</beans>

第四步:编写测试程序

@Test
public void test(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");

    Husband husband = applicationContext.getBean("husband", Husband.class);
    System.out.println(husband);

    Wife wife = applicationContext.getBean("wife", Wife.class);
    System.out.println(wife);
}

第五步:运行测试程序

image-20230320151129125

通过测试得知:singleton + 构造方法模式无法解决循环依赖

主要原因是因为通过构造方法注入导致的:因为构造方法注入会导致实例化对象的过程和对象属性赋值的过程没有分离开,必须在一起完成导致的。

5. Spring解决循环依赖的机理

Spring为什么可以解决set + singleton模式下循环依赖?

根本的原因在于:这种方式可以做到将“实例化Bean”和“给Bean属性赋值”这两个动作分开去完成。

  • 实例化Bean的时候:调用无参数构造方法来完成。此时可以先不给属性赋值,可以提前将该Bean对象“曝光”给外界。

  • 给Bean属性赋值的时候:调用setter方法来完成。

两个步骤是完全可以分离开去完成的,并且这两步不要求在同一个时间点上完成

也就是说,Bean都是单例的,我们可以先把所有的单例Bean实例化出来,放到一个集合当中(我们可以称之为缓存),所有的单例Bean全部实例化完成之后,以后我们再慢慢的调用setter方法给属性赋值。这样就解决了循环依赖的问题。

那么在Spring框架底层源码级别上是如何实现的呢?请看:

在DefaultSingletonBeanRegistry这个类中,有三个重要的缓存

image-20230320160237711

private final Map<String, Object> singletonObjects  // 一级缓存
private final Map<String, ObjectFactory<?>> singletonFactories // 二级缓存
private final Map<String, Object> earlySingletonObjects // 三级缓存

这三个缓存其实本质上是三个Map集合。

一级缓存,存储的是:完整的单例Bean对象,也就是说这个缓存中Bean对象的属性已经赋值了,是一个完整的Bean对象

二级缓存,存储的是:早期的单例Bean对象,这个存储中Bean对象的属性没有赋值,只是一个早期的Bean对象

三级缓存,存储的是:单例工厂对象,这个缓存中存储了大量“工厂对象”,每一个单例Bean对象都会对应一个单例工厂对象,这个集合中 存储的是创建该Bean对象的那个单例工厂对象

在该类中有这样一个方法addSingletonFactory(),这个方法的作用是:将创建Bean对象的ObjectFactory对象提前曝光

image-20230320161418900

然后在该类中有getSingleton()方法

image-20230320161757398

从源码中可以看到,spring会先从一级缓存中获取Bean,如果获取不到,则从二级缓存中获取Bean,如果二级缓存还是获取不到,则从三级缓存中获取之前曝光的ObjectFactory对象,通过ObjectFactory对象获取Bean实例,这样就解决了循环依赖的问题。

总结:

Spring只能解决setter方法注入的单例bean之间的循环依赖。ClassA依赖ClassB,ClassB又依赖ClassA,形成依赖闭环。Spring在创建ClassA对象后,不需要等给属性赋值,直接将其曝光到bean缓存当中。在解析ClassA的属性时,又发现依赖于ClassB,再次去获取ClassB,当解析ClassB的属性时,又发现需要ClassA的属性,但此时的ClassA已经被提前曝光加入了正在创建的bean的缓存中,则无需创建新的的ClassA的实例,直接从缓存中获取即可。从而解决循环依赖问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值