Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?

2021年11月23日 阅读数:3
这篇文章主要向大家介绍Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

写做背景
git

作 Java 开发的,通常都绕不开 Spring,那么面试中确定会被问到 Spring 的相关内容,而循环依赖又是 Spring 中的高频面试题面试

这不前段时间,个人一朋友去面试,就被问到了循环依赖,结果他还在上面还小磕了一下,他们聊天过程以下spring

面试官:说下什么是循环依赖缓存

朋友:两个或则两个以上的对象互相依赖对方,最终造成 闭环 。例如 A 对象依赖 B 对象,B 对象也依赖 A 对象ide

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring

面试官:那会有什么问题呢oop

朋友:对象的建立过程会产生死循环,相似以下源码分析

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_02

面试官:Spring 是如何解决的呢性能

朋友:经过三级缓存提早暴露对象来解决的debug

面试官:三级缓存里面分别存的什么设计

朋友:一级缓存里存的是成品对象,实例化和初始化都完成了,咱们的应用中使用的对象就是一级缓存中的

二级缓存中存的是半成品,用来解决对象建立过程当中的循环依赖问题

三级缓存中存的是 ObjectFactory

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_03

面试官:为何要用三级缓存来解决循环依赖问题(只用一级缓存行不行,只用二级缓存行不行)

朋友:霸点蛮,只用一级缓存也是能够解决的,可是会复杂化整个逻辑

半成品对象是无法直接使用的(存在 NPE 问题),因此 Spring 须要保证在启动的过程当中,全部中间产生的半成品对象最终都会变成成品对象

若是将半成品对象和成品对象都混在一级缓存中,那么为了区分他们,势必会增长一些而外的标记和逻辑处理,这就会致使对象的建立过程变得复杂化了

将半成品对象与成品对象分开存放,两级缓存各司其职,可以简化对象的建立过程,更简单、直观

若是 Spring 不引入 AOP,那么两级缓存就够了,可是做为 Spring 的核心之一,AOP 怎能少得了呢

因此为了处理 AOP 时的循环依赖,Spring 引入第三级缓存来处理循环依赖时的代理对象的建立

面试官:若是将代理对象的建立过程提早,紧随于实例化以后,而在初始化以前,那是否是就能够只用两级缓存了?

朋友心想:这到了我知识盲区了呀,我干哦!却点头道:你说的有道理耶,我没有细想这一点,回头我去改改源码试试看

前面几问,感受朋友答的还不错,可是最后一问中的第三级缓存的做用,回答的还差那么一丢丢,到底那一丢丢是什么,咱们慢慢往下看

写在前面

正式开讲以前,咱们先来回顾一些内容,否则可能后面的内容看起来有点蒙(其实主要是怕大家杠我)

一、对象的建立

通常而言,对象的建立分红两步:实例化、初始化,实例化指的是从堆中申请内存空间,完成 JVM 层面的对象建立,初始化指的是给属性值赋值

固然也能够直接经过构造方法一步完成实例化与初始化,实现对象的建立

固然还要其余的方式,好比工厂等

二、Spring 的的注入方式

有三种:构造方法注入、setter 方法注入、接口注入

接口注入的方式太灵活,易用性比较差,因此并未普遍应用起来,你们知道有这么一说就好,不要去细扣了

构造方法注入的方式,将实例化与初始化并在一块儿完成,可以快速建立一个可直接使用的对象,但它无法处理循环依赖的问题,了解就好

setter 方法注入的方式,是在对象实例化完成以后,再经过反射调用对象的 setter 方法完成属性的赋值,可以处理循环依赖的问题,是后文的基石,必需要熟悉

三、Spring 三级缓存的顺序

三级缓存的顺序是由查询循序而来,与在类中的定义顺序无关

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_04

因此第一级缓存:singletonObjects ,第二级缓存:earlySingletonObjects ,第三级缓存:singletonFactories

四、解决思路

抛开 Spring,让咱们本身来实现,会如何处理循环依赖问题呢

半成品虽然不能直接在应用中使用,可是在对象的建立过程当中仍是可使用的嘛,就像这样

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_05

有入栈,有出栈,而不是一直入栈,也就解决了循环依赖的死循环问题

Spring 是否是也是这样实现的了,基于 5.2.12.RELEASE ,咱们一块儿来看看 Spring 是如何解决循环依赖的

Spring 源码分析

