专栏首页开发技术Spring 的循环依赖,源码详细分析 → 真的非要三级缓存吗

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

开心一刻

  吃完晚饭,坐在院子里和父亲聊天

  父亲:你有什么人生追求?

  我:金钱和美女

  父亲对着我的头就是一丁弓,说道:小小年纪,怎么这么庸俗,重说一次

  我:事业与爱情

  父亲赞赏的摸了我的头,说道:嗯嗯,这就对咯

写作背景

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

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

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

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

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

  朋友:对象的创建过程会产生死循环,类似如下

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

  朋友:通过三级缓存提前暴露对象来解决的

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

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

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

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

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

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

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

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

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

    如果 Spring 不引入 AOP,那么两级缓存就够了,但是作为 Spring 的核心之一,AOP 怎能少得了呢

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

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

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

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

写在前面

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

  对象的创建

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

    当然也可以直接通过构造方法一步完成实例化与初始化,实现对象的创建

    当然还要其他的方式,比如工厂等

  Spring 的的注入方式

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

    接口注入的方式太灵活,易用性比较差,所以并未广泛应用起来,大家知道有这么一说就好,不要去细扣了

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

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

  Spring 三级缓存的顺序

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

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

  解决思路

    抛开 Spring,让我们自己来实现,会如何处理循环依赖问题呢

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

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

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

Spring 源码分析

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

  没有依赖,有 AOP

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

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

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

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

    我们接着从 createBean 往下跟

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

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

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

  循环依赖,没有AOP

    代码依旧非常简单:spring-circle-simple,此时循环依赖的两个类是: 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 的时候有用到)

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

  循环依赖 + 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 代理,用于解决循环依赖;也许在这个示例体现的不够明显,因为依赖比较简单,依赖稍复杂一些,就能感受到了

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

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

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

    没有依赖,有AOP 这种情况中,我们知道 AOP 代理对象的生成是在成品对象创建完成之后创建的,这也是 Spring 的设计原则,代理对象尽量推迟创建

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

    如果我们打破这个原则,将代理对象的创建逻辑提前,那是不是就可以不用三级缓存了,而只用两级缓存了呢?

    代码依旧简单:spring-circle-custom,只是对 Spring 的源码做了非常小的改动,改动如下

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

    并没有什么问题,有兴趣的可以去跟下源码,跟踪过程相信大家已经掌握,这里就不再演示了

  循环依赖 + AOP + 注解

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

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

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

总结

  1、三级缓存各自的作用

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

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

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

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

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

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

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

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • spring无法用三级缓存解决循环依赖的问题分析

    这种构造器的循环依赖spring是无法正常进行创建的,因为在a进行初始化的时候,在构造器阶段就会去找b对象,在去构造b的原始对象时,在初始化也就是构造器阶段的时...

    gzq大数据
  • 再探循环依赖 → Spring 是如何判定原型循环依赖和构造方法循环依赖的?

    侄子:那你赶紧给我妈花吧,我妈要是跑了,你还得花钱娶一个,到最后,钱我捞不着,亲妈还混没了

    青石路
  • 面试必杀技,讲一讲Spring中的循环依赖

    Spring中的循环依赖一直是Spring中一个很重要的话题,一方面是因为源码中为了解决循环依赖做了很多处理,另外一方面是因为面试的时候,如果问到Spring中...

    程序员DMZ
  • Spring中异步注解@Async的使用、原理及使用时可能导致的问题

    很多同学碰到了下面这个问题,添加了Spring提供的一个异步注解@Async循环依赖无法被解决了,下面是一些读者的留言跟群里同学碰到的问题:

    程序员DMZ
  • 面经手册 · 第31篇《Spring Bean IOC、AOP 循环依赖解读》

    大学有四年时间,但几乎所有人都是临近毕业才发现找一份好工作费劲,尤其是我能非常熟悉的软件开发行业,即使是毕业了还需要额外花钱到培训机构,在学一遍编程技术才能出去...

    小傅哥
  • Spring Ioc源码分析 之 Bean的加载(六):循环依赖处理

    循环依赖,其实就是循环引用,就是两个或者两个以上的 bean 互相引用对方,最终形成一个闭环,如 A 依赖 B,B 依赖 C,C 依赖 A。如下图所示:

    大王叫下
  • 烂了大街的 Spring 循环依赖问题,你觉得自己会了吗

    初学 Spring 的时候,我们就知道 IOC,控制反转么,它将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理,不需要我们手动去各种 new ...

    海星
  • Spring5.0源码深度解析之Spring是如何利用三级缓存解决循环依赖的问题

    Spring已经成为了开发项目的不可缺少的组件了,我们在平常开发项目中难免会遇到以下这些情况,比如说,我有A类和B类,两个业务类都注入到Spring容器里了,且...

    黎明大大
  • 逐行阅读Spring5.X源码(十)spring如何解决循环引用,bean实例化过程源码详解

    bean的实例化是在refresh()——>finishBeanFactoryInitialization(beanFactory);方法里完成的。当然,只能实...

    源码之路
  • Java面试:2021.05.28

    大体来说,经历以下过程:接口需求调研、接口测试工具选择、接口测试用例编写、接口测试执行、接口测试回归、接口测试自动化持续集成。具体来说,接口测试流程分成以下九步...

    夕梦
  • Spring 循环依赖及三级缓存

    Spring启动过程大致如下: 1.加载配置文件 2.解析配置文件转化beanDefination,获取到bean的所有属性、依赖及初始化用到的各类处理...

  • 堂妹让我聊:Spring循环依赖

    在跟学弟学妹们聊完Spring IOC之后,有学弟反馈他们面试经常会遇到面试官询问Spring 里面的循环依赖问题。

    敖丙
  • Spring系列第56篇:一文搞懂spring到底为什么要用三级缓存??

    本文未指明 bean scope 默认情况下,所有 bean 都是单例的,即 scope 是 singleton,即下面所有问题都是在单例的情况下分析的。

    路人甲Java
  • 万字长文,助你深度遨游Spring循环依赖源码实现!

    最近有很多读者面试的时候都被问到了有关于Spring三级缓存的解决方案,很多读者在面试受挫之后,试着自己去读源码,试着去跟断点又发现一层套一层,一会自己就懵了,...

    止术
  • 同学,Spring 是怎么解决循环依赖的?

    循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。如下图:

    用户5224393
  • 《设计模式》.pdf

    不少人只会用框架,却看不懂源码,不了解其底层机制与实现原理,成了一名只会搬运源码库的开发。例如大家都知道 Mybatis 有 23 个设计模式,但是大多人只是停...

    GitHubDaily
  • spring:我是如何解决循环依赖的?

    最近项目组的一个同事遇到了一个问题,问我的意见,一下子引起的我的兴趣,因为这个问题我也是第一次遇到。平时自认为对spring循环依赖问题还是比较了解的,直到遇到...

    苏三说技术
  • 老师,Spring 是怎么解决循环依赖的?

    循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。如下图:

    Java知音
  • 7000字长文带你深入IOC加载流程

    来看一下传统的干活方式:在对象单一职责原则的基础上,一个对象很少有不依赖其他对象而完成自己的工作,所以这个时候就会出现对象之间的依赖。而体现在我们的开发中,就是...

    石的三次方

扫码关注云+社区

领取腾讯云代金券