什么是Bean的循环依赖?解决方案是什么?

Spring Bean循环依赖以及解决方案:

什么是Bean的循环依赖?

在这里插入图片描述

在这里插入图片描述

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

如图所示,Father类中有Son属性的成员变量,Son类中有Father属性的成员变量。这就是循环依赖。

public class Son {
    private String name;
    private Father father;

}
public class Father {
    private String name;
    private Son son;
}

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

导入坐标:

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

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.3.25</version>
    </dependency>
</dependencies>

pojo:

Father实体类:

package com.stringzhua.pojo;

/**
 * @Author Stringzhua
 * @Date 2024/9/14 16:07
 * description:
 */
public class Father {
    private String name;
    private Son son;

    public String getName() {
        return name;
    }

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

    public Son getSon() {
        return son;
    }

    public void setSon(Son son) {
        this.son = son;
    }

    @Override
    public String toString() {
        return "Father{" +
                "name='" + name + '\'' +
                ", son=" + son.getName() +
                '}';
    }
}

Son实体类:

package com.stringzhua.pojo;

/**
 * @Author Stringzhua
 * @Date 2024/9/14 16:07
 * description:
 */
public class Son {
    private String name;
    private Father father;

    public String getName() {
        return name;
    }

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

    public Father getFather() {
        return father;
    }

    public void setFather(Father father) {
        this.father = father;
    }

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

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="father" class="com.stringzhua.pojo.Father" scope="singleton">
        <property name="name" value="小头爸爸"></property>
        <property name="son" ref="son"></property>
    </bean>

    <bean id="son" class="com.stringzhua.pojo.Son" scope="singleton">
        <property name="name" value="大头儿子"></property>
        <property name="father" ref="father"></property>
    </bean>
</beans>

测试类:

 @Test
 public void test01() {
     ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
     Father father = (Father) applicationContext.getBean("father");
     Son son = (Son) applicationContext.getBean("son");
     System.out.println(father);
     System.out.println(son);
 }

测试结果:
在这里插入图片描述

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

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

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

将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="father" class="com.stringzhua.pojo.Father" scope="prototype">
        <property name="name" value="小头爸爸"></property>
        <property name="son" ref="son"></property>
    </bean>

    <bean id="son" class="com.stringzhua.pojo.Son" scope="prototype">
        <property name="name" value="大头儿子"></property>
        <property name="father" ref="father"></property>
    </bean>
</beans>

报错内容为:创建名为 ‘father’ 的 bean 时出错:请求的 bean 当前正在创建中:是否存在无法解析的循环引用?
在这里插入图片描述

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

以上两个Bean,如果其中一个是singleton,另一个是prototype,是没有问题的。为什么两个Bean都是prototype时会出错呢?
在这里插入图片描述

这里的循环依赖问题:
在这里插入图片描述

通过测试得知:在property + set注入的情况下,循环依赖Spring是无法解决的,会抛出BeanCurrentlyInCreationException异常

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

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

public class Father {
    private String name;
    private Son son;

    public Father(String name, Son son) {
        this.name = name;
        this.son = son;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Father{" +
                "name='" + name + '\'' +
                ", son=" + son +
                '}';
    }
}
public class Son {
    private String name;
    private Father father;

    public Son(String name, Father father) {
        this.name = name;
        this.father = father;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Son{" +
                "name='" + name + '\'' +
                ", father=" + father +
                '}';
    }
}
	<bean id="father" class="com.stringzhua.pojo.Father" scope="singleton">
        <constructor-arg name="name" value="小头爸爸"></constructor-arg>
        <constructor-arg name="son" ref="son"></constructor-arg>
    </bean>

    <bean id="son" class="com.stringzhua.pojo.Son" scope="singleton">
        <constructor-arg name="name" value="大头儿子"></constructor-arg>
        <constructor-arg name="father" ref="father"></constructor-arg>
    </bean>

测试:

