0

Spring6 当中的 Bean 循环依赖的详细处理方案+源码解析 - Rainbow-Sea

 1 month ago
source link: https://www.cnblogs.com/TheMagicalRainbowSea/p/18168793
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

1. Spring6 当中的 Bean 循环依赖的详细处理方案+源码解析


每博一文案

听完这段话就勇敢起来吧,在任何犹豫的时刻,一旦抱有人生就这么短短几十年,我不去做一定会后悔这样的想法,就会凭空多出几分勇气,比如:尝试新的穿衣风格,向喜欢的人表白,去特别贵的餐厅大吃一顿,对看不惯的人和事说不,不乐观的想,我们其实都是没有来路和归途的,能拥有的就是现在,所以想做什么就去做吧,冲动一点也没关系,吃点亏也没关系.

1.1 Bean的循环依赖

A对象中有B属性。B对象中有A属性。这就是循环依赖。我依赖你,你也依赖我。
比如:丈夫类Husband,妻子类Wife。Husband中有Wife的引用。Wife中有Husband的引用。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
package com.rainbowsea.bean;

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

    public Wife() {
    }

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


    public String getName() {
        return name;
    }

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

    public Husband getHusband() {
        return husband;
    }

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

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

Husband

package com.rainbowsea.bean;

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


    public Husband() {
    }

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


    public String getName() {
        return name;
    }

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

    public Wife getWife() {
        return wife;
    }

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


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

注意点: toString()方法重写时需要注意:不能直接输出wife,输出wife.getName()。要不然会出现递归导致的栈内存溢出错误。

1.2 singletion 下的 set 注入下的 Bean 的循环依赖

我们来编写程序,测试一下在singleton+setter的模式下产生的循环依赖,Spring是否能够解决?

准备工作:配置导入 相关的 spring 框架,让 Maven 帮我们导入 spring的相关jar包。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.rainbowsea</groupId>
    <artifactId>spring6-007-circular-dependency-blog</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.11</version>
        </dependency>


        <!-- junit4 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>

    </dependencies>
</project>

配置相关的 spring.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">

    <!--     Husband 的配置-->
    <bean id="husbandBean" class="com.rainbowsea.bean.Husband" scope="singleton">
        <property name="name" value="小明" ></property>
        <property name="wife" ref="wifeBean"></property> <!--set 注入-->
    </bean>
    <!--    Wife 的配置-->
    <bean id="wifeBean" class="com.rainbowsea.bean.Wife" scope="singleton">
        <property name="name" value="小花"></property>
        <property name="husband" ref="husbandBean"></property>
    </bean>
</beans>

运行测试:

在这里插入图片描述

通过测试得知:在singleton + set注入的情况下,循环依赖是没有问题的。Spring可以解决这个问题。

package com.rainbowsea.test;

import com.rainbowsea.bean.Husband;
import com.rainbowsea.bean.Wife;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class CircularDependencyTest {
    @Test
    public void testCircularDependency() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        Husband husbandBean = applicationContext.getBean("husbandBean", Husband.class);
        System.out.println(husbandBean);


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


}

解决分析:

singleton + setter模式下可以解决的循环依赖问题

在singleton + setter 模式下,为什么循环依赖不会出现问题,Spring是如何应对的?

主要原因是:在这个 singleton 单例模式下,在Spring 容器中的 bean 对象是独一无二的对象,是唯一的一个。同志在该 singleton 单例模式下:Spring 对 Bean 的管理主要分为清晰的两个阶段

  1. 第一个阶段:在Spring 容器加载的时候,实例Bean ,只要其中任意一个 Bean 实例化之后,马上进行一个“曝光” (注意:曝光不等于属性赋值,曝光了,但是属性并没有附上值的)
  2. 第二个阶段:Bean “曝光”之后,再进行属性的赋值操作(调用 set()方法实现对属性的赋值操作)

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

