☘ 一步一步带你理解 Spring 循环依赖

写在开头

学习 Spring 的过程当中,对 Spring 的循环依赖大致明白了,可是自己再仔细跟踪源码,却又总差点意思,似懂非懂就很烦躁,然后就埋头苦干,一定要自己弄清楚,就肝下了这篇文章,也希望看了这篇文章的朋友能有所收获。

大家也可以关注我的公众号,文章也会同步更新,当然,公众号还会有一些资源可以分享给大家~
在这里插入图片描述

由于 CSDN 排版的原因,🤩🤩🤩 关注我的公众号,发送关键字: 循环依赖时序图
即可获得超高清时序图!

☘ Spring 循环依赖

🤔 什么是循环依赖?

举个栗子🌰

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

再看个简单的图:

在这里插入图片描述

像这样,创建 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…

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

在这里插入图片描述

图中的缓存就是二级缓存

看完图,可能还会有疑惑,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 的管理

在这里插入图片描述

要明白 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,过程就是这样的:

二级缓存问题示例图

  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,存入 java Map<Object, Object> earlyProxyReferences = new ConcurrentHashMap<>(16);

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

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

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

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

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

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

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

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

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

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

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

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

看个图理解下

在这里插入图片描述

总结

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

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

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

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

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

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

在这里插入图片描述

由于 CSDN 排版的原因,🤩🤩🤩 关注我的公众号,发送关键字: 循环依赖时序图
即可获得超高清时序图!

🎉🎉🎉🎉另外特殊福利 🤩🤩🤩 关注我的公众号,发送关键字: 循环依赖精美
即可获得本文的精美 PDF 版本哦!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Suremotoo丶小小书童

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值