一文深入分析阿里Java二面神仙问题:Spring循环依赖

  • 作者: 凯哥Java(公众号:凯哥Java)
  • spring
  • 时间:2021-06-04 18:33
  • 4453人已阅读
简介 什么是循环依赖?举个例子/** * A 类,引入 B 类的属性 b */public class A {  private B b;}/** * B 类,引入 A 类的属性 a */publ

🔔🔔好消息!好消息!🔔🔔

 如果您需要注册ChatGPT,想要升级ChatGPT4。凯哥可以代注册ChatGPT账号代升级ChatGPT4

有需要的朋友👉:微信号 kaigejava2022

什么是循环依赖?

举个例子

/**
 * A 类,引入 B 类的属性 b
 */public class A {
  private B b;}/**
 * B 类,引入 A 类的属性 a
 */public class B {
  private A a;}

再看个简单的图:

68617ffb57da83e8daad3415bc8ffe92.png

像这样,创建 a 的时候需要依赖 b,那就创建 b,结果创建 b 的时候又需要依赖 a,那就创建 a,创建 a 的时候需要依赖 b,那就创建 b,结果创建 b 的时候又需要依赖 a ……

互相依赖何时了,死循环了吧? 诺,这就是循环依赖!


循环依赖其实不算个问题或者错误,我们实际在开发的时候,也可能会用到。 再拿最开始的 A 和 B 来说,我们手动使用的时候会用以下方式:

A a = new A();
  B b = new B();
  b.setA(a);
  a.setB(b);

其实这样就解决了循环依赖,功能上是没有问题的,但是为什么 Spring 要解决循环依赖?

为什么 Spring 要解决循环依赖?

首先简单了解下,我们用 Spring 框架,它帮我们做了什么事情?总结上来说,六字真言:IoC 和 AOP

由于 Spring 解决循环依赖是考虑到 IoC 和 AOP 相关知识了,所以这里我先提一下。

由于本文主要的核心是 Spring 的循环依赖处理,所以不会对 IoC 和 AOP 做详细的说明,想了解以后有机会再说

IoC,主要是将对象的创建、管理都交给了 Spring 来管理,能够解决对象之间的耦合问题,对开发人员来说也是省时省力的。

AOP,主要是在不改变原有业务逻辑情况下,增强横切逻辑代码,也是解耦合,避免横切逻辑代码重复;也是对 OOP 的延续、补充。

既然类的实例化都交给了 Spring 来管理了,那么循环依赖 Spring 肯定也要考虑到怎么去处理(怎么总觉得有点像是废话 )。

解决循环依赖的方式

参考我们能想到的肯定是手动处理的方式,先将对象都 new 出来,然后进行 set 属性值,而 Spring 也是通过这样的形式来处理的(你说巧不巧?其实一点都不巧 ,后面再说为什么),其实 Spring 管理 Bean 的实例化底层其实是由反射实现的。

而我们实例化的方式也有好多种,比如通过构造函数,一次性将属性赋值,像下面这样

// 假设有学生这个类public class Student {
  private int id;
  private String name;

  public Student(int id, String name) {
    this.id = id;
    this.name = name;
  }}// 通过构造器方式实例化并赋值new Student(1, "Suremotoo");

但是使用构造器这样的方式,是无法解决循环依赖的!为什么不能呢?

我们还是以文中开头的 A 和 B 互相依赖来说, 要通过构造器的方式实现 A 的实例化,如下

new A(b);

Wow,是不是发现问题了?要通过构造器的方式,首先要将属性值实例化出来啊!A 要依赖属性 b,就需要先将 B 实例化,可是 B 的实例化是不是还是需要依赖 A❗️ 这不就是文中开头描述的样子嘛,所以通过构造器的方式,Spring 也没有办法解决循环依赖

我们使用 set 可以解决,那么 Spring 也使用 set 方式呢?答案是可以的。

既然底层是通过反射实现的,我们自己也用反射实现的话,大概思路是这样的(还是以 A 和 B 为例)

  1. 先实例化 A 类

  2. 再实例化 B 类

  3. set B 类中的 a 属性

  4. set A 类中的 b 属性

其实就是通过反射,实现以下代码

      A a = new A();
      B b = new B();
      b.setA(a);
      a.setB(b);

这里可以稍微说明一下,为什么这样可以?

A a = new A(),说明 A 只是实例化,还未初始化

同理,B b = new B() 也只是实例化,并未初始化

a.setB(b);, 对 a 的属性赋值,完成 a 的初始化

b.setA(a);, 对 b 的属性赋值,完成 b 的初始化

现在是不是有点感觉了,先把狗骗进来,再杀

Spring 如何解决循环依赖问题