简单来说:就是:singleton 优先被“曝光”,实例化和赋值是分开的,会优先把实例化的对象的地址曝光出来,因为在 singleton 单例模式下,bean 是唯一的一个,独一无二的,并且早晚都要进行赋值操作。提前曝光,后面再进行赋值也是无妨的。因为你弄来弄去,就是那唯一的一个 bean。不存在多个,不知道是哪一个的问题

在这里插入图片描述

1.3 prototype下的 set 注入下的 Bean 的循环依赖

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

我们只需将 spring.xml 配置文件信息,修改为 protoype (多例)即可。

在这里插入图片描述

运行测试看看。

在这里插入图片描述

报错,报错信息如下:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'husbandBean' defined in class path resource [spring.xml]: Cannot resolve reference to bean 'wifeBean' while setting bean property 'wife'

创建名为“husbandBean”的bean时出错:请求的bean当前正在创建中:是否存在无法解析的循环引用?通过测试得知,当循环依赖的所有Bean的scope="prototype"的时候,产生的循环依赖,Spring是无法解决的,会出现BeanCurrentlyInCreationException异常。

prototype下的 set 注入下的 Bean 的循环依赖;并不能解决循环依赖,原因是:prototype 是多例的存在,多个 Bean 对象,不是唯一的一个Bean,无法确定是具体是哪个,Bean无法提前曝光。

BeanCreationException 报错:当前的Bean正在处于创建中异常

特别的:当两个bean的scope都是prototype的时候,才会出现异常,如果其中任意一个是singleton的,就不会出现异常了。是其中的任意一个 就行,就不会出现异常了。如果是三个 bean 的话,那就需要其中的任意两个 是为singleton才行。

原因是:singleton 优先被“曝光”,实例化和赋值是分开的,会优先把实例化的对象的地址曝光出来,因为在 singleton 单例模式下,bean 是唯一的一个,独一无二的,并且早晚都要进行赋值操作。提前曝光,后面再进行赋值也是无妨的。因为你弄来弄去,就是那唯一的一个 bean。不存在多个,不知道是哪一个的问题。

测试:当两个bean的scope都是prototype的时候,才会出现异常,如果其中任意一个是singleton的,就不会出现异常了。

Husband 为 prototype ,Wife 为 singleten

在这里插入图片描述

反一下:Husband 为 singleten ,Wife 为 prototype

在这里插入图片描述

至于,三个 Bean ,需要任意两个为 singleten ,才不会报异常,就大家自行测试了。理论上就是:n 个 就需要 N-1个为 singleten 。

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

如果是基于构造注入(进行赋值),很明显,要调用构造方法进行赋值就一定要完完整整的进行一次性赋值+实例化,没有分段的,所以会产生循环依赖并且无法解决的,
所以编写代码时一定要注意。同样是报: BeanCreationException 报错:当前的Bean正在处于创建中异常

我们来测试一下。

在这里插入图片描述

1.5 Spring 解决循环依赖的原理(源码解析)

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

根本原因在于:这种方式可以做到将 “实例化 Bean” 和“给 Bean 属性赋值” 这两个动作分开去完成。实例化Bean的时候:调用无参数构造方法来完成此时可以先不给属性赋值(因为在 singleton 单例模式下,bean 是唯一的一个,独一无二的,并且早晚都要进行赋值操作。提前曝光,后面再进行赋值也是无妨的。因为你弄来弄去,就是那唯一的一个 bean),可以提前将Bean 对象“曝光”给外界

给Bean 属性赋值的时候:调用 setter()方法来完成(set注入完成,调用其中 bean对象当中的 set()方法,所以千万要记得写 set()方法)。

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

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

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

我们先来分析一下:AbstractAutowireCapableBeanFactory类下的doCreateBean() 方法

在这里插入图片描述

doCreateBean() 方法 下调用的:addSingletonFactory() 方法,这里源码上使用了正则表达式,关于Lambda 表达式,由于设置的内容较多,想要了解更多的,大家可以移步至✏️✏️✏️ 函数式编程:Lambda 表达式_(ws, bs)>-CSDN博客

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
在这里插入图片描述

下面这个DefaultSingletonBeanRegistry类,才是我们真正要探究的源码内容

