一、什么是循环依赖
循环依赖指的是一个实例或多个实例存在相互依赖的关系(类之间循环嵌套引用)。
举例:
@Component
public class AService {
// A中注入了B
@Autowired
private BService bService;
}
@Component
public class BService {
// B中也注入了A
@Autowired
private AService aService;
}
上述例子中 AService 依赖于 BService,BService 也依赖了 AService,这就是两个对象之间互相依赖。循环依赖还包括 身依赖、多个实例之间相互依赖(A依赖于B,B依赖于C,C又依赖于A)。
在普通Java环境下正常运行上面的代码调用 AService 对象并不会出现问题,也就是说普通对象就算出现循环依赖也不会存在问题,因为可以将属性设置为null,那么为什么被 Spring 容器管理后的对象有循环依赖的情况会出现问题呢?
二、Spring 可以解决哪些循环依赖问题
在Spring 中,只有同时满足以下两点才能解决循环依赖的问题:
- 依赖的 Bean 必须都是单例
- 依赖注入的方式,必须不全是构造器注入,且 beanName字母序在前的不能是构造器注入
1.为什么必须是单例
如果两个Bean都是原型模式的话,那么创建A1需要创建一个B1,创建B1的时候要创建一个A2,创建A2又要创建一个B,创建B2又要创建一个A3,创建A3又要创建—个B3…
就又卡BUG了。因为原型模式都需要创建新的对象,不能用以前的对象。
如果是单例的话,创建A需要创建B,而创建的B需要的是之前的那个A。也是基于这点,Spring 就能操作操作了。具体做法就是:先创建A,此时的A是不完整的(没有注入B),用个map保存这个不完整的A,再创建B,B需要A,所以从那个map得到不完整"的A,此时的B就完整了,然后A就可以注入B,然后A就完整了,B也完整了,且它们是相互依赖的。
2.为什么不能全是构造器注入
在 Spring 中创建 Bean 分三步(详细见Spring Bean 的生命周期):
- 实例化, createBeanInstance,就是 new了个对象
- 属性注入, populateBean,就是 set 一些属性值
- 初始化, initializeBean,执行一些 aware 接口中的方法,init-Method,AOP代理等
明确了上面这三点,再结合我上面说的“不完整的”,我们来理一下。
如果全是构造器注入,比如 A(B b),那表明在new A 的时候,就需要得到 B,此时需要new B,但是 B 也是要在构造的时候注入 A,即B(A a),这时候 B 需要在一个 map 中找到不完整的 A,发现找不到。为什么找不到? 因为 A 还没 new 完呢,所以找不到完整的A,因此如果全是构造器注入的话,那么 Spring 无法处理循环依赖。而如果 A 是通过 set 注入 B 的,那么 B 在属性注入时就能注入不完整的 A 了(因为 A 虽然没有进行属性注入,但是已经实例化了),因此 B 就能完整创建 Bean,B 创建完,A 也能进行属性注入了,因此就解决了循环依赖。
三、Spring 如何解决循环依赖问题
1、三级缓存
- 一级缓存,singletonObjects,存储所有已创建完毕的单例 Bean (完整的 Bean)
- 二级缓存,earlySingletonObjects,存储所有仅完成实例化,但还未进行属性注入和初始化的Bean。
- 三级缓存,singletonFactories,存储能建立这个 Bean 的一个工厂,通过工厂能获取这个 Bean,延迟化 Bean 的生成,工厂生成的 Bean 会塞入二级缓存。
2、三个缓存如何配合解决循环依赖问题?
关键就是提前暴露未完全创建完毕的Bean。
- 首先,获取单例 Bean 的时候会通过 BeanName 先去一级缓存查找完整的 Bean,如果找到则直接返回,否则进行步骤2。
- 看对应的 Bean 是否在创建中,如果不在直接返回找不到,如果是,则会去二级缓存查找 Bean,如果找到则返回,否则进行步骤3。
- 去三级缓存通过 BeanName 查找到对应的工厂,如果存在工厂则通过工厂创建 Bean,并且放置到二级缓存中。
- 如果三个缓存都没找到,则返回null。
从上面的步骤我们可以得知,如果查询发现 Bean 还未创建,到第二步就直接返回 null,不会继续查二级和三级缓存。
返回 null 之后,说明这个 Bean 还未创建,这个时候会标记这个 Bean 正在创建中,然后再调用 createBean 来创建 Bean,而实际创建是调用方法 doCreateBean。
doCreateBean 这个方法就会执行上面我们说的三步骤:实例化、属性注入、初始化。
在实例化 Bean 之后,会往三级缓存塞入一个工厂,而调用这个工厂的 getObject 方法,就能得到这个 Bean。
3、举例说明
举例:A、B 两个类循环依赖,Spring 结合三级缓存这样解决:
- 一开始创造 A 时候查询—级缓存(里面存成品),发现没找到则看二级缓存是否在创建中(有没有半成品)。都没有则需要创建 A 的 bean,调用的是 createBean,过程分别是实例化、属性注入、初始化。
- A 实例化之后往三级缓存加入一个 A 的 getObject 方法,这个就是解决循环依赖的关键。
- 到了属性注入,因为 A 依赖 B 因此需要创建 B 。同样的路线 B 也要 createBean 。不一样的也是解决循环依赖的一环:到了属性注入,查询二级缓存的 A 为创建中,则调用三级缓存的工厂 getObject 创建一个半成品的 A,放入到二级缓存中,并完成 B 的第二步属性注入。
- 后面初始化 initializeBean,完成 B 的 Bean 创建,放到—级缓存。
回到 A 刚刚卡在属性注入,现在可以成功注入 B,然后初始化,A 也完成了 Bean 创建。(注︰成品和半成品就是没有注入所需的依赖)
为什么 A 中注入的 B 是构造器注入就不能解决?
再次结合实例解释一下:
如果 A 是构造器注入,B 是 set 注入。则说明 A 需要 B 的时刻提前了,在实例化new A(B b) 的时候就需要 B。此时 A 没有往三级缓存放 getObject,因此到了创建依赖 B 的时候,无法获取 A 的 getObject 工厂方法,只能继续 new A,造成循环依赖的死循环。
4、三级缓存有必要吗?
在上述的步骤中,三级缓存的作用就是直接返回实例化的 A,去掉三级缓存,直接从二级缓存中取出 A 也可以。所以理论上通过二级缓存就能解决循环依赖。那么三级缓存的作用是什么?
三级缓存的作用体现在:在 A 被代理的情况下,三级缓存里的 Bean 工厂有返回代理对象的能力,可以控制:1)当没有循环依赖时,会按照 Bean 的生命周期来创建 Bean(即不会用到三级缓存,在初始化后的后处理器中才会进行代理)2)当有循环依赖时,通过三级缓存提前将代理对象返回给 B。(此时的代理对象是还未进行属性注入的代理对象)