在关于Spring的面试中,我们经常会被问到一个问题:Spring是如何解决循环依赖的问题的。
一、问题的提出
spring循环依赖包括:构造器循环依赖、field属性注入循环依赖
1、构造器循环依赖
@Service
public class A {
public A(B b) { }
}
@Service
public class B {
public B(C c) {
}
}
@Service
public class C {
public C(A a) { }
}
结果:项目启动失败,发现了一个cycle
2、field属性注入循环依赖
@Service
public class A1 {
@Autowired
private B1 b1;
}
@Service
public class B1 {
@Autowired
public C1 c1;
}
@Service
public class C1 {
@Autowired public A1 a1;
}
结果:项目启动成功
3、field属性注入循环依赖(prototype)
@Service
@Scope("prototype")
public class A1 {
@Autowired
private B1 b1;
}
@Service
@Scope("prototype")
public class B1 {
@Autowired
public C1 c1;
}
@Service
@Scope("prototype")
public class C1 {
@Autowired public A1 a1;
}
结果:项目启动失败,发现了一个cycle
结论:
1、构造器注入和prototype类型的field注入发生循环依赖时都无法初始化
2、field注入单例的bean时,尽管有循环依赖,但bean仍然可以被成功初始化
二、Spring创建Bean的过程
Spring创建好了BeanDefinition之后呢,会开始实例化Bean,并且对Bean的依赖属性进行填充。Spring实例化bean是通过ApplicationContext.getBean()方法来进行的,实例化时底层使用了CGLIB或Java反射技术。
要理解两点:
1、Spring是通过递归的方式获取目标bean及其所依赖的bean的;
2、Spring实例化一个bean的时候,是分两步进行的,首先实例化目标bean,然后为其注入属性。
也就是说,Spring在实例化一个bean的时候,是首先递归的实例化其所依赖的所有bean,直到某个bean没有依赖其他bean,此时就会将该实例返回,然后反递归的将获取到的bean设置为各个上层bean的属性的。
实例化过程中有一个重要的类DefaultListableBeanFactory,它有三个Map类型的属性,是解决问题的关键,这三个Map实际上是bean的缓存。对于单例来说,在Spring容器整个生命周期内,有且只有一个对象,所以很容易想到这个对象应该存在Cache中,Spring为了解决单例的循环依赖问题,使用了三级缓存,就是DefaultListableBeanFactory类的这三个Map。
三、单例注入bean是如何解决循环依赖问题呢?
假设循环注入是A-B-A:A依赖B(A中autowire了B),B又依赖A(B中又autowire了A):
@Component
public class A {
private B b;
public void setB(B b) {
this.b = b;
}
}
@Component
public class B {
private A a;
public void setA(A a) {
this.a = a;
}
}
根据spring创建bean的流程,解决循环依赖的步骤大概是这样(假设容器首先初始化A对象):
1、首先Spring尝试通过ApplicationContext.getBean()方法获取A对象的实例,由于Spring容器缓存中还没有A对象实例,因而其会创建一个A对象,此时A并没有注入属性,也可以认为是半成品
2、A初始化后,Spring将在这个还没有设置属性的A放入缓存中
3、然后发现其依赖了B对象,因而会尝试递归的通过ApplicationContext.getBean()方法获取B对象的实例,但是Spring容器缓存中此时也没有B对象的实例,因而其还是会先创建一个B对象的实例。
4、此时A对象和B对象都已经创建了,并且保存在Spring容器中了,只不过A对象的属性b和B对象的属性a都还没有设置进去。
5、Spring创建B对象之后,Spring发现B对象依赖了属性A,因而还是会尝试递归的调用ApplicationContext.getBean()方法获取A对象的实例。
6、因为Spring中已经有一个A对象的实例,虽然只是半成品(其属性b还未初始化),但其也还是目标bean,因而会将该A对象的实例返回。
7、B对象的属性a就设置进去了,然后还是ApplicationContext.getBean()方法递归的返回,也就是将B对象的实例返回,此时就会将该实例设置到A对象的属性b中。
8、此时A对象的属性b和B对象的属性a都已经设置了目标对象的实例了
注意:前面在为对象B设置属性a的时候,这个A类型属性还是个半成品。但是这个A是一个引用,其本质上还是最开始就实例化的A对象,其引用地址是同一个。
四、为什么prototype类型的和构造器类型的Spring无法解决循环依赖呢?
答案也很简单,因为A中构造器注入了B,那么A在关键的方法addSingletonFactory()把A放入singletonFactories之前就去初始化了B,导致三级缓存中根本没有A,所以会发生死循环,Spring发现之后就抛出异常了。至于Spring是如何发现异常的呢,本质上是根据Bean的状态给Bean进行mark,如果递归调用时发现bean当时正在创建中,那么就抛出循环依赖的异常即可。