在这里插入图片描述

在这个DefaultSingletonBeanRegistry 类当中中包含三个重要的属性同时也是三个Map集合:

在这里插入图片描述
	/** Cache of singleton objects: bean name to bean instance. */
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

	/** Cache of singleton factories: bean name to ObjectFactory. */
	private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

	/** Cache of early singleton objects: bean name to bean instance. */
	private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

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

  • Cache of singleton objects: bean name to bean instance. 单例对象的缓存:key存储bean名称,value存储Bean对象【一级缓存】
  • Cache of early singleton objects: bean name to bean instance. 早期单例对象的缓存:key存储bean名称,value存储早期的Bean对象【二级缓存】
  • Cache of singleton factories: bean name to ObjectFactory.单例工厂缓存:key存储bean名称,value存储该Bean对应的ObjectFactory对象【三级缓存】
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); 一级缓存
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16); 二级缓存
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); 三级缓存
这个三个缓存都是Map集合
Map集合的key 存储的都是bean的name(bean id)
> 一级缓存存储的是:单例Bean对象,完整的单例Bean对象,也就是这个缓存中的Bean对象的属性都已经赋值了,是一个完整的Bean对象
> 二级缓存存储的是: 早期的案例Bean对象,这个缓存中的单例Bean对象的属性灭有赋值,只是一个早期的实例对象
> 三级缓存存储的是: 单例工厂对象,这个里面存储了大力的“工厂对象”,每一个单例Bean对象都会对应一个单例工厂对象。
> 这个集合中存储的是,创建该单例对象时对应的那个单例工厂对象。    

我们再来看,在该类中有这样一个方法 addSingletonFactory(),这个方法的作用是:将创建Bean对象的ObjectFactory对象提前曝光。这里我们Debug 调试看看。

在这里插入图片描述
在这里插入图片描述

再分析对应下面的源码:

在这里插入图片描述
在这里插入图片描述

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

总结:

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

2. 总结:

  1. Bean的循环依赖:A对象中有B属性。B对象中有A属性。这就是循环依赖。我依赖你,你也依赖我。

  2. singletion 下的 set 注入下的 Bean 的循环依赖能够被解决。主要原因是:在这个 singleton 单例模式下,在Spring 容器中的 bean 对象是独一无二的对象,是唯一的一个。同志在该 singleton 单例模式下:Spring 对 Bean 的管理主要分为清晰的两个阶段

    1. 第一个阶段:在Spring 容器加载的时候,实例Bean ,只要其中任意一个 Bean 实例化之后,马上进行一个“曝光” (注意:曝光不等于属性赋值,曝光了,但是属性并没有附上值的)
    2. 第二个阶段:Bean “曝光”之后,再进行属性的赋值操作(调用 set()方法实现对属性的赋值操作)

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

  3. prototype下的 set 注入下的 Bean 的循环依赖;并不能解决循环依赖,原因是:prototype 是多例的存在,多个 Bean 对象,不是唯一的一个Bean,无法确定是具体是哪个,Bean无法提前曝光。

  4. 特别的:当两个bean的scope都是prototype的时候,才会出现异常,如果其中任意一个是singleton的,就不会出现异常了。是其中的任意一个 就行,就不会出现异常了。如果是三个 bean 的话,那就需要其中的任意两个 是为singleton才行。

    1. 至于,三个 Bean ,需要任意两个为 singleten ,才不会报异常,就大家自行测试了。理论上就是:n 个 就需要 N-1个为 singleten 。
    2. 注意报错信息:org.springframework.beans.factory.BeanCreationException: 当前的Bean正在处于创建中异常
  5. singleton下的构造注入产生的循环依赖;是基于构造注入(进行赋值),很明显,要调用构造方法进行赋值就一定要完完整整的进行一次性赋值+实例化,没有分段的,所以会产生循环依赖并且无法解决的,

  6. Spring 解决循环依赖的原理(源码解析):一级缓存,二级缓存,三级缓存的存在。提前“曝光”机制

3. 最后:

“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”

在这里插入图片描述

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK