创意电子

标题: Spring 的循环依靠,源码具体分析 → 真的非要三级缓存吗 [打印本页]

作者: 贩卖日落和猫的脚步声    时间: 2021-10-16 07:20
标题: Spring 的循环依靠,源码具体分析 → 真的非要三级缓存吗
保举阅读:这份 500页的4大实战20W+字解析的《Spring Security》,果断收下
开心一刻


                               
登录/注册后可看大图

写作配景

  做 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 中是如何创建的呢,我们一起来跟下源码

                               
登录/注册后可看大图

    接下来,我们从 DefaultListableBeanFactorypreInstantiateSingletons 方法开始 debug

                               
登录/注册后可看大图

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

                               
登录/注册后可看大图

    我们接着从 createBean 往下跟

                               
登录/注册后可看大图

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

                               
登录/注册后可看大图

    此时:代理对象的创建是在对象实例化完成,并且初始化也完成之后进行的,是对一个成品对象创建代理对象
    所以此种环境下:只用一级缓存就够了,其他两个缓存可以不要
  循环依赖,没有AOP

    代码仍旧非常简单:spring-circle-simple,此时循环依赖的两个类是: CircleLoop
    对象的创建过程与前面的基本同等,只是多了循环依赖,少了 AOP,所以我们重点关注: populateBeaninitializeBean 方法
    先创建的是 Circle 对象,那么我们就从创建它的 populateBean 开始,再开始之前,我们先看看三级缓存中的数据环境

                               
登录/注册后可看大图


                               
登录/注册后可看大图

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

                               
登录/注册后可看大图

    对 circle 对象的属性 loop 进行填充的时间,去 Spring 容器中找 loop 对象,发现没有则进行创建,又来到了认识的 createBean
    此时三级缓存中的数据没有变化,但是 Set 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 的代理对象放到了第二级缓存中,并将代理对象返回赋值给了 半成品 loopcircle 属性
    注意:此时是在进行 loop 的初始化,但却把 半成品 circle 的代理对象提前创建出来了
     loop 的初始化还未完成,我们接着往下看,又是一个重点,细致看

                               
登录/注册后可看大图

    在 initializeBean 方法中完成了 半成品 loop 的初始化,并在最后创建了 loop 成品 的代理对象
     loop 代理对象创建完成之后会将其放入到第一级缓存中(移除第三级缓存中的 loop ,第二级缓存自始至终都没有 loop
    然后将 loop 代理对象返回并赋值给 半成品 circle 的属性 loop ,接着进行 半成品 circleinitializeBean

                               
登录/注册后可看大图

    因为 circle 的代理对象已经天生过了(在第二级缓存中),所以不用再天生代理对象了;将第二级缓存中的 circle 代理对象移到第一级缓存中,并返回该代理对象
    此时各级缓存中的数据环境如下(普通 circleloop 对象在各自代理对象的 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 用了第三级缓存,既维持了设计原则,又处理了循环依赖;牺牲那么一丢丢内存空间是乐意接受的
原文链接:https://www.cnblogs.com/youzhibing/p/14337244.html

作者: 牧涯475    时间: 2021-10-16 08:37
转发了
作者: 面向对象编程    时间: 2021-10-16 08:43
转发了
作者: 十八里铺孩子王    时间: 2021-10-16 09:01
转发了
作者: 谨守Version0915    时间: 2021-10-16 09:41
转发了
作者: 极客自修室    时间: 2021-10-16 09:45
转发了
作者: 诗舒    时间: 2021-10-16 09:58
转发了
作者: 三月177102128    时间: 2021-10-16 12:37
转发了
作者: 独钓寒江雪之IT    时间: 2021-10-16 13:22
转发了
作者: 小船掀大浪    时间: 2021-10-16 15:20
转发了
作者: JAVA码农痴情者    时间: 2021-10-16 15:41
转发了
作者: D5娱乐看客    时间: 2021-10-16 16:05
转发了
作者: 且听雨声入眠夜    时间: 2021-10-16 16:06
转发了
作者: 1古德刚    时间: 2021-10-16 16:41
转发了
作者: 我爱白大宝宝    时间: 2021-10-16 19:07
转发了
作者: hh3266719543952    时间: 2021-10-16 20:01
转发了
作者: 懂球    时间: 2021-10-16 21:14
转发了
作者: Heisq1an    时间: 2021-10-16 23:14
转发了
作者: 幻415    时间: 2021-10-16 23:24
转发了
作者: 老妖不吃人    时间: 2021-10-16 23:35
转发了
作者: 老妖不吃人    时间: 2021-10-16 23:39
转发了




欢迎光临 创意电子 (https://wxcydz.cc/) Powered by Discuz! X3.4