写点什么

Spring 源码解析 - 从源码角度看 bean 的循环依赖

  • 2023-03-28
    湖南
  • 本文字数:5086 字

    阅读完需:约 17 分钟

一、概述及目录

一直想单独写一遍关于从源码角度看 bean 的循环依赖,因为现在网上的大部分关于循环依赖的文章都是从理论的角度在讲述,属于一些比较硬背的八股文,我以前也看过,背下来了可能面试的时候还好,能够简单的说下具体流程,但是如果过了很长时间或者面试问的比较深,就 gg 了。


所以抽这个周末下了 Spring 源码,想记录下源码角度是怎么解决 Spring 循环依赖的。后续还会有关于 Spring AOP 相关的章节。

二、bean 的创建过程

实际上在 bean 的创建过程中已经包含了循环依赖的相应知识点,话不多少,直接上肝货哦,博主趁着周末时间基于源码简要整理出来了一个关于 bean 创建的大致流程图。

三、什么是循环依赖,什么是三级缓存,那又是怎么执行的?

3.1 什么是循环依赖

循环依赖:大白话的说就是一个或多个对象实例之间存在直接或间接的互相依赖关系,这种依赖关系构成了构成一个环形调用。

  • 第一种情况:自依赖情况


  • 第二种情况:两个对象直接的相互依赖


  • 第三种情况:多个对象直接的间接依赖


前面两种情况是直接循环依赖看起来就比较直观,而第三种属于间接循环依赖的情况有时候因为业务代码调用层级很深,很难排查出来。

3.2 循环依赖的主要场景

3.3 三级缓存解释

  • 第一级缓存:单例池 singletonObjects,它用来存放经过完整 Bean 生命周期过程的单例 Bean 对象,此时 bean 的已经完成实例化、注⼊、初始化完成。

  • 第二级缓存:earlySingletonObjects,它用来保存那些没有经过完整 Bean 生命周期的单例 Bean 对象,用来保证不完整的 bean 也是单例,简单来说就是刚实例化完成的 bean,未初始化的 bean 对象。

  • 第三级缓存:singletonFactories,它保存的就是一个 lambda 表达式,它主要的作用就是 bean 出现循环依赖后,某一个 bean 到底会不会进行 AOP 操作,也就是我们说的 bean 工厂,存放 ObjectFactory 对象在需要的时候创建代理对象。

3.4 代码维度展示

@Servicepublic class XiaoMaYi1 {
@Autowired private XiaoMaYi1 xiaoMaYi1;
public void test1() { }}
复制代码


@Servicepublic class XiaoMaYi2 {
@Autowired private XiaoMaYi1 xiaoMaYi1;
public void test2() { }}
复制代码

拿这个循环依赖,它能正常运⾏,为什么能够正常运行,后⾯我们会通过源码的⻆度,解读整体的执⾏流程。


下边还有一个例子:

@Servicepublic class A {      public A(B b) {  }}
复制代码


@Servicepublic class B {      public B(C c) {      }}
复制代码


@Servicepublic class C {      public C(A a) {  }}
复制代码

结果启动发现如下:


这就是上边所说的构造器注入的方式,Spring 没有帮我们解决。

3.5 模块解读

3.6 相应执行流程

源码流程:


大概流程 先去获取 A 的 Bean,发现没有就准备去创建⼀个,然后将 A 的代理⼯⼚放⼊“三级缓存”(这个 A 其实是⼀个半成品,只有实例化还没有对⾥⾯的属性进⾏注⼊),此时发现 A 依赖 B 的创建,就必须先去创建 B; 这时候准备创建 B,发现 B ⼜依赖 A,需要先去创建 A; B 中创建 A 的过,因为在前边第⼀层已经创建了 A 的代理⼯⼚,直接从“三级缓存”中拿到 A 的代理⼯⼚,获取 A 的代理对象,放⼊“⼆级缓存”,并清除“三级缓存”; 回到第 2,现在有了 A 的代理对象,对 A 的依赖完美解决(这⾥的 A 仍然是个半成品),B 初始化成功; 回到 1,现在 B 初始化成功,完成 A 对象的属性注⼊,然后再填充 A 的其它属性,以及 A 的其它步骤(包括 AOP),完成对 A 完整的初始化功能(这⾥的 A 才是完整的 Bean)。 将 A 放⼊“⼀级缓存”。

