2023.11.10
通俗来讲,循环依赖指的是一个实例或多个实例存在相互依赖的关系(类之间循环嵌套引用)。比如:丈夫类Husband,妻子类Wife。Husband中有Wife的引用。Wife中有Husband的引用。
正常调用这两对象不会出现问题,但是被 Spring 容器管理后的对象就会出现循环依赖问题。
singleton下的set注入产生的循环依赖
下面先测试一下在singleton+setter的模式下产生的循环依赖,Spring是否能够解决。
定义相互依赖的两个类husband和wife:
package spring6.bean;
public class husband {
private String name;
private wife wife;
public void setName(String name) {
this.name = name;
}
public void setWife(spring6.bean.wife wife) {
this.wife = wife;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "husband{" +
"name='" + name + '\'' +
", wife=" + wife.getName() +
'}';
}
}
package spring6.bean;
public class wife {
private String name;
private husband husband;
public void setName(String name) {
this.name = name;
}
public void setHusband(spring6.bean.husband husband) {
this.husband = husband;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "wife{" +
"name='" + name + '\'' +
", husband=" + husband.getName() +
'}';
}
}
在xml文件配置:
<bean id="husbandBean" class="spring6.bean.husband" scope="singleton">
<property name="name" value="艾弗森"/>
<property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="spring6.bean.wife" scope="singleton">
<property name="name" value="卡戴珊"/>
<property name="husband" ref="husbandBean"/>
</bean>
测试程序:
@Test
public void testSingletonAndSet(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("cycle-depend.xml");
husband husbandBean = applicationContext.getBean("husbandBean", husband.class);
wife wifeBean = applicationContext.getBean("wifeBean",wife.class);
System.out.println(husbandBean);
System.out.println(wifeBean);
}
运行结果:
通过测试得知:在singleton + set注入的情况下,循环依赖是没有问题的。Spring可以解决这个问题。
根据bean的生命周期可以知道,spring容器当中的bean是先被实例化的 实例化之后才是属性初始化,所以当husband需要给wife属性赋值的时候 wife这个bean已经提前曝光了而且还是单例的,所以可以解决循环依赖问题。
prototype下的set注入产生的循环依赖
再来测试一下prototype+set注入的方式下,循环依赖会不会出现问题,只需要将配置文件的两个singleton都改为prototype即可。 运行测试程序:
发现结果会报:BeanCreationException 异常
再次测试,发现如果只改一个singleton为prototype,也可以正常运行。
singleton下的构造注入产生的循环依赖
测试一下singleton + 构造注入的方式下,spring是否能够解决这种循环依赖。
不放代码了,直接说结论:不行。
因为构造方法注入会导致实例化对象的过程和对象属性赋值的过程没有分离开,这两过程是同时进行的,而之前set注入中,两个过程是分离开的,这也是set注入能解决循环依赖的根本原因。
Spring解决循环依赖的机理
根本原因在于:这种方式可以做到将“实例化Bean”和“给Bean属性赋值”这两个动作分开去完成。
在实例化Bean的时候:调用无参数构造方法来完成。此时可以先不给属性赋值,可以提前将该Bean对象“曝光”给外界。给Bean属性赋值的时候:调用setter方法来完成。
两个步骤是完全可以分离开去完成的,并且这两步不要求在同一个时间点上完成。
也就是说,Bean都是单例的,我们可以先把所有的单例Bean实例化出来,放到一个集合当中(我们可以称之为缓存),所有的单例Bean全部实例化完成之后,以后我们再慢慢的调用setter方法给属性赋值。这样就解决了循环依赖的问题。
总结:Spring只能解决setter方法注入的单例bean之间的循环依赖。
ClassA依赖ClassB,ClassB又依赖ClassA,形成依赖闭环。Spring在创建ClassA对象后,不需要等给属性赋值,直接将其曝光到bean缓存当中。在解析ClassA的属性时,又发现依赖于ClassB,再次去获取ClassB,当解析ClassB的属性时,又发现需要ClassA的属性,但此时的ClassA已经被提前曝光加入了正在创建的bean的缓存中,则无需创建新的的ClassA的实例,直接从缓存中获取即可。从而解决循环依赖问题。
Spring的三级缓存
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
一级缓存:存储的是完整的单例Bean对象,这个Bean对象已经赋值过了。
二级缓存:存储的是早期的单例Bean对象,这个Bean对象属性还没有赋值。
三级缓存:存储的是单例工厂对象,每一个单例Bean对象都会对应一个单例工厂对象。
spring会先从一级缓存中获取Bean,如果获取不到,则从二级缓存中获取Bean,如果二级缓存还是获取不到,则从三级缓存中获取之前曝光的ObjectFactory对象,通过ObjectFactory对象获取Bean实例,这样就解决了循环依赖的问题。