欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

Spring 如何解决循环依赖问题?写得很好。

最编程 2024-03-07 22:53:47
...

写作背景

做 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/spring-boot-best-practice


1、没有依赖,有 AOP


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


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


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


image.png


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


image.png


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


image.png


我们接着从 createBean 往下跟


image.png


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


image.png


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


所以此种情况下:只用一级缓存就够了,其他两个缓存可以不要


2、循环依赖,没有AOP


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


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


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


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


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



1.png



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


2.png


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


此时三级缓存中的数据没有变化,但是 Set singletonsCurrentlyInCreation 中多了个 loop


相信到这里大家都没有问题,我们继续往下看


3.png


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


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


4.png


通过 getSingleton 获取 circle 时,三级缓存调用了 getEarlyBeanReference ,但由于没有 AOP,所以 getEarlyBeanReference 直接返回了普通的 半成品 circle


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


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


5.png


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


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


image.png


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


6.png


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


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


所以此种情况下:可以减少某个缓存,只需要两级缓存就够了


3、循环依赖 + AOP


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


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


创建过程与上一种情况大体一样,只是有小部分区别,跟源码的时候我会在这些区别上有所停顿,其他的会跳过,大家要仔细看


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


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


image.png


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


image.png


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


7.png


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


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


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


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


上一篇: 如果表存储中的查询数据部分不在二级索引表中,是反向检查数据表没错,计费是否会存在按卷两次读取的成本?

下一篇: 弹簧循环依赖的三种方法(三级缓存解决集合循环依赖问题)