高频面试:Spring 如何解决循环依赖?

2023年 10月 7日 62.1k 0

Spring 如何解决循环依赖,网上的资料很多,但是感觉写得好的极少,特别是源码解读方面,我就自己单独出一篇,这篇文章绝对肝!

不 BB,上文章目录。

图片图片

1. 基础知识

1.1 什么是循环依赖 ?

一个或多个对象之间存在直接或间接的依赖关系,这种依赖关系构成一个环形调用,有下面 3 种方式。

图片图片

我们看一个简单的 Demo,对标“情况 2”。

@Service
public class Louzai1 {

    @Autowired
    private Louzai2 louzai2;

    public void test1() {
    }
}

@Service
public class Louzai2 {
    @Autowired
    private Louzai1 louzai1;

    public void test2() {
    }
}

这是一个经典的循环依赖,它能正常运行,后面我们会通过源码的角度,解读整体的执行流程。

1.2 三级缓存

解读源码流程之前,spring 内部的三级缓存逻辑必须了解,要不然后面看代码会蒙圈。

  • 第一级缓存:singletonObjects,用于保存实例化、注入、初始化完成的 bean 实例;
  • 第二级缓存:earlySingletonObjects,用于保存实例化完成的 bean 实例;
  • 第三级缓存:singletonFactories,用于保存 bean 创建工厂,以便后面有机会创建代理对象。

这是最核心,我们直接上源码:

图片图片

执行逻辑:

  • 先从“第一级缓存”找对象,有就返回,没有就找“二级缓存”;
  • 找“二级缓存”,有就返回,没有就找“三级缓存”;
  • 找“三级缓存”,找到了,就获取对象,放到“二级缓存”,从“三级缓存”移除。

1.3 原理执行流程

我把“情况 2”执行的流程分解为下面 3 步,是不是和“套娃”很像 ?

图片图片

整个执行逻辑如下:

  • 在第一层中,先去获取 A 的 Bean,发现没有就准备去创建一个,然后将 A 的代理工厂放入“三级缓存”(这个 A 其实是一个半成品,还没有对里面的属性进行注入),但是 A 依赖 B 的创建,就必须先去创建 B;
  • 在第二层中,准备创建 B,发现 B 又依赖 A,需要先去创建 A;
  • 在第三层中,去创建 A,因为第一层已经创建了 A 的代理工厂,直接从“三级缓存”中拿到 A 的代理工厂,获取 A 的代理对象,放入“二级缓存”,并清除“三级缓存”;
  • 回到第二层,现在有了 A 的代理对象,对 A 的依赖完美解决(这里的 A 仍然是个半成品),B 初始化成功;
  • 回到第一层,现在 B 初始化成功,完成 A 对象的属性注入,然后再填充 A 的其它属性,以及 A 的其它步骤(包括 AOP),完成对 A 完整的初始化功能(这里的 A 才是完整的 Bean)。
  • 将 A 放入“一级缓存”。
  • 为什么要用 3 级缓存 ?我们先看源码执行流程,后面我会给出答案。

    2. 源码解读

    注意:Spring 的版本是 5.2.15.RELEASE,否则和我的代码不一样!!!

    上面的知识,网上其实都有,下面才是我们的重头戏,让你跟着楼仔,走一遍代码流程。

    2.1 代码入口

    图片图片

    图片图片

    这里需要多跑几次,把前面的 beanName 跳过去,只看 louzai1。

    图片图片

    图片图片

    2.2 第一层

    图片图片

    进入 doGetBean(),从 getSingleton() 没有找到对象,进入创建 Bean 的逻辑。

    图片图片

    图片图片

    进入 doCreateBean() 后,调用 addSingletonFactory()。

    图片图片

    往三级缓存 singletonFactories 塞入 louzai1 的工厂对象。

    图片图片

    图片图片

    进入到 populateBean(),执行 postProcessProperties(),这里是一个策略模式,找到下图的策略对象。

    图片图片

    正式进入该策略对应的方法。

    图片图片

    下面都是为了获取 louzai1 的成员对象,然后进行注入。

    图片图片

    图片图片

    图片图片

    图片图片

    进入 doResolveDependency(),找到 louzai1 依赖的对象名 louzai2

    图片图片

    需要获取 louzai2 的 bean,是 AbstractBeanFactory 的方法。

    图片图片

    正式获取 louzai2 的 bean。

    图片图片

    到这里,第一层套娃基本结束,因为 louzai1 依赖 louzai2,下面我们进入第二层套娃。

    2.3 第二层

    图片图片

    获取 louzai2 的 bean,从 doGetBean(),到 doResolveDependency(),和第一层的逻辑完全一样,找到 louzai2 依赖的对象名 louzai1。

    前面的流程全部省略,直接到 doResolveDependency()。

    图片图片

    正式获取 louzai1 的 bean。

    图片图片

    到这里,第二层套娃结束,因为 louzai2 依赖 louzai1,所以我们进入第三层套娃。

    2.4 第三层

    图片图片

    获取 louzai1 的 bean,在第一层和第二层中,我们每次都会从 getSingleton() 获取对象,但是由于之前没有初始化 louzai1 和 louzai2 的三级缓存,所以获取对象为空。

    图片图片

    图片

    敲重点!敲重点!!敲重点!!!

    到了第三层,由于第三级缓存有 louzai1 数据,这里使用三级缓存中的工厂,为 louzai1 创建一个代理对象,塞入二级缓存。

    图片图片

    这里就拿到了 louzai1 的代理对象,解决了 louzai2 的依赖关系,返回到第二层。

    2.5 返回第二层

    返回第二层后,louzai2 初始化结束,这里就结束了么?二级缓存的数据,啥时候会给到一级呢?

    甭着急,看这里,还记得在 doGetBean() 中,我们会通过 createBean() 创建一个 louzai2 的 bean,当 louzai2 的 bean 创建成功后,我们会执行 getSingleton(),它会对 louzai2 的结果进行处理。

    图片图片

    我们进入 getSingleton(),会看到下面这个方法。

    图片图片

    这里就是处理 louzai2 的 一、二级缓存的逻辑,将二级缓存清除,放入一级缓存。

    图片图片

    2.6 返回第一层

    同 2.5,louzai1 初始化完毕后,会把 louzai1 的二级缓存清除,将对象放入一级缓存。

    图片图片

    到这里,所有的流程结束,我们返回 louzai1 对象。

    3. 原理深度解读

    3.1 什么要有 3 级缓存 ?

    这是一道非常经典的面试题,前面已经告诉大家详细的执行流程,包括源码解读,但是没有告诉大家为什么要用 3 级缓存?

    这里是重点!敲黑板!!!

    我们先说“一级缓存”的作用,变量命名为 singletonObjects,结构是 Map,它就是一个单例池,将初始化好的对象放到里面,给其它线程使用,如果没有第一级缓存,程序不能保证 Spring 的单例属性。

    “二级缓存”先放放,我们直接看“三级缓存”的作用,变量命名为 singletonFactories,结构是 Map

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论