四、相应源码解读

本示例:Spring 的版本是 5.2.x.RELEASE。

4.1 代码入口解析

refresh 方法是 Spring 的核心方法,可以慢慢品下哦!!!

这里过滤掉一些系统的 bean,只关心我们创建的 XiaoMaYi 相关的 bean

这里就可以看到我们创建的 xiaomayi 的 bean 了

4.2 一级缓存

进⼊ doGetBean(),从 getSingleton() 实际上也是预先处理处理好的缓存中 没有找到对象,进⼊创建 Bean 的逻辑。


我们进入 createBean 方法找到 doCreateBean:

然后我们就会发现调用了 addSingletonFactory 方法,然后在这里还会获取 bean 的早期引用 getEarlyBeanReference

将 xiaomayi1 工厂对象塞⼊三级缓存 singletonFactories 中。


进⼊到 populateBean()放置中,然后看执⾏ postProcessProperties(),这⾥是⼀个策略模式,找到下图的策略对象。



下面则都是为获取 xiaoMaYi1 的成员对象,然后进行属性注入。





进入 doResolveDependency 这个方法后就是真正找 xiaomayi1 依赖的属性 xiaomayi2 了。



从 beanFactory 中正式获取 xiaoMaYi2 的 bean。

到这⾥,一级缓存已经结束了,doGetBean 层层嵌套被调用了好多次,有种递归算法的感觉. 因为 xiaomqyi1 依赖 xiaomayi2,下⾯我们进⼊二级缓存处理的逻辑。

4.3 二级缓存

圈着的就是二级缓存的依赖逻辑,其实也是先从一级缓存中获取->创建实例->提前暴露,添加到三级缓存->再添加自己依赖的属性, 这个流程,其实在这里就是 xiaoMaYi2 依赖 xiaoMaYi1 的流程。


所以前边和 xiaoMaYi1 相同的流程我们就直接省略了!!!




这里正式获取 xiaoMaYi1 的 bean。


到这⾥,二级缓存已经结束了,因为 xiaoMaYi2 依赖 xiaoMaYi1,所以我们进⼊三级缓存看是怎么处理的。

4.3 三级缓存

图中圈出的为这次说明的流程!!!


获取 xiaoMaYi1 的 bean,在前边第⼀层和第⼆层中每次都会从 getSingleton() 获取依赖的 bean,但是由于之前都没有初始化 xiaoMaYi1 和 xiaoMaYi2 的三级缓存,所以获取对象都是空对象。那要怎么处理呢,话不多说,我们开始下边的流程。


这里是重中之重 来到了第三层,由于第三级缓存中有了 xiaomayi1 的 bean,这⾥使⽤三级缓存中的⼯⼚为 xiaomayi1 创建⼀个代理对象,塞⼊ ⼆级缓存。


这⾥就拿到了 xiaomayi1 的代理对象,解决了 xiaomayi2 的依赖关系,返回到二级缓存中。

4.4 返回二级缓存

返回返回二级缓存后,xiaomayi2 初始化结束,这里有个问题,二级缓存就结束了吗??? 还有⼆级缓存的数据,啥时候会在⼀级缓存中使用了呢? 心急吃不了热豆腐,我们继续。


看这⾥,还记得在 doGetBean() 中,我们会通过 createBean() 创建⼀个 xiaomayi2 的 bean,当 xiaomayi2 的 bean 创建成功后,我们会执⾏ getSingleton(),它会对 xiaomayi2 的结果进⾏处理这个流程吗???

