Spring 循环依赖原理及解决方案

一、什么是循环依赖

循环依赖指的是一个实例或多个实例存在相互依赖的关系(类之间循环嵌套引用)。
举例:

@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 中,只有同时满足以下两点才能解决循环依赖的问题:

  1. 依赖的 Bean 必须都是单例
  2. 依赖注入的方式,必须不全是构造器注入,且 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 的生命周期):

  1. 实例化, createBeanInstance,就是 new了个对象
  2. 属性注入, populateBean,就是 set 一些属性值
  3. 初始化, 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、三级缓存

  1. 一级缓存,singletonObjects,存储所有已创建完毕的单例 Bean (完整的 Bean)
  2. 二级缓存,earlySingletonObjects,存储所有仅完成实例化,但还未进行属性注入和初始化的Bean。
  3. 三级缓存,singletonFactories,存储能建立这个 Bean 的一个工厂,通过工厂能获取这个 Bean,延迟化 Bean 的生成,工厂生成的 Bean 会塞入二级缓存。

2、三个缓存如何配合解决循环依赖问题?

关键就是提前暴露未完全创建完毕的Bean。

  1. 首先,获取单例 Bean 的时候会通过 BeanName 先去一级缓存查找完整的 Bean,如果找到则直接返回,否则进行步骤2。
  2. 看对应的 Bean 是否在创建中,如果不在直接返回找不到,如果是,则会去二级缓存查找 Bean,如果找到则返回,否则进行步骤3。
  3. 三级缓存通过 BeanName 查找到对应的工厂,如果存在工厂则通过工厂创建 Bean,并且放置到二级缓存中。
  4. 如果三个缓存都没找到,则返回null。

从上面的步骤我们可以得知,如果查询发现 Bean 还未创建,到第二步就直接返回 null,不会继续查二级和三级缓存。

返回 null 之后,说明这个 Bean 还未创建,这个时候会标记这个 Bean 正在创建中,然后再调用 createBean 来创建 Bean,而实际创建是调用方法 doCreateBean。
doCreateBean 这个方法就会执行上面我们说的三步骤:实例化、属性注入、初始化。

在实例化 Bean 之后,会往三级缓存塞入一个工厂,而调用这个工厂的 getObject 方法,就能得到这个 Bean。

3、举例说明

举例:A、B 两个类循环依赖,Spring 结合三级缓存这样解决:

  1. 一开始创造 A 时候查询—级缓存(里面存成品),发现没找到则看二级缓存是否在创建中(有没有半成品)。都没有则需要创建 A 的 bean,调用的是 createBean,过程分别是实例化、属性注入、初始化。
  2. A 实例化之后往三级缓存加入一个 A 的 getObject 方法,这个就是解决循环依赖的关键。
  3. 到了属性注入,因为 A 依赖 B 因此需要创建 B 。同样的路线 B 也要 createBean 。不一样的也是解决循环依赖的一环:到了属性注入,查询二级缓存的 A 为创建中,则调用三级缓存的工厂 getObject 创建一个半成品的 A,放入到二级缓存中,并完成 B 的第二步属性注入。
  4. 后面初始化 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。(此时的代理对象是还未进行属性注入的代理对象)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值