spring中的循环依赖以及spring怎么解决循环依赖的

Spring中的循环依赖

1. 问题的提出

我们有这么两个类,Boy类和Girl类。按理来说哈,男孩和女孩在热恋中,男孩的心中有女孩,女孩的心中有男孩;这是很合理的是吧,那么当我们把概念转移到spring的bean中的时候,就变成Boy类和Gril类相互作为属性注入到对方中,Boy依赖了Girl的同时,Girl也依赖的Boy,这个就叫做循环依赖。

2. 循环依赖的分类

  • setter方法/属性注入的循环依赖(单例/多例)
  • 构造方法注入的循环依赖

3. Spring能给我们解决哪些循环依赖?

  • 主bean通过属性/setter方法注入所依赖的bean且为单例,通过纯构造函数注入而造成的循环依赖spring处理不了。
  • 为什么要分主bean,这里要注意,很多博客都说什么spring解决了setter方式下的循环依赖,不算太严谨,其实setter方法和构造方法混用的情况下也可以,只不过setter方式作为主bean需要先创建对象,且为单例。

4.构造方法注入的循环依赖

  1. 首先我们先准备上面提到的两个类,Boy类和Girl类,并且编写好有参构造,getter/setter方法等

    Boy类:

    public class Boy {
    
        private String name;
        private Girl girl;
    
        public Boy() {
        }
    
        public Boy(String name, Girl girl) {
            this.name = name;
            this.girl = girl;
        }
    
        public void setGirl(Girl girl) {
            this.girl = girl;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    
        public void have(){
            System.out.println(name + "的心里都是" + girl.getName());
        }
    }
    

    Girl类:

    public class Girl {
    
        private String name;
        private Boy boy;
    
        public Girl() {
        }
    
        public Girl(String name, Boy boy) {
            this.name = name;
            this.boy = boy;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public void setBoy(Boy boy) {
            this.boy = boy;
        }
    
        public String getName() {
            return name;
        }
    
        public void have(){
            System.out.println(name + "的心里都是" + boy.getName());
        }
    }
    
  2. 配置文件创建bean,方法通过构造方法注入

    <bean id="boy" class="com.wjw.pojo.Boy">
        <constructor-arg name="name" value="wjw"></constructor-arg>
        <constructor-arg name="girl" ref="girl"></constructor-arg>
    </bean>
    <bean id="girl" class="com.wjw.pojo.Girl">
        <constructor-arg name="name" value="ikaros"></constructor-arg>
        <constructor-arg name="boy" ref="boy"></constructor-arg>
    </bean>
    
  3. 编写测试类Test,测试方法

    @Test
    public void test(){
        ApplicationContext applicationContext =
            new ClassPathXmlApplicationContext("bean.xml");
    
        Boy boy = applicationContext.getBean("boy", Boy.class);
        Girl girl = applicationContext.getBean("girl", Girl.class);
    
        boy.have();
        girl.have();
    }
    
  4. 运行结果,可以发现出现异常:“org.springframework.beans.factory.BeanCreationException:创建类路径资源[bean.xml]中定义的名称为“boy”的bean时出错:设置构造函数参数时无法解析对bean“girl”的引用;嵌套异常为org.springframework.beans.factory.BeanCreationException:创建类路径资源[bean.xml]中定义的名称为“girl”的bean时出错:设置构造函数参数时无法解析对bean“boy”的引用;嵌套异常为org.springframework.beans.factory.BeanCurrentlyInCreationException:创建名为“boy”的bean时出错:请求的bean当前正在创建中:是否存在无法解决的循环引用”
    在这里插入图片描述
    为什么会这样呢?这里我们保留疑问,到后面setter方法注入的循环依赖演示完后一并解释。

5.setter方法注入的循环依赖

  1. 我们复用上面的类,更改配置文件中的注入方式,改为setter方法注入。

     <bean id="boy" class="com.wjw.pojo.Boy" scope="singleton">
         <property name="name" value="wjw"></property>
         <property name="girl" ref="girl"></property>
    </bean>
    <bean id="girl" class="com.wjw.pojo.Girl" scope="singleton">
        <property name="name" value="ikaros"></property>
        <property name="boy" ref="boy"></property>
    </bean>
    
  2. 同样运行,运行结果:

  3. 注意:我特意添加了bean的scope属性为单例,当我将bean的作用域全改成scope="prototype",此时的运行结果为,出现了和构造方法注入一样的异常,无法解决的循环引用

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xxwPBR8V-1689750671214)(assets/image-20230717193842889.png)]

6.为什么无参构造以及多例情况下会出现异常?

  • 在spring解析xml或者注解时,它创建bean大致可以分为两个部分:
    • (1)调用构造方法实例化bean,在这里又可以细分为两步:
      • 调用构造方法
      • 在内存(spring中是缓存)中开辟该bean的内存空间地址,这一步才算实例化完成。
    • (2)为改bean注入属性值,这里底层会调用populateBean方法进行属性赋值