我们进⼊ getSingleton(),会看到下⾯这个⽅法中片段。

这个地方就是处理 xiaomayi2 的 ⼀、⼆级缓存的具体逻辑,那么怎么处理呢??? 答案就是,将⼆级缓存清除,放⼊⼀级缓存,具体看代码哦 .

4.5 返回一级缓存

同 4.4,xiaomayi1 初始化完毕后,会把 xiaomayi1 的⼆级缓存清除,将对象放⼊⼀级缓存,直接上代码。

到这⾥,所有的流程结束,返回了我们想要的并且完整的 xiaomayi1 对象。

五、什么是单例池,什么是一级缓存?

singletonObjects,结构是 Map,它就是⼀个单例池, 存放已经完全创建好的 Bean,那么什么叫完完全全创建好的?就是上面说的是,所有的步骤都处理完了,就是创建好的 Bean。一个 Bean 在产的过程中是需要经历很多的步骤,在这些步骤中可能要处理 @Autowired 注解,又或是处理 @Value ,@Resource, 保存在该缓存中的 Bean 所实现 Aware 子接口的方法已经回调完毕,自定义初始化方法已经执行完毕,也经过 BeanPostProcessor 实现类的 postProcessorBeforeInitialization、postProcessorAfterInitialization 方法处理等; 当需要处理的都处理完之后的 Bean,就是完全创建好的 Bean,这个 Bean 是可以用来使用的,我们平时在用的 Bean 其实就是创建好的,同时单例池也是一级缓存的一个别称。

六、什么是二级缓存,它的作用是什么?

二级缓存-earlySingletonObjects 结构就 Map<String,ObjectFactory<>>,是用来存放早期暴露的 Bean,一般只有处于循环引用状态的 Bean 才会被保存在该缓存中【早期的意思就是没有完全创建好,但是由于有循环依赖,就需要把这种 Bean 提前暴露出去】。保存在该缓存中的 Bean 所实现 Aware 子接口的方法还未回调,自定义初始化方法未执行,也未经过 BeanPostProcessor 实现类的 postProcessorBeforeInitialization、postProcessorAfterInitialization 方法处理。如果启用了 Spring AOP,并且处于切点表达式处理范围之内,那么会被增强,即创建其代理对象。

七、什么是三级缓存,它的作用是什么?

这里三级缓存就是每个 Bean 对应的 ObjectFactory 对象也就是工厂对象,通过调用这个对象的 getObject 方法,就可以获取到早期暴露出去的 Bean 了。

八、为什么 Spring 要用三级缓存来解决循环依赖?

这是我们面试过程中常问的一个问题,前⾯了解了 bean 创建的执⾏流程和源码解读,但是没提到什么要⽤ 3 级缓存这个问题,这个地方我们解释下!!!

8.1 . 一级缓存能解决吗?


  • 第一级缓存,也就是缓存完全创建好的 Bean 的缓存,这个缓存肯定是需要的,因为单例的 Bean 只能创建一次,那么肯定需要第一级缓存存储这些对象,如果有需要,直接从第一级缓存返回。那么如果只能有二级缓存的话,就只能舍弃第二级或者第三级缓存。

8.2 . 如果没有三级缓存?


  • 如果没有三级缓存,也就是没有 ObjectFactory,那么就需要往第二缓存放入早期的 Bean,那么这个地方就有一个问题,二级缓存应该放什么东西呢 ? 是放没有代理的 Bean 还是被代理的 Bean 呢?

  1. 如果直接往二级缓存添加没有被代理的 Bean,那么可能注入给其它对象的 Bean 跟最后最后完全生成的 Bean 是不一样的,因为最后生成的是代理对象,这肯定是不允许的;

  2. 那么如果直接往二级缓存添加一个代理 Bean 呢?假设没有循环依赖,提前暴露了代理对象,那么如果跟最后创建好的不一样,那么项目启动就会报错,假设没有循环依赖,使用了 ObjectFactory,那么就不会提前暴露了代理对象,到最后生成的对象是什么就是什么,就不会报错,如果有循环依赖,不论怎样都会提前暴露代理对象,那么如果跟最后创建好的不一样,那么项目启动就会报错通过上面分析,如果没有循环依赖,使用 ObjectFactory,就减少了提前暴露代理对象的可能性,从而减少报错的可能。


