Spring学习记录之Bean的循环依赖

发布时间 2024-01-03 19:20:38作者: zhao-XH

Spring学习记录之Bean的循环依赖


前言

这篇文章是我第二次学习b站老杜spring相关课程所进行的学习记录,算是对课程内容及笔记的二次整理,以自己的理解方式进行二次记录,其中理解可能存在错误,欢迎且接受各位大佬们的批评指正;

关于本笔记,只是我对于相关知识遗忘时快速查阅了解使用,至于课程中实际实验配置等,也只是记录关键,并不会记录详细步骤,若想了解可以关注我博客的项目经验模块,我会在实际项目开发过程中总结项目经验,在该模块发布!

学习视频地址:https://www.bilibili.com/video/BV1Ft4y1g7Fb/

视频配套笔记:https://www.yuque.com/dujubin/ltckqu/kipzgd?singleDoc# 《Spring6》 密码:mg9b


目录


一、我个人对这部分学习的一些见解

这部分包含Spring常见面试题,同时也是实际项目开发常见问题,需要重点掌握熟悉

这部分我会继续引用老杜的笔记。

二、什么是Bean的循环依赖

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

举例:丈夫类Husband,妻子类WifeHusband中有Wife的引用。Wife中有Husband的引用。

image.png

Husband

package com.powernode.spring6.bean;

/**
 * @author 动力节点
 * @version 1.0
 * @className Husband
 * @since 1.0
 **/
public class Husband {
    private String name;
    private Wife wife;
}

Wife

package com.powernode.spring6.bean;

/**
 * @author 动力节点
 * @version 1.0
 * @className Wife
 * @since 1.0
 **/
public class Wife {
    private String name;
    private Husband husband;
}

在注入以上两种BeanSpring容器中时,可能会产生一个问题,即在Husband实例化之后需要注入Wife,然后Spring开始实例化Wife,但是当Wife实例化时又需要注入Husband,这时是否会出现注入不了的问题?我们将这种问题描述为Bean的循环依赖问题。

三、singleton下的set注入产生的循环依赖

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

Husband

package com.powernode.spring6.bean;

/**
 * @author 动力节点
 * @version 1.0
 * @className Husband
 * @since 1.0
 **/
public class Husband {
    private String name;
    private Wife wife;

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

    public String getName() {
        return name;
    }

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

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

Wife

package com.powernode.spring6.bean;

/**
 * @author 动力节点
 * @version 1.0
 * @className Wife
 * @since 1.0
 **/
public class Wife {
    private String name;
    private Husband husband;

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

    public String getName() {
        return name;
    }

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

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

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">

    <bean id="husbandBean" class="com.powernode.spring6.bean.Husband" scope="singleton">
        <property name="name" value="张三"/>
        <property name="wife" ref="wifeBean"/>
    </bean>
    <bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="singleton">
        <property name="name" value="小花"/>
        <property name="husband" ref="husbandBean"/>
    </bean>
</beans>

测试程序:

package com.powernode.spring6.test;

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

/**
 * @author 动力节点
 * @version 1.0
 * @className CircularDependencyTest
 * @since 1.0
 **/
public class CircularDependencyTest {

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

执行结果:

img

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

补充分析:

我们来分析一下,Spring是如何解决singleton + set情况下的循环依赖问题的。

在学习了Bean的生命周期之后,我们知道,单例情况下Bean的生命周期有十步,这里我们不需要一一回忆,我们只需要知道SpringBean的实例化和给Bean的属性赋值是分开做的。

那么我们可以从上往下分析我们配置的xml文件,当Spring读到以下配置时,Spring先进行Husband的实例化,创建了名称为husbandBean的对象,此时还未给该对象赋值。当读到name属性时,赋值为张三。当读到wife属性时,Spring会去找xml中是否配置了idwifeBean的实例。

    <bean id="husbandBean" class="com.powernode.spring6.bean.Husband" scope="singleton">
        <property name="name" value="张三"/>
        <property name="wife" ref="wifeBean"/>
    </bean>

此时Spring找到并创建Wife的实例化对象wifeBean,此时还未给该对象赋值,但是这个时候husbandBean对象的wife属性就可以成功赋值上wifeBean。当读到name属性时,赋值为小花。当读到husband属性时,Spring会去找xml中是否配置了idhusbandBean的实例。发现容器中已经有了husbandBean对象实例,其也可以直接赋值成功。

