关于java:Spring-是如何解决循环依赖的写得太好了

35次阅读

共计 5267 个字符,预计需要花费 14 分钟才能阅读完成。

作者:青石路 \
www,cnblogs.com/youzhibing/p/14337244.html

写作背景

做 Java 开发的,个别都绕不开 Spring,那么面试中必定会被问到 Spring 的相干内容,而循环依赖又是 Spring 中的高频面试题。

这不前段时间,我的一敌人去面试,就被问到了循环依赖,后果他还在下面还小磕了一下,他们聊天过程如下

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

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

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

敌人:对象的创立过程会产生死循环,相似如下

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

敌人:通过三级缓存提前裸露对象来解决的

面试官:三级缓存外面别离存的什么

敌人:一级缓存里存的是成品对象,实例化和初始化都实现了,咱们的利用中应用的对象就是一级缓存中的

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

三级缓存中存的是 ObjectFactory<?> 类型的 lambda 表达式,用于解决存在 AOP 时的循环依赖问题

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

敌人:霸点蛮,只用一级缓存也是能够解决的,然而会复杂化整个逻辑

半成品对象是没法间接应用的(存在 NPE 问题),所以 Spring 须要保障在启动的过程中,所有两头产生的半成品对象最终都会变成成品对象

如果将半成品对象和成品对象都混在一级缓存中,那么为了辨别他们,势必会减少一些而外的标记和逻辑解决,这就会导致对象的创立过程变得复杂化了

将半成品对象与成品对象离开寄存,两级缓存各司其职,可能简化对象的创立过程,更简略、直观

如果 Spring 不引入 AOP,那么两级缓存就够了,然而作为 Spring 的外围之一,AOP 怎能少得了呢

所以为了解决 AOP 时的循环依赖,Spring 引入第三级缓存来解决循环依赖时的代理对象的创立

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

敌人心想:这到了我常识盲区了呀,我干哦!却拍板道:你说的有情理耶,我没有细想这一点,回头我去改改源码试试看

后面几问,感觉敌人答的还不错,然而最初一问中的第三级缓存的作用,答复的还差那么一丢丢,到底那一丢丢是什么,咱们缓缓往下看

写在后面

正式开讲之前,咱们先来回顾一些内容,不然可能前面的内容看起来有点蒙(其实次要是怕你们杠我)

1、对象的创立

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

当然也能够间接通过构造方法一步实现实例化与初始化,实现对象的创立

当然还要其余的形式,比方工厂等

2、Spring 的的注入形式

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

接口注入的形式太灵便,易用性比拟差,所以并未广泛应用起来,大家晓得有这么一说就好,不要去细扣了

构造方法注入的形式,将实例化与初始化并在一起实现,可能疾速创立一个可间接应用的对象,但它没法解决循环依赖的问题,理解就好

setter 办法注入的形式,是在对象实例化实现之后,再通过反射调用对象的 setter 办法实现属性的赋值,可能解决循环依赖的问题,是后文的基石,必须要相熟

3、Spring 三级缓存的程序

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

所以第一级缓存:singletonObjects,第二级缓存:earlySingletonObjects,第三级缓存:singletonFactories

4、解决思路

抛开 Spring,让咱们本人来实现,会如何解决循环依赖问题呢

半成品尽管不能间接在利用中应用,然而在对象的创立过程中还是能够应用的嘛,就像这样

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

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

Spring 源码剖析

上面会从几种不同的状况来进行源码跟踪,如果中途有疑难,先用笔记下来,全副看完了之后还有疑难,那就请评论区留言。

举荐一个 Spring Boot 基础教程及实战示例:
https://github.com/javastacks…

1、没有依赖,有 AOP

代码非常简单:spring-no-dependence

https://gitee.com/youzhibing/…

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

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

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

咱们接着从 createBean 往下跟

要害代码在 doCreateBean 中,其中有几个要害办法的调用值得大家去跟下

此时:代理对象的创立是在对象实例化实现,并且初始化也实现之后进行的,是对一个成品对象创立代理对象

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

2、循环依赖,没有 AOP

代码仍旧非常简单:spring-circle-simple

https://gitee.com/youzhibing/…

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

对象的创立过程与后面的基本一致,只是多了循环依赖,少了 AOP,所以咱们重点关注:populateBean 和 initializeBean 办法

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