那这个对象的代理⼯⼚在这里有什么作⽤呢,它的主要作⽤是存放半成品的单例 Bean,⽬的是为了“打破 循环”还有一个问题 为什么“三级缓存”不直接存半成品的 XiaoMaYi1,⽽是要存⼀个代理⼯⼚呢 ?这答案就是 AOP 了,具体原因我们还是看上边的源码来说


从源码维度我们分析下三级缓存:


这里的重点是主要看这个对象⼯⼚是如何得到的,我们进⼊ getEarlyBeanReference() ⽅法。

进入下边方法


从这里就可以看到是处理相应 AOP 的操作了,也对应了我们上边说的为什么要三级缓存的一些点,同时从源码角度分析:


如果 XiaoMaYi1 有 AOP,就创建⼀个代理对象; 如果 XiaoMaYi1 没有 AOP,就返回原对象。

8.3 如果没有第二级缓存


如果没有第二级缓存, 也就是没有存放早期的 Bean 的缓存,其实肯定也不行。上面说过,ObjectFactory 其实获取的对象可能是代理的对象,那么如果每次都通过 ObjectFactory 获取代理对象,那么每次都重新创建一个代理对象,这肯定也是不允许的。


从上面的分析,我们知道为什么不能直接使用二级缓存了吧,第三级缓存就是为了避免过早地创建代理对象,从而避免没有循环依赖过早暴露代理对象产生的问题,而第二级缓存就是防止多次创建代理对象,导致对象不同。

  • 有了二级缓存都能解决 Spring 依赖了,怎么要有三级缓存呢。其实我们在前面分析源码时也提到过,三级缓存主要是解决 Spring AOP 的特性。AOP 本身就是对方法的增强,是 ObjectFactory<?> 类型的 lambda 表达式,而 Spring 的原则又不希望将此类类型的 Bean 前置创建,所以要存放到三级缓存中处理。

  • 其实整体处理过程类似,唯独是 B 在填充属性 A 时,先查询成品缓存、再查半成品缓存,最后在看看有没有单例工程类在三级缓存中。最终获取到以后调用 getObject 方法返回代理引用或者原始引用。


至此也就解决了 Spring AOP 所带来的三级缓存问题

九、总要有总结

先回顾下本章主要知识点:三级缓存


⼀级缓存:其实就是个单例池,⽤来存放已经初始化完成的单例 Bean; ⼆级缓存:这个主要是为了解决 AOP,存放的是半成品的 AOP 的单例 Bean 三级缓存:这里主要是解决循环依赖问题,存放的是⽣成半成品单例 Bean 的⼯⼚⽅法。


这是 Spring 部分循环依赖解决的方法,当然有些方式上边也说了是处理不了的,比如构造器注入的等。

还有一些其他体会:阅读源码刚开始是一个痛苦的过程,但是如果坚持下来自我感觉还是挺有意思,在这个过程中,你可以学习框架的设计模式,同时也理解我们平时用的东西底层是怎么实现的,像 Spring 可能我们有时候只知道项目中用到了,但是底层具体怎么实现,我们声明一个 bean 他是怎么生成的,我们添加了 @Autowired 注解为什么就直接能使用了等等。这些疑问你阅读 debug 下源码都会有答案,而且非常深刻。


作者:小蚂蚁技术

链接:https://juejin.cn/post/7212830775970889786

来源:稀土掘金

用户头像

还未添加个人签名 2021-07-28 加入

公众号:该用户快成仙了

评论

发布
暂无评论
Spring 源码解析-从源码角度看bean的循环依赖_Java_做梦都在改BUG_InfoQ写作社区