当我们使用构造方法注入时,

  1. 假如先创建Boy的bean对象,那么顺序就是,调用有参构造方法实例化,发现我实例化必须要注入属性值;
  2. 先注入name的值为wjw,随后注入girl的值,注入的时候发现,Girl的bean对象还未实例化,接着spring就去创建Girl类的bean,调用Girl的构造方法开始实例化;首先注入属性name的值为ikaros;
  3. 接着注入boy属性的值,发现Boy同样还未实例化,因为它还等着你Girl实例化后注入值才能完成构造方法的调用,接着去缓存中开辟内存空间好让Boy构造方法中的属性girl指向内存地址引用。
  4. 这样就造成了一个类似于线程死锁的状况,所以程序报错。

图解:

而当我们使用setter方法注入时,bean的作用域全是多例的话,原理其实也是差不多,但是还是有一些小小的差别(好吧差别还是有点大的)。假定Boy的bean先开始创建,无参构造调用实例化Boy,在内存中成功开辟了内存中开辟了空间拥有地址指向。好,这个时候开始属性注入girl,spring开始创建Girl的bean对象,Girl开始实例化,实例化成功,开始注入boy属性时就出现问题了,因为作用域是多例的,所以spring又会开始创建一个新的Boy的bean对象,开辟一个新的内存空间,在创建对象的时候又会注入girl属性,这个时候又会再去创建一个Girl对象,开辟一个内存空间。就这样,会形成一个类似于死循环的局面,最终出现异常。

7.为什么spring可以解决setter&singleton方法注入

  • 其根本原因在于:这种方式可以将“实例化bean”和“给bean属性赋值”这两个动作分开去做。实例化bean的时候,我们此时可以先不给属性赋值,此时的对象虽然属性没有赋值,但是拥有内存地址可以作为对象引用,也就是说我们给对象类型的属性赋值时,实际是添加该对象内存中的引用

  • 在我的理解,spring创建bean的时候,大致可以分为三步:

    1. 调用无参构造开始实例化
    2. 内存中开辟空间,标志实例化成功,其对象能够指向内存地址
    3. 调用setter方法开始给属性赋值

    spring中是将一个个BeanDefinition对象通过反射转成bean对象,同时,spring中对bean生命周期的管理大体是通过AbstractAutowireCapableBeanFactory中的doCreateBean()方法。对应我以上的三步就是:

    调用无参构造方法 —> Object bean = instanceWrapper.getWrappedInstance();

    开辟内存空间 —> addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

    属性赋值 —> populateBean(beanName, mbd, instanceWrapper);

  • 在内存开辟空间这一环节,spring使用了三级缓存来解决循环依赖的问题,众所周知,缓存是内存的一部分,且是以map形式存储数据,其中spring的三级缓存中的key全部都是beanName

    1. 一级缓存:private final Map<String, Object> singletonObjects
    2. 二级缓存:private final Map<String, Object> earlySingletonObjects
    3. 三级缓存:private final Map<String, ObjectFactory<?>> singletonFactories

    其中,一级缓存存储:单例bean对象(singletonObject),完整的单例bean对象,已经完成实例化和属性赋值

    二级缓存存储:被曝光的单例bean对象,已经实例化但是其属性还没赋值,是一个空壳bean,这里面的bean 只能确保已经进行了实例化,但是属性赋值跟初始化还没有做完,因此该 bean 还没创建完成,仅仅能作为指针提前曝光,被其他 bean 所引用。

    三级缓存存储:单例工厂对象,里面存储了大量的工程对象,每一个单例bean对应一个单例工厂对象,在这里bean其实就已经实例化完成,并提前曝光存入到三级缓存中。

  • 分析:

    先看图示:

    解释:

    1. spring开始创建Boy类的bean,调用无参构造实例化bean对象
    2. 开辟内存空间,将Boy类的bean对象存入三级缓存
    3. 开始属性赋值,name属性赋值,girl属性赋值
    4. 发现Girl类的bean还未创建,无法完成属性赋值
    5. spring开始创建Girl类的bean,调用无参构造实例化bean对象
    6. 开辟内存空间,将Girl类的bean对象存入三级缓存
    7. 开始属性赋值,name属性赋值,boy属性赋值
    8. Boy类的bean对象已经实例化,开始从一级缓存里面找,没有,到二级缓存里面找,没有,最终在三级缓存里面找到
    9. 将Boy类的bean对象曝光存入二级缓存,并删除三级缓存中的singleFactory对象
    10. 存入二级缓存标志着可以进行boy属性的赋值,Girl类的bean属性赋值成功,成为一个完成的bean,将其存入一级缓存,在三级缓存中删除。
    11. 此时Boy类的bean也可以完成girl属性赋值,将其存入一级缓存,在二级缓存中删除。

    至此,两个bean对象创建成功,可以使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值