 @Test
    public void test02() {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        Father father = (Father) applicationContext.getBean("father");
        Son son = (Son) applicationContext.getBean("son");
        System.out.println(father);
        System.out.println(son);
    }

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

在这里插入图片描述

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'father': 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:333)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:330)
	... 51 more

和上一个测试结果相同,都是提示产生了循环依赖,并且Spring是无法解决这种循环依赖的

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

Spring解决循环依赖的机理

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

参考链接

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

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

两个步骤是完全可以分离开去完成的,并且这两步不要求在同一个时间点上完成。也就是说,Bean都是单例的,我们可以先把所有的单例Bean实例化出来,放到一个集合当中(我们可以称之为缓存),所有的单例Bean全部实例化完成之后,以后我们再慢慢的调用setter方法给属性赋值。这样就解决了循环依赖的问题。
在这里插入图片描述

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

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对象提前曝光。
在这里插入图片描述

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

总结:

Spring只能解决setter方法注入的单例bean之间的循环依赖。

ClassA依赖ClassB,ClassB又依赖ClassA,形成依赖闭环。

11d6e7b6116b6ff2ccb3a37beb457352.png

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

面试常问到的:

1.关于三级缓存的问题,三级缓存的关键? 三级缓存能不能换成二级缓存?

三级缓存的关键是 实例化和初始化分开操作。

三级缓存的意义是存在代理,如果只用二级缓存的话(一个存放半成品,一个存放成品),如果要是有代理对象需要一层覆 盖掉原来的,所以又加了一层,形成三级(一个存放代理用于覆盖半成品,一个存放半成品,一个存放成品)。

在普通的循环依赖的情况下,三级缓存没有任何作用。三级缓存实际上跟Spring中的AOP相关。AOP场景下的getEarlyBeanReference 会拿到一个代理的对象,但是不确定有没有依赖,需不需要用到这个依赖对象,所以先给一个工厂放到三级缓存里。

这个工厂的目的在于延迟对实例化阶段生成的对象的代理,只有真正发生循环依赖的时候,才去提前生成代理对象,否则只会创建一个工厂并将其放入到三级缓存中,但是不会去通过这个工厂去真正创建对象。

2.Spring中的循环引用是什么?

循环依赖:循环依赖其实就是循环引用,也就是两个或两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于A

循环依赖在spring中是允许存在,spring框架依据三级缓存已经解决了大部分的循环依赖

①一级缓存:单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象

②二级缓存:缓存早期的bean对象(生命周期还没走完)

③三级缓存:缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的

3.如果产生了循环依赖,那解决流程清楚嘛?

第一,先实例A对象,同时会创建ObjectFactory对象存入三级缓存singletonFactories

第二,A在初始化的时候需要B对象,这个走B的创建的逻辑

第三,B实例化完成,也会创建ObjectFactory对象存入三级缓存singletonFactories

第四,B需要注入A,通过三级缓存中获取ObjectFactory来生成一个A的对象同时存入二级缓存,这个是有两种情况,一个是可能是A的普通对象,另外一个是A的代理对象,都可以让ObjectFactory来生产对应的对象,这也是三级缓存的关键

第五,B通过从通过二级缓存earlySingletonObjects 获得到A的对象后可以正常注入,B创建成功,存入一级缓存singletonObjects

第六,回到A对象初始化,因为B对象已经创建完成,则可以直接注入B,A创建成功存入一次缓存singletonObjects

第七,二级缓存中的临时对象A清除
4.Spring 如何解决循环依赖?

Spring bean注入流程
类实例化 -> 属性注入 -> 执行初始化方法 -> (如果有需要)生成代理对象 -> 使用

三级缓存解决循环依赖流程

A、B两个类相互依赖,初始化A的时候,第一步实例化A完成(生成对象工厂实例放入三级缓存),注入依赖属性B,一级缓存查询B没有,二级缓存查询B没有,

初始化B(生成对象工厂实例放入三级缓存),注入依赖属性A,一级缓存查询A没有,二级缓存查询A没有,三级缓存查询到A的对象工厂,需要AOP增强则生成A的代理对象,没有则直接创建A实例对象,并将A放入到二级缓存,注入A的代理对象完成,生成代理对象B,B移入一级缓存。

继续A属性注入(B的代理对象),然后可能还会依赖注入C对象流程和B一致,所有依赖注入完成后A初始化,生成A的代理对象,发现A的代理对象已存在,则跳过,放入一级缓存。此时A的代理对象也是提前生成的,但是仅针对循环依赖提前生成。
下面为流程图:
在这里插入图片描述

5.如果构造方法出现了循环依赖怎么解决?

由于bean的生命周期中构造函数是第一个执行的,spring框架并不能解决构造函数的的依赖注入,可以使用@Lazy懒加载,什么时候需要对象再进行bean对象的创建

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值