先上个通俗的答案解释,三级缓存

  /**
   * 单例对象的缓存:bean 名称——bean 实例,即:所谓的单例池。
   * 表示已经经历了完整生命周期的 Bean 对象
   * <b>第一级缓存</b>
   */
  Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

  /**
   * 早期的单例对象的高速缓存:bean 名称——bean 实例。
   * 表示 Bean 的生命周期还没走完(Bean 的属性还未填充)就把这个 Bean 存入该缓存中
   * 也就是实例化但未初始化的 bean 放入该缓存里
   * <b>第二级缓存</b>
   */
  Map<String, Object> earlySingletonObjects = new HashMap<>(16);

  /**
   * 单例工厂的高速缓存:bean 名称——ObjectFactory。
   * 表示存放生成 bean 的工厂
   * <b>第三级缓存</b>
   */
  Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

代码中注释可能不清晰,我再给贴一下三级缓存:

第一级缓存(也叫单例池):Map<String, Object> singletonObjects,存放已经经历了完整生命周期的 Bean 对象

第二级缓存:Map<String, Object> earlySingletonObjects,存放早期暴露出来的 Bean 对象,Bean 的生命周期未结束(属性还未填充完)

第三级缓存:Map<String, ObjectFactory<?>> singletonFactories,存放可以生成 Bean 的工厂

Spring 管理的 Bean 其实默认都是单例的,也就是说 Spring 将最终可以使用的 Bean 统一放入第一级缓存中,也就是 singletonObjects(单例池)里,以后凡是用到某个 Bean 了都从这里获取就行了。

仅使用一级缓存可以吗?

既然都从 singletonObjects 里获取,那么仅仅使用这一个 singletonObjects,可以吗?肯定不可以的。 首先 singletonObjects 存入的是完全初始化好的 Bean,可以拿来直接用的。 如果我们直接将未初始化完的 Bean 放在 singletonObjects 里面,注意,这个未初始化完的 Bean 极有可能会被其他的类拿去用,它都没完事呢,就被拿去造了,肯定要出事啊!

我们以 A 、 B、C 举例子

  1. 先实例化 A 类,叫 a

  2. 将 a 放入 singletonObjects 中(此时 a 中的 b 属性还是空的呢)

  3. C 类需要使用 A 类,去 singletonObjects 获取,且获取到了 a

  4. C 类使用 a,拿出 a 类的 b 属性,然后 NPE了.

诺,出事了吧,这下就不是解决循环依赖的问题了,反而设计就不对了。

NPE 就是 NullPointerException

使用二级缓存可以吗?

再来回顾下循环依赖的问题:A→B→A→B……

说到底就是怎么打破这个循环,一级缓存不行,我们就再加一级,可以吗? 我们看个图

ed1a74c90bb6dbb6d6b6a342d4d6e432.png

图中的缓存就是二级缓存

看完图,可能还会有疑惑,A 没初始化完成放入了缓存,那么 B 用的岂不是就是未完成的 A,是这样的没错! 在整个过程当中,A 是只有 1 个,而 B 那里的 A 只是 A 的引用,所以后面 A 完成了初始化,B 中的 A 自然也就完成了。这里就是文中前面提到的手动 setA,setB 那里,我再贴一下代码:

  A a = new A();
  B b = new B();
  b.setA(a); // 这里设置 b 的属性 a,其实就是 a 的引用
  a.setB(b); // 这里设置 a 的属性 b,此时的 b 已经完成了初始化,设置完 a 的属性, a 也就完成了初始化,那么对应的 b 也就完成了初始化

分析到这里呢,我们就会发现二级缓存就解决了循环依赖的问题了,可是为什么还要三级缓存呢? 这里就要说说 Spring 中 Bean 的生命周期。

Spring 中 Bean 的管理

bb57282f5f66cbb6d1cddf3a312e64ff.png

要明白 Spring 中的循环依赖,首先得了解下 Spring 中 Bean 的生命周期。

被 Spring 管理的对象叫 Bean 这里不会对 Bean 的生命周期进行详细的描述,只是描述一下大概的过程,方便大家去理解循环依赖。

Spring 中 Bean 的生命周期,指的就是 Bean 从创建到销毁的一系列生命活动。

那么由 Spring 来管理 Bean,要经过的主要步骤有:

  1. Spring 根据开发人员的配置,扫描哪些类由 Spring 来管理,并为每个类生成一个 BeanDefintion,里面封装了类的一些信息,如全限定类名、哪些属性、是否单例等等

  2. 根据 BeanDefintion 的信息,通过反射,去实例化 Bean(此时就是实例化但未初始化 的 Bean)

  3. 填充上述未初始化对象中的属性(依赖注入)

  4. 如果上述未初始化对象中的方法被 AOP 了,那么就需要生成代理类(也叫包装类)

  5. 最后将完成初始化的对象存入缓存中(此处缓存 Spring 里叫: singletonObjects),下次用从缓存获取 ok 了

