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.构造方法注入的循环依赖
首先我们先准备上面提到的两个类,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()); } }
配置文件创建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>
编写测试类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(); }
运行结果,可以发现出现异常:“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方法注入的循环依赖
我们复用上面的类,更改配置文件中的注入方式,改为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>
同样运行,运行结果:
注意:我特意添加了bean的scope属性为单例,当我将bean的作用域全改成
scope="prototype"
时,此时的运行结果为,出现了和构造方法注入一样的异常,无法解决的循环引用。
6.为什么无参构造以及多例情况下会出现异常?
- 在spring解析xml或者注解时,它创建bean大致可以分为两个部分:
- (1)调用构造方法实例化bean,在这里又可以细分为两步:
- 调用构造方法
- 在内存(spring中是缓存)中开辟该bean的内存空间地址,这一步才算实例化完成。
- (2)为改bean注入属性值,这里底层会调用populateBean方法进行属性赋值
- (1)调用构造方法实例化bean,在这里又可以细分为两步:
当我们使用构造方法注入时,
- 假如先创建Boy的bean对象,那么顺序就是,调用有参构造方法实例化,发现我实例化必须要注入属性值;
- 先注入name的值为wjw,随后注入girl的值,注入的时候发现,Girl的bean对象还未实例化,接着spring就去创建Girl类的bean,调用Girl的构造方法开始实例化;首先注入属性name的值为ikaros;
- 接着注入boy属性的值,发现Boy同样还未实例化,因为它还等着你Girl实例化后注入值才能完成构造方法的调用,接着去缓存中开辟内存空间好让Boy构造方法中的属性girl指向内存地址引用。
- 这样就造成了一个类似于线程死锁的状况,所以程序报错。
图解:
而当我们使用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的时候,大致可以分为三步:
- 调用无参构造开始实例化
- 在内存中开辟空间,标志实例化成功,其对象能够指向内存地址
- 调用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
- 一级缓存:private final Map<String, Object> singletonObjects
- 二级缓存:private final Map<String, Object> earlySingletonObjects
- 三级缓存:private final Map<String, ObjectFactory<?>> singletonFactories
其中,一级缓存存储:单例bean对象(singletonObject),完整的单例bean对象,已经完成实例化和属性赋值
二级缓存存储:被曝光的单例bean对象,已经实例化但是其属性还没赋值,是一个空壳bean,这里面的bean 只能确保已经进行了实例化,但是属性赋值跟初始化还没有做完,因此该 bean 还没创建完成,仅仅能作为指针提前曝光,被其他 bean 所引用。
三级缓存存储:单例工厂对象,里面存储了大量的工程对象,每一个单例bean对应一个单例工厂对象,在这里bean其实就已经实例化完成,并提前曝光存入到三级缓存中。
-
分析:
先看图示:
解释:
- spring开始创建Boy类的bean,调用无参构造实例化bean对象
- 开辟内存空间,将Boy类的bean对象存入三级缓存
- 开始属性赋值,name属性赋值,girl属性赋值
- 发现Girl类的bean还未创建,无法完成属性赋值
- spring开始创建Girl类的bean,调用无参构造实例化bean对象
- 开辟内存空间,将Girl类的bean对象存入三级缓存
- 开始属性赋值,name属性赋值,boy属性赋值
- Boy类的bean对象已经实例化,开始从一级缓存里面找,没有,到二级缓存里面找,没有,最终在三级缓存里面找到
- 将Boy类的bean对象曝光存入二级缓存,并删除三级缓存中的singleFactory对象
- 存入二级缓存标志着可以进行boy属性的赋值,Girl类的bean属性赋值成功,成为一个完成的bean,将其存入一级缓存,在三级缓存中删除。
- 此时Boy类的bean也可以完成girl属性赋值,将其存入一级缓存,在二级缓存中删除。
至此,两个bean对象创建成功,可以使用。