下面会从几种不一样的状况来进行源码跟踪,若是中途有疑问,先用笔记下来,所有看完了以后还有疑问,那就请评论区留言

一、没有依赖,有 AOP

代码很是简单:spring-no-dependence

https://gitee.com/youzhibing/spring-circle/tree/master/spring-no-dependence

此时, SimpleBean 对象在 Spring 中是如何建立的呢,咱们一块儿来跟下源码

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_06

接下来,咱们从 DefaultListableBeanFactory 的 preInstantiateSingletons 方法开始 debug 

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_07

没有跟进去的方法,或者快速跳过的,咱们能够先略过,重点关注跟进去了的方法和停留了的代码,此时有几个属性值中的内容值得咱们留意下

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_08

咱们接着从 createBean 往下跟

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_09

关键代码在 doCreateBean 中,其中有几个关键方法的调用值得你们去跟下

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_10

此时:代理对象的建立是在对象实例化完成,而且初始化也完成以后进行的,是对一个成品对象建立代理对象

因此此种状况下:只用一级缓存就够了,其余两个缓存能够不要

二、循环依赖,没有AOP

代码依旧很是简单:spring-circle-simple

https://gitee.com/youzhibing/spring-circle/tree/master/spring-circle-simple

此时循环依赖的两个类是:Circle 和 Loop

对象的建立过程与前面的基本一致,只是多了循环依赖,少了 AOP,因此咱们重点关注:populateBean 和 initializeBean 方法

先建立的是 Circle 对象,那么咱们就从建立它的 populateBean 开始,再开始以前,咱们先看看三级缓存中的数据状况

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_11

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_12

咱们开始跟 populateBean ,它完成属性的填充,与循环依赖有关,必定要仔细看,仔细跟

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_13

对 circle 对象的属性 loop 进行填充的时候,去 Spring 容器中找 loop 对象,发现没有则进行建立,又来到了熟悉的 createBean 

此时三级缓存中的数据没有变化,可是 Set

相信到这里你们都没有问题,咱们继续往下看

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_14

 loop 实例化完成以后,对其属性 circle 进行填充,去 Spring 中获取 circle 对象,又来到了熟悉的 doGetBean 

此时1、二级缓存中都没有 circle、loop ,而三级缓存中有这两个,咱们接着往下看,重点来了,仔细看哦

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_15

经过 getSingleton 获取 circle 时,三级缓存调用了 getEarlyBeanReference ,但因为没有 AOP,因此 getEarlyBeanReference 直接返回了普通的 半成品 circle 

而后将 半成品 circle 放到了二级缓存,并将其返回,而后填充到了 loop 对象中

此时的 loop 对象就是一个成品对象了;接着将 loop 对象返回,填充到 circle 对象中,以下如所示

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_15

咱们发现直接将 成品 loop 放到了一级缓存中,二级缓存自始至终都没有过 loop ,三级缓存虽然说存了 loop ,但没用到就直接 remove 了

此时缓存中的数据,相信你们都能想到了

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_17

虽然说 loop 对象已经填充到了 circle 对象中,但还有一丢丢流程没走完,咱们接着往下看

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_18

将 成品 circle 放到了一级缓存中,二级缓存中的 circle 没有用到就直接 remove 了,最后各级缓存中的数据相信你们都清楚了,就不展现了

咱们回顾下这种状况下各级缓存的存在感,一级缓存存在感十足,二级缓存能够说无存在感,三级缓存有存在感(向 loop 中填充 circle 的时候有用到)

因此此种状况下:能够减小某个缓存,只须要两级缓存就够了

三、循环依赖 + AOP

代码仍是很是简单:spring-circle-aop,在循环依赖的基础上加了 AOP

比上一种状况多了 AOP,咱们来看看对象的建立过程有什么不同;一样是先建立 Circle ,在建立 Loop

建立过程与上一种状况大致同样,只是有小部分区别,跟源码的时候我会在这些区别上有所停顿,其余的会跳过,你们要仔细看

实例化 Circle ,而后填充 半成品 circle 的属性 loop ,去 Spring 容器中获取 loop 对象,发现没有

则实例化 Loop ,接着填充 半成品 loop 的属性 circle ,去 Spring 容器中获取 circle 对象

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_19

这个过程与前一种状况是一致的,就直接跳过了,咱们从上图中的红色步骤开始跟源码,此时三级缓存中的数据以下

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_20

注意看啦,重要的地方来了

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_21