咱们开始跟 populateBean,它实现属性的填充,与循环依赖无关,肯定要认真看,认真跟

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

此时三级缓存中的数据没有变动,然而 Set<String> singletonsCurrentlyInCreation 中多了个 loop

置信到这里大家都没有问题,咱们持续往下看

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

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

通过 getSingleton 获取 circle 时,三级缓存调用了 getEarlyBeanReference,但因为没有 AOP,所以 getEarlyBeanReference 间接返回了一般的 半成品 circle

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

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

咱们发现间接将 成品 loop 放到了一级缓存中,二级缓存从头至尾都没有过 loop,三级缓存虽说存了 loop,但没用到就间接 remove 了

此时缓存中的数据,置信大家都能想到了

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

将 成品 circle 放到了一级缓存中,二级缓存中的 circle 没有用到就间接 remove 了,最初各级缓存中的数据置信大家都分明了,就不展现了

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

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

3、循环依赖 + AOP

代码还是非常简单:spring-circle-aop,在循环依赖的根底上加了 AOP

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

创立过程与上一种状况大体一样,只是有小局部区别,跟源码的时候我会在这些区别上有所进展,其余的会跳过,大家要认真看

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

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

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

留神看啦,重要的中央来了

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

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

留神:此时是在进行 loop 的初始化,但却把 半成品 circle 的代理对象提前创立进去了

loop 的初始化还未实现,咱们接着往下看,又是一个重点,认真看

在 initializeBean 办法中实现了 半成品 loop 的初始化,并在最初创立了 loop 成品 的代理对象

loop 代理对象创立实现之后会将其放入到第一级缓存中(移除第三级缓存中的 loop,第二级缓存从头至尾都没有 loop)

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

因为 circle 的代理对象曾经生成过了(在第二级缓存中),所以不必再生成代理对象了;将第二级缓存中的 circle 代理对象移到第一级缓存中,并返回该代理对象

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

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

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

第二级缓存用于寄存 circle 代理,用于解决循环依赖;兴许在这个示例体现的不够显著,因为依赖比较简单,依赖稍简单一些,就能感触到了

第一级缓存寄存的是对外裸露的对象,可能是代理对象,也可能是一般对象

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

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

没有依赖,有 AOP 这种状况中,咱们晓得 AOP 代理对象的生成是在成品对象创立实现之后创立的,这也是 Spring 的设计准则,代理对象尽量推延创立

循环依赖 + AOP 这种状况中,circle 代理对象的生成提前了,因为必须要保障其 AOP 性能,但 loop 代理对象的生成还是遵循的 Spring 的准则

如果咱们突破这个准则,将代理对象的创立逻辑提前,那是不是就能够不必三级缓存了,而只用两级缓存了呢?

代码仍旧简略:spring-circle-custom

https://gitee.com/youzhibing/…

只是对 Spring 的源码做了十分小的改变,改变如下:

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

并没有什么问题,有趣味的能够去跟下源码,跟踪过程置信大家曾经把握,这里就不再演示了

5、循环依赖 + AOP + 注解

目前基于 xml 的配置越来越少,而基于注解的配置越来越多,所以了也提供了一个注解的版本供大家去跟源码

代码还是很简略:spring-circle-annotation

https://gitee.com/youzhibing/…

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

总结

1、三级缓存各自的作用

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

第二级缓存的作用是为了解决循环依赖的对象创立问题,外面存的是半成品对象或半成品对象的代理对象

第三级缓存的作用解决存在 AOP + 循环依赖的对象创立问题,能将代理对象提前创立

2、Spring 为什么要引入第三级缓存

严格来讲,第三级缓存并非缺它不可,因为能够提前创立代理对象

提前创立代理对象只是会节俭那么一丢丢内存空间,并不会带来性能上的晋升,然而会破环 Spring 的设计准则

Spring 的设计准则是尽可能保障一般对象创立实现之后,再生成其 AOP 代理(尽可能提早代理对象的生成)

所以 Spring 用了第三级缓存,既维持了设计准则,又解决了循环依赖;就义那么一丢丢内存空间是违心承受的.

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿 (2021 最新版)

2. 别在再满屏的 if/ else 了,试试策略模式,真香!!

3. 卧槽!Java 中的 xx ≠ null 是什么新语法?

4.Spring Boot 2.6 正式公布,一大波新个性。。

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0