一、什么是循环依赖?
Spring循环依赖指的是两个或多个Bean之间相互依赖,形成一个环状依赖的情况。通俗的说,就是A依赖B,B依赖C,C依赖A,这样就形成了一个循环依赖的环。
Spring循环依赖通常会导致Bean无法正确地被实例化,从而导致应用程序无法正常启动或者出现异常。因此,Spring循环依赖是一种需要尽量避免的情况。
二、循环依赖的分类
循环依赖的分类可以根据不同的维度进行划分,但主要可以从依赖注入的方式和Spring Bean的作用域两个方面来详细讨论。
根据依赖注入的方式不同
循环依赖可以分为以下几种类型:
1、构造器循环依赖
构造器循环依赖是指两个或多个Bean通过构造器参数相互依赖。例如,A的构造器需要B作为参数,而B的构造器又需要A作为参数,这就形成了一个闭环。
解决方式:构造器循环依赖在Spring中通常无法解决,因为构造器注入是在Bean实例化时就需要确定依赖关系,如果存在循环依赖,则无法实例化任何一个Bean,Spring会抛出BeanCurrentlyInCreationException
异常。
@Component
public class A {
private B b;
public A(B b) {
this.b = b;
}
}
@Component
public class B {
private A a;
public B(A a) {
this.a = a;
}
}
2、属性(set)循环依赖
属性循环依赖是指两个或多个Bean通过属性相互依赖,例如,A有一个setter方法用于注入B,而B也有一个set方法用于注入A。
解决方式:Spring通过三级缓存机制来解决这种类型的循环依赖。在Bean的实例化过程中,Spring会先将部分初始化的Bean提前暴露到早期曝光对象池中,以便其他Bean能够引用,从而解决循环依赖问题。
public class A {
private B b;
public void setB(B b) {
this.b = b;
}
}
public class B {
private A a;
public void setA(B b) {
this.b = b;
}
}
根据Spring Bean的作用域分类
1、单例循环依赖
单例Bean在Spring容器中是唯一的实例,被多个组件共享。单例Bean之间的循环依赖问题,如上文所述,Spring主要通过三级缓存机制来解决。
singleton下的构造注入产生的循环依赖
实体类
public class A {
private String name;
private B b;
public A(String name, B b) {
this.name = name;
this.b = b;
}
public A(String name) {
this.name = name;
}
public A(B b) {
this.b = b;
}
public A() {
}
@Override
public String toString() {
return "A{" +
"name='" + name + '\'' +
", b=" + b +
'}';
}
}
public class B {
private String name;
private A a;
public B() {
}
public B(String name, A a) {
this.name = name;
this.a = a;
}
public B(String name) {
this.name = name;
}
public B(A a) {
this.a = a;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "B{" +
"name='" + name + '\'' +
", a=" + a +
'}';
}
}
xml文件
<bean id="a" class="com.ape.pojo.A" scope="singleton">
<constructor-arg name="name" value="我是A"/>
<constructor-arg name="b" ref ="b"/>
</bean>
<bean id="b" class="com.ape.pojo.B" scope="singleton">
<constructor-arg name="name" value="我是B"/>
<constructor-arg name="a" ref ="a"/>
</bean>
测试类
public class Test01 {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
A a = context.getBean("a", A.class);
B b = context.getBean("b", B.class);
System.out.println(a);
System.out.println(b);
}
}
测试结果为
产生错误:创建名称为“a”的 Bean 时出错:请求的 Bean 当前正在创建中:是否存在无法解析的循环引用?
Spring是无法解决这种循环依赖的。
主要原因是因为通过构造方法注入导致的:因为构造方法注入会导致实例化对象的过程和对象属性赋值的过程没有分离开,必须在一起完成导致的。
singleton下的set注入产生的循环依赖
实体类
public class A {
private String name;
private B b;
public void setName(String name) {
this.name = name;
}
public void setB(B b) {
this.b = b;
}
public String getName() {
return name;
}
public B getB() {
return b;
}
// toString()方法重写时需要注意:不能直接输出husband,输出husband.getName()。要不然会出现递归导致的栈内存溢出错误。
@Override
public String toString() {
return "A{" +
"name='" + name + '\'' +
", b=" + b.getName() +
'}';
}
}
public class B {
private String name;
private A a;
public void setName(String name) {
this.name = name;
}
public void setA(A a) {
this.a = a;
}
public String getName() {
return name;
}
public A getA() {
return a;
}
// toString()方法重写时需要注意:不能直接输出husband,输出husband.getName()。要不然会出现递归导致的栈内存溢出错误。
@Override
public String toString() {
return "B{" +
"name='" + name + '\'' +
", a=" + a.getName() +
'}';
}
}
xml文件
<bean id="a" class="com.ape.pojo.A" scope="singleton">
<property name="name" value="我是a"></property>
<property name="b" ref="b"></property>
</bean>
<bean id="b" class="com.ape.pojo.B" scope="singleton">
<property name="name" value="我是b"></property>
<property name="a" ref="a"></property>
</bean>
测试类
public class Test01 {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
A a = context.getBean("a", A.class);
B b = context.getBean("b", B.class);
System.out.println(a);
System.out.println(b);
}
}
测试结果为
通过测试得知:在singleton + set注入的情况下,循环依赖是没有问题的。Spring可以解决这个问题。
2、原型循环依赖
原型(Prototype)作用域的Bean在每次请求时都会创建一个新的实例。由于每次请求都会创建新的实例,因此原型Bean之间的循环依赖问题在理论上不会发生,因为每个Bean都是独立的实例。然而,如果在实际应用中通过其他方式(如手动管理依赖)造成了类似循环依赖的情况,那么就需要开发者自行解决。
prototype下的set注入产生的循环依赖
实体类和测试类如上就不重复展示了。
xml文件
<bean id="a" class="com.ape.pojo.A" scope="prototype">
<property name="name" value="我是a"></property>
<property name="b" ref="b"></property>
</bean>
<bean id="b" class="com.ape.pojo.B" scope="prototype">
<property name="name" value="我是b"></property>
<property name="a" ref="a"></property>
</bean>
测试结果为
产生错误:创建名称为“a”的 Bean 时出错:请求的 Bean 当前正在创建中:是否存在无法解析的循环引用?
Spring是无法解决这种循环依赖的。
三、Spring的源码分析
Spring为什么可以解决set + singleton模式下循环依赖?
根本原因:这种方式可以做到将“实例化Bean”和“给Bean属性赋值”这两个动作分开去完成。实例化Bean的时候:调用无参数构造方法来完成。此时可以先不给属性赋值,可以提前将该Bean对象“曝光”给外界。给Bean属性赋值的时候:调用set方法来完成。
两个步骤是完全可以分离开去完成的,并且这两步不要求在同一个时间点上完成。也就是说,Bean都是单例的,我们可以先把所有的单例Bean实例化出来,放到一个集合当中(我们可以称之为缓存),所有的单例Bean全部实例化完成之后,以后我们再慢慢的调用setter方法给属性赋值,这样就解决了循环依赖的问题!
以下是它的源码分析
我们进入DefaultSingletonBeanRegistry类中查看底层
①一级缓存存储的是:单例Bean对象,完整的单例Bean对象,也就是说这个缓存中的Bean对象的属性都已经赋值了;是一个完整的Bean对象(已经赋值)。
②二级缓存存储的是:早期的单例Bean对象,这个缓存中的单例Bean对象的属性没有赋值,只是一个早期的实例对象(没有赋值)。
③三级缓存存储的是:单例工厂对象,这个里面存储了大量的“工厂对象”,每一个单例Bean对象都会对应一个单例工厂对象;这个集合中存储的是,创建该单例对象时对应的那个单例工厂对象。
在该类中有一个addSingletonFactory()方法,这个方法的作用是:将创建Bean对象的ObjectFactory对象提前曝光,解决循环依赖的问题,singleton+set注入能够解决循环依赖问题,就是利用三级缓存SingletonFctories进行曝光!。
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 先查一级缓存
Object singletonObject = this.singletonObjects.get(beanName);
// 没有一级缓存,看bean是否正在创建中
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
// 再查二级缓存
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
// 最后查三级缓存
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 从三级缓存中根据beanName取出的value是ObjectFactory对象,执行它的方法。
singletonObject = singletonFactory.getObject();
// 放入二级缓存中
this.earlySingletonObjects.put(beanName, singletonObject);
// 移除对应的三级缓存
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}