咱们发现从第三级缓存获取 circle 的时候,调用了 getEarlyBeanReference 建立了 半成品 circle 的代理对象

将 半成品 circle 的代理对象放到了第二级缓存中,并将代理对象返回赋值给了 半成品 loop 的 circle 属性 

注意:此时是在进行 loop 的初始化,但却把 半成品 circle 的代理对象提早建立出来了

 loop 的初始化还未完成,咱们接着往下看,又是一个重点,仔细看

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_22

在 initializeBean 方法中完成了 半成品 loop 的初始化,并在最后建立了 loop 成品 的代理对象

 loop 代理对象建立完成以后会将其放入到第一级缓存中(移除第三级缓存中的 loop ,第二级缓存自始至终都没有 loop )

而后将 loop 代理对象返回并赋值给 半成品 circle 的属性 loop ,接着进行 半成品 circle 的 initializeBean 

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_23

由于 circle 的代理对象已经生成过了(在第二级缓存中),因此不用再生成代理对象了;将第二级缓存中的 circle 代理对象移到第一级缓存中,并返回该代理对象

此时各级缓存中的数据状况以下(普通 circle 、 loop 对象在各自代理对象的 target 中)

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_24

咱们回顾下这种状况下各级缓存的存在感,一级缓存还是存在感十足,二级缓存有存在感,三级缓存挺有存在感

第三级缓存提早建立 circle 代理对象,不提早建立则只能给 loop 对象的属性 circle 赋值成 半成品 circle ,那么 loop 对象中的 circle 对象就无 AOP 加强功能了

第二级缓存用于存放 circle 代理,用于解决循环依赖;也许在这个示例体现的不够明显,由于依赖比较简单,依赖稍复杂一些,就能感觉到了

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_25

第一级缓存存放的是对外暴露的对象,多是代理对象,也多是普通对象

因此此种状况下:三级缓存一个都不能少

四、循环依赖 + AOP + 删除第三级缓存

没有依赖,有AOP 这种状况中,咱们知道 AOP 代理对象的生成是在成品对象建立完成以后建立的,这也是 Spring 的设计原则,代理对象尽可能推迟建立

循环依赖 + AOP 这种状况中, circle 代理对象的生成提早了,由于必需要保证其 AOP 功能,但 loop 代理对象的生成仍是遵循的 Spring 的原则

若是咱们打破这个原则,将代理对象的建立逻辑提早,那是否是就能够不用三级缓存了,而只用两级缓存了呢?

代码依旧简单:spring-circle-custom

https://gitee.com/youzhibing/spring-circle/tree/master/spring-circle-custom

只是对 Spring 的源码作了很是小的改动,改动以下:

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_26

去除了第三级缓存,并将代理对象的建立逻辑提早,置于实例化以后,初始化以前;咱们来看下执行结果

Spring 循环依赖,源码详细分析,真的必需要三级缓存吗?_Spring_27

并无什么问题,有兴趣的能够去跟下源码,跟踪过程相信你们已经掌握,这里就再也不演示了

五、循环依赖 + AOP + 注解

目前基于 xml 的配置愈来愈少,而基于注解的配置愈来愈多,因此了也提供了一个注解的版本供你们去跟源码

代码仍是很简单:spring-circle-annotation

https://gitee.com/youzhibing/spring-circle/tree/master/spring-circle-annotation

跟踪流程与 循环依赖 + AOP 那种状况基本一致,只是属性的填充有了一些区别,具体可查看:Spring 的自动装配 → 骚话 @Autowired 的底层工做原理

总结

一、三级缓存各自的做用

第一级缓存存的是对外暴露的对象,也就是咱们应用须要用到的

第二级缓存的做用是为了处理循环依赖的对象建立问题,里面存的是半成品对象或半成品对象的代理对象

第三级缓存的做用处理存在 AOP + 循环依赖的对象建立问题,能将代理对象提早建立

二、Spring 为何要引入第三级缓存

严格来说,第三级缓存并不是缺它不可,由于能够提早建立代理对象

提早建立代理对象只是会节省那么一丢丢内存空间,并不会带来性能上的提高,可是会破环 Spring 的设计原则

Spring 的设计原则是尽量保证普通对象建立完成以后,再生成其 AOP 代理(尽量延迟代理对象的生成)

因此 Spring 用了第三级缓存,既维持了设计原则,又处理了循环依赖;牺牲那么一丢丢内存空间是愿意接受的