如果没有涉及到 AOP,那么第四步就没有生成代理类,将第三步完成属性填充的对象存入缓存中。

二级缓存有什么问题?

如果 Bean 没有 AOP,那么用二级缓存其实没有什么问题的,一旦有上述生命周期中第四步,就会导致的一个问题。因为 AOP 处理后,往往是需要生成代理对象的,代理对象和原来的对象根本就不是 1 个对象

以二级缓存的场景来说,假设 A 类的某个方法会被 AOP,过程就是这样的:

33682029290de492b0fee953e29cee5a.png

  1. 生成 a 的实例,然后放入缓存,a 需要 b

  2. 再生成 b ,填充 b 的时候,需要 a,从缓存中取到了 a,完成 b 的初始化;

  3. 紧接着 a 把初始化好的 b 拿过来用,完成 a 的属性填充和初始化

  4. 由于 A 类涉及到了 AOP,再然后 a 要生成一个代理类,这里就叫:代理 a 吧

结果就是:a 最终的产物是代理 a,那 b 中其实也应该用代理 a,而现在 b 中用的却是原始的 a 代理 a 和原始的 a 不是一个对象,现在这就有问题了。

使用三级缓存如何解决?

二级缓存还是有问题,那就再加一层缓存,也就是第三级缓存:Map<String, ObjectFactory<?>> singletonFactories,在 bean 的生命周期中,创建完对象之后,就会构造一个这个对象对应的 ObjectFactory 存入 singletonFactories 中。

singletonFactories 中存的是某个 beanName 及对应的 ObjectFactory,这个 ObjectFactory 其实就是生成这个 Bean 的工厂。实际中,这个 ObjectFactory 是个 Lambda 表达式:() -> getEarlyBeanReference(beanName, mbd, bean)而且,这个表达式并没有执行

那么 getEarlyBeanReference 具体做了什么事情?

核心就是两步:

第一步:根据 beanName 将它对应的实例化后且未初始化完的 Bean,存入 Map<Object, Object> earlyProxyReferences = new ConcurrentHashMap<>(16);

第二步:生成该 Bean 对应的代理类返回

这个 earlyProxyReferences其实就是用于记录哪些 Bean 执行过 AOP,防止后期再次对 Bean 进行 AOP

那么 getEarlyBeanReference 什么时候被触发,什么时候执行?

在二级缓存示例中,填充 B 的属性时候,需要 A,然后去缓存中拿 A,此时先去第三级缓存中去取 A,如果存在,此时就执行 getEarlyBeanReference 函数,然后该函数就会返回 A 对应的代理对象。

后续再将该代理对象放入第二级缓存中,也就是 Map<String, Object> earlySingletonObjects里。

为什么不放入第一级缓存呢?

此时就拿到的代理对象,也是未填充属性的,也就是仍然是未初始化完的对象。

如果直接放入第一级缓存,此时被其他类拿去使用,肯定有问题了。

那么什么时候放入第一级缓存?

这里需要再简单说下第二级缓存的作用,假如 A 经过第三级缓存,获得代理对象,这个代理对象仍然是未初始化完的!那么就暂时把这个代理对象放入第二级缓存,然后删除该代理对象原本在第三级缓存中的数据(确保后期不会每次都生成新的代理对象),后面其他类要用了 A,就去第二级缓存中找,就获取到了 A 的代理对象,而且都用的是同一个 A 的代理对象,这样后面只需要对这一个代理对象进行完善,其他引入该代理对象的类就都完善了。

再往后面,继续完成 A 的初始化,那么先判断 A 是否存在于 earlyProxyReferences 中, 存在就说明 A 已经经历过 AOP 了,就无须再次 AOP。那 A 的操作就转换从二级缓存中获取,把 A 的代理类拿出来,填充代理类的属性。

完成后再将 A 的代理对象加入到第一级缓存,再把它原本在第二级缓存中的数据删掉,确保后面还用到 A 地类,直接从第一级缓存中获取。

看个图理解下:

9e69c2b6551f5eb783d123a98e0e1aa1.png

总结

说了这么多,总结下三级缓存:

第一级缓存(也叫单例池):Map<String, Object> singletonObjects,存放已经经历了完整生命周期的 Bean 对象

第二级缓存:Map<String, Object> earlySingletonObjects,存放早期暴露出来的 Bean 对象,Bean 的生命周期未结束(属性还未填充完),可能是代理对象,也可能是原始对象

第三级缓存:Map<String, ObjectFactory<?>> singletonFactories,存放可以生成 Bean 的工厂,工厂主要用来生成 Bean 的代理对象

附: 一个完整的 Spring 循环依赖时序图

时序图并不标准,但是方便大家去理解 在理解循环依赖的时候,整体是个递归,你要有种套中套、梦中梦的感觉:

https://www.jianshu.com/p/e55d6c6db3af




TopTop