    <bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="singleton">
        <property name="name" value="小花"/>
        <property name="husband" ref="husbandBean"/>
    </bean>

所以在singleton + set的情况下,Spring可以解决循环依赖问题。

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

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

Husband

package com.powernode.spring6.bean2;

/**
 * @author 动力节点
 * @version 1.0
 * @className Husband
 * @since 1.0
 **/
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;
    }

    @Override
    public String toString() {
        return "Husband{" +
                "name='" + name + '\'' +
                ", wife=" + wife +
                '}';
    }
}

Wife

package com.powernode.spring6.bean2;

/**
 * @author 动力节点
 * @version 1.0
 * @className Wife
 * @since 1.0
 **/
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;
    }

    @Override
    public String toString() {
        return "Wife{" +
                "name='" + name + '\'' +
                ", husband=" + husband +
                '}';
    }
}

spring2.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="hBean" class="com.powernode.spring6.bean2.Husband" scope="singleton">
        <constructor-arg name="name" value="张三"/>
        <constructor-arg name="wife" ref="wBean"/>
    </bean>

    <bean id="wBean" class="com.powernode.spring6.bean2.Wife" scope="singleton">
        <constructor-arg name="name" value="小花"/>
        <constructor-arg name="husband" ref="hBean"/>
    </bean>
</beans>

测试程序:

@Test
public void testSingletonAndConstructor(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring2.xml");
    Husband hBean = applicationContext.getBean("hBean", Husband.class);
    Wife wBean = applicationContext.getBean("wBean", Wife.class);
    System.out.println(hBean);
    System.out.println(wBean);
}

执行结果:发生了异常,信息如下:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'hBean': Requested bean is currently in creation: Is there an unresolvable circular reference?
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:355)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:227)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:325)
	... 56 more

产生了循环依赖问题,并且Spring是无法解决这种循环依赖的。

为什么呢?

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

补充分析:

看到上述实验的BeanCurrentlyInCreationException异常(Bean正在创建中异常)。我们和上述singleton + set的情况分析思路一样,为什么singleton + set可以,而singleton +构造方法的方式就会产生循环依赖问题呢?

其本质原因其实是对象的实例化和依赖注入(属性赋值)的步骤是否分开。我们知道singleton + set的情况,是先实例化对象,将该对象曝光Spring容器中,不管该对象是否已经给属性赋值,都可以根据对象的名称(id的值)来注入到其他对象中去。但是使用构造方法进行依赖注入时,对象在创建时就要给其属性赋值。这就导致当我拿着hBeanwife指定的wBean去创建对应的对象实例时,wBean也需要找到创建好的hBean,但是hBean还在等待wBean创建好。这就形成了一种"死锁",导致循环依赖问题无法解决。

五、prototype下的set注入产生的循环依赖

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

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">

    <bean id="husbandBean" class="com.powernode.spring6.bean.Husband" scope="prototype">
        <property name="name" value="张三"/>
        <property name="wife" ref="wifeBean"/>
    </bean>
    <bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="prototype">
        <property name="name" value="小花"/>
        <property name="husband" ref="husbandBean"/>
    </bean>
</beans>

执行测试程序:发生了异常,异常信息如下:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'husbandBean': Requested bean is currently in creation: Is there an unresolvable circular reference?
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:265)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:325)
	... 44 more

翻译为:创建名为“husbandBean”的bean时出错:请求的bean当前正在创建中:是否存在无法解析的循环引用?

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

补充分析:

可以看到同样报的是BeanCurrentlyInCreationException异常(Bean正在创建中异常),这其实也很好理解,每次当husbandBean去找wifeBean的时候创建的都是新的wifeBean,而新的wifeBean去找husbandBean创建的也是新的husbandBean,导致循环依赖。

img

大家可以测试一下,以上两个Bean,如果其中一个是singleton,另一个是prototype,是没有问题的。(因为找对应实例对象的过程被单例对象终止了)

六、 Spring解决循环依赖的机理

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

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

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

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

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

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

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

补充:这部分源码的解读可以直接跳到老杜的讲解视频,讲述更为清晰。(需要注意的是Bean的三级缓存也是面试常见题目)

058-Bean的循环依赖之源码分析_哔哩哔哩_bilibili

img

在以上类中包含三个重要的属性:

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对象【三级缓存】

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

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

img

再分析下面的源码:

img

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

源码总结:

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

七、总结

这部分我们了解到:

  • singleton + set的情况下,Spring可以解决循环依赖问题。
  • 了解Bean的三级缓存(常见面试题)。

这里需要去了解老杜这节相关讲解,可以直接点击下面链接跳转到对应课程学习了解!

054-Bean的循环依赖之单例和set模式下_哔哩哔哩_bilibili