面试官:Spring是如何解决循依赖问题?

news2024/9/23 2:24:37

Spring 的循环依赖一直都是 Spring 中一个很重要的话题,一方面是 Spring 为了解决循环依赖做了很多工作,另一个方面是因为它是面试 Spring 的常客,因为他要求你看过 Spring 的源码,如果没有看过 Spring 源码你基本上是回答不了这个问题的,虽然也有一些面试经会分析这个问题,但是如果别人深究的话,你没有看过源码真心回答不了。

什么是循环依赖

从字面上来理解就是 Spring Bean 之间的依赖产生了循环,例如 A 依赖 B ,B 依赖 C,C 依赖 A,如下:

代码大致如下:

@Service
public class AService {
    @Autowired
    private BService bService;
}

@Service
public class BService {
    @Autowired
    private CService cService;
}

@Service
public class CService {
    @Autowired
    private AService aService;
}


Spring 解决了哪些情况的循环依赖

在文章Spring 中的 Bean 有几种作用域?,大明哥说到 Spring 有五种作用域:

  1. singleton:单例作用域
  2. prototype:原型作用域
  3. request:请求作用域
  4. session:会话作用域
  5. application:全局作用域

但是 Spring 只解决单例作用域(singleton)循环依赖,主要原因如下:

  • prototype:每次请求都会创建新的 Bean 实例,会形成一个死循环,同时用一次就丢,解决循环依赖成本比较大。
  • requestsessionapplication:解决他们的循环依赖需要很复杂的处理机制,可能会引入额外的性能开销和复杂性。
  • 同时 ,Spring 的设计哲学倾向于鼓励良好的编程实践,依赖循环依赖可能是设计上的问题,我们应该是采取更加简洁和更加优雅的代码结构,而不是依赖 Spring 来解决它。

同时,在文章 Spring为什么建议使用构造器来注入 中讲到,Spring 有三种注入方式:

  • 基于字段的注入
  • 基于 setter 方法的注入
  • 基于构造器注入

对于这三种注入方式 ,Spring 不解决全是基于构造器注入的方式 ,因为 Spring 会报错,例如我们将上面的调整为构造器注入,启动时会报如下错误 :

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  AService defined in file [/xxx/AService.class]
↑     ↓
|  BService defined in file [/xxx/BService.class]
↑     ↓
|  CService defined in file [/xxx/CService.class]
└─────┘

注:我是依赖 Spring Boot 写的测试案例

基于上面的分析,我们知道 Spring 解决循环依赖的前提条件是 :

  1. 出现循环依赖的 B 必须是单例作用域。
  2. 依赖注入的方式不能全是构造器注入的方式。

有很多文章说,Spring 只解决 setter 方法的循环依赖,这是错误的,我们来演示下:

@Service
public class AService {

    @Autowired
    private BService bService;
}

B、C 还是构造器注入,你会发现这样启动是不会报错的。

基于这个问题,大明哥留一个思考题,如果 A、B、C 的注入方式是如下:

@Service
public class AService {

    private BService bService;

    @Autowired
    public AService(BService bService) {
        this.bService = bService;
    }
}

@Service
public class BService {

    @Autowired
    private CService cService;
}

@Service
public class CService {

    private AService aService;

    @Autowired
    public CService(AService aService) {
        this.aService = aService;
    }
}

会报错吗?为什么?

Spring 是怎么解决循环依赖的

Spring 解决循环依赖就靠三招:

  1. 单例模式
  2. 三级缓存
  3. 提前暴露、早起引用

单例模式前面已经分析过了,我们先看三级缓存。Spring 提供了三级缓存用来存储单例 的 Bean 实例(下面所说的 Bean 全部都为单例模式的 Bean),这三个缓存是互斥的,同一个 Bean 实例只会三级缓存中的一个中存在。三级缓存分别是:

  • 一级缓存singletonObjects
    • 用于存放完全初始化好的 Bean。
    • 当一个 Bean 被完全初始化好后(即所有的属性都被注入,所有的初始化方法都被调用),它会被放入这个缓存中。在此之后,每次请求这个 Bean 时,Spring 容器都会直接从这个缓存返回实例。
  • 二级缓存earlySingletonObjects
    • 用于存放提前暴露的 Bean 对象,即已经实例化但尚未完全初始化(未完成依赖注入和初始化方法调用)的 Bean,注意该 Bean 还处于创建中。
    • 该缓存是解决循环依赖的核心所在。当一个 Bean 正在创建的过程中,如果另外一个 Bean 需要引用它,则 Spring 为它提供该 Bean的一个早期引用,这个早期引用就存放在 earlySingletonObjects 缓存中。
  • 三级缓存singletonFactories
    • 存放 Bean 的工厂对象,用于生成 Bean 的早期引用。
    • 它是解决循环依赖的第一步。当一个 Bean 开始创建时,Spring 首先在这个缓存中放入一个工厂对象。这个工厂对象能够生成 Bean 的早期引用,当这个 Bean 需要被注入到其他 Bean 中时,就会通过这个工厂对象来创建早期引用。

三个缓存协同工作 ,以确保在应用中存在循环依赖的情况下,Spring 容器依然可以正确创建 Bean,并管理他们。

现在跟着大明哥的脚步来详细分析 Spring 是如何解决循环依赖的,为了更好地演示,我们将上面三个 Bean 循环依赖调整为两个即 A 依赖 B,B 依赖 A。

Spring 创建 Bean 的过程分为三个步骤:

  1. 实例化:AbstractAutowireCapableBeanFactory#createBean()
  2. 属性注入:AbstractAutowireCapableBeanFactory#populateBean()
  3. 初始化:AbstractAutowireCapableBeanFactory#initializeBean()

首先我们创建 A,先进行 A 对象的实例化过程 ,跟踪 AbstractAutowireCapableBeanFactory#createBean(),到 doCreateBean()

    protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
        // .....

        boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
        isSingletonCurrentlyInCreation(beanName));
        if (earlySingletonExposure) {
            // 加入到三级缓存 singletonFactories 中
            addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
        }

        // ...
    }

earlySingletonExposuretrue,则将 singletonFactory 保存到三级缓存 singletonFactories 中。条件为:

  • mbd.isSingleton():为单例模式
  • this.allowCircularReferences:检查配置是否允许循环引用
  • this.isSingletonCurrentlyInCreation(beanName):检查当前 Bean 是否正在创建。这是为了检测 Bean 是否处于创建的半成品状态(即已经开始创建但还没有完全初始化),这种状态的 Bean 是解决循环依赖的关键,因为他需要提前暴露给其他 Bean 引用,用来解决循环依赖 。

addSingletonFactory() 是将 singletonFactory 添加到三级缓存中:

  protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
      if (!this.singletonObjects.containsKey(beanName)) {
        // 添加到三级缓存中
        this.singletonFactories.put(beanName, singletonFactory);
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
      }
    }
  }

singletonFactory 由 getEarlyBeanReference() 创建:

  protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
      for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
        exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
      }
    }
    return exposedObject;
  }

这个方法是一个很重要的方法,用来出来 AOP 的,我们先放在这里,后面来分析。

A 实例化后,就调用 populateBean() 来完成属性注入,这里开始注入 B,调用 getBean(b) ,我们一直跟踪源代码到 getSingleton(String beanName, boolean allowEarlyReference),该方法有两个参数:

  • beanName:获取 Bean 实例的名称。
  • allowEarlyReference:用于指定是否允许早期引用。
    • true:允许在 Bean 的初始化过程中提前获取引用,即使 Bean 正在创建中。
    • false:只有在 Bean 创建完成后才能获取引用

代码如下:

  @Nullable
  protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 从一级缓存中获取完整的 Bean
    Object singletonObject = this.singletonObjects.get(beanName);
    // 如果 singletonObject 为空,且在新建中
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
      // 从二级缓存中获取该 Bean 的早期引用
      singletonObject = this.earlySingletonObjects.get(beanName);
      // 早期引用为空,且允许早期引用
      if (singletonObject == null && allowEarlyReference) {
        synchronized (this.singletonObjects) {
          // 双重检查
          // 一级
          singletonObject = this.singletonObjects.get(beanName);
          if (singletonObject == null) {
            // 二级
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null) {
              // 三级
              ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
              if (singletonFactory != null) {
                // 从单例工厂获取单例 Bean
                // 注意这个 Bean 还只是一个早期引用
                singletonObject = singletonFactory.getObject();
                
                // 将 Bean 从三级缓存移动到二级缓存去
                this.earlySingletonObjects.put(beanName, singletonObject);
                this.singletonFactories.remove(beanName);
              }
            }
          }
        }
      }
    }
    return singletonObject;
  }

由于 B 还没有创建,所以这里会返回 null 。故 B 依然和 A 一样去走创建过程,也会在三级缓存中存放 Bean 工厂。当 B 完成实例化后开始属性注入,这个时候它会调用 getBean(a) 去获取 A 的实例对象,由于 A 还处于创建过程中,一级缓存没有,二级缓存也没有,但是在三级缓存 A 提前暴露了一个 Bean 工厂对象,B 可以在三级缓存中获取 A 的 Bean 工厂对象,通过 singletonFactory.getObject() 获取 A 的早期引用,完成注入,也就是这段代码:

// 从单例工厂获取单例 Bean
// 注意这个 Bean 还只是一个早期引用
singletonObject = singletonFactory.getObject();
                
// 将 Bean 从三级缓存移动到二级缓存去
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);

B 完成注入后,开始初始化,最终得到一个完整 B。B 完成后,这个时候 A 得到的就是一个完整的 B 实例对象,A 完成注入,并进行初始化。整个过程如下图:

到这里循环依赖已经解决了。整个过程这张图已经详细阐述了,大明哥就不过多阐述了。在上面大明哥还埋了一个点,就是 getEarlyBeanReference(),我们继续。

我们知道注入到 B 中的 A 是通过 getEarlyBeanReference() 提前暴露出去的一个对象获取的,我们再看这个方法:

  protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
      for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
        exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
      }
    }
    return exposedObject;
  }

这里的参数 bean 就是已经实例化的 A 对象。

这行代码是关键:if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()),如果它为 false,这个方法等同于:

  protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    return exposedObject;
  }

返回的是一个原生的 A 对象,但是如果 if 语句为 true 呢?那么 exposedObject = bp.getEarlyBeanReference(exposedObject, beanName),调用后置处理器的 getEarlyBeanReference() ,然后真正实现这个方法的后置处理器就只有一个地方,那就是 AOP 的 AnnotationAwareAspectJAutoProxyCreator,如果我们的 A 进行了 AOP 代理 ,那么注入 B 的是 A 的代理对象而不是 A 本身。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2156357.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

pytorch的动态计算图机制

pytorch的动态计算图机制 一&#xff0c;动态计算图简介 Pytorch的计算图由节点和边组成&#xff0c;节点表示张量或者Function&#xff0c;边表示张量和Function之间的依赖关系。 Pytorch中的计算图是动态图。这里的动态主要有两重含义。 第一层含义是&#xff1a;计算图的…

“吉林一号”宽幅02B系列卫星

离轴四反光学成像系统 1.光学系统参数&#xff1a; 焦距&#xff1a;77.5mm&#xff1b; F/#&#xff1a;7.4&#xff1b; 视场&#xff1a;≥56゜&#xff1b; 光谱范围&#xff1a;400nm&#xff5e;1000nm。 2.说明&#xff1a; 光学系统采用离轴全反射式结构&#xff0c;整…

解密的军事卫星图像在各种民用地理空间研究中都有应用

一、美军光学侦察卫星计划概述 国家侦察局 &#xff08;NRO&#xff09; 负责开发和操作太空侦察系统&#xff0c;并为美国国家安全开展情报相关活动。NRO 开发了几代机密锁眼 &#xff08;KH&#xff09; 军事光学侦察卫星&#xff0c;这些卫星一直是美国国防部 &#xff08;D…

人工智能不是人工“制”能

文/孟永辉 如果你去过今年在上海举办的世界人工智能大会&#xff0c;就会知道当下的人工智能行业在中国是多么火爆。 的确&#xff0c;作为第四次工业革命的重要组成部分&#xff0c;人工智能愈发引起越来越多的重视。 不仅仅是在中国&#xff0c;当今世界的很多工业强国都在将…

python爬虫案例——异步加载网站数据抓取,post请求(6)

文章目录 前言1、任务目标2、抓取流程2.1 分析网页2.2 编写代码2.3 思路分析前言 本篇案例主要讲解异步加载网站如何分析网页接口,以及如何观察post请求URL的参数,网站数据并不难抓取,主要是将要抓取的数据接口分析清楚,才能根据需求编写想要的代码。 1、任务目标 目标网…

Win10 安装Node.js 以及 Vue项目的创建

一、Node.js和Vue介绍 1. Node.js Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。它允许你在服务器端运行 JavaScript&#xff0c;使得你能够使用 JavaScript 来编写后端代码。以下是 Node.js 的一些关键特点&#xff1a; 事件驱动和非阻塞 I/O&#xff1a;Node…

list(一)

list是可以在常数范围内在任意位置进行插入和删除的序列式容器&#xff0c;并且该容器可以前后双向迭代。list的底层是双向链表结构&#xff0c;双向链表中每个元素存储在互不相关的独立节点中&#xff0c;在节点中通过指针指向 其前一个元素和后一个元素。 支持 -- 但是不支持…

Linux:终端(terminal)与终端管理器(agetty)

终端的设备文件 打开/dev目录可以发现其中有许多字符设备文件&#xff0c;例如对于我的RedHat操作系统&#xff0c;拥有tty0到tty59&#xff0c;它们是操作系统提供的终端设备。对于tty1-tty12使用ctrlaltF*可以进行快捷切换&#xff0c;下面的命令可以进行通用切换。 sudo ch…

校园热捧的“人气新贵”,D 咖智能饮品机器人

在 2024 年的校园中&#xff0c;一股全新的潮流正在悄然兴起。D 咖智能饮品机器人以其独特的魅力&#xff0c;成功入驻多个校园&#xff0c;迅速成为学生们热烈追捧的对象&#xff0c;在长江大学、荆州职业技术学院、中医高专等多个大学校园&#xff0c;都能发现他们靓丽的身姿…

calibre-web报错:File type isn‘t allowed to be uploaded to this server

calibre-web报错&#xff1a;File type isnt allowed to be uploaded to this server 最新版的calibre-web在Upload时候会报错&#xff1a; File type isnt allowed to be uploaded to this server 解决方案&#xff1a; Admin - Basic Configuration - Security Settings 把…

投资学 01 定义,投资

02. 03. 3.1 直接投资&#xff1a;使用方和提供方是一个人

VUE3学习---【一】【从零开始的VUE学习】

目录​​​​​​​ 什么是Vue 渐进式框架 创建一个Vue应用 什么是Vue应用 使用Vue应用 根组件 挂载应用 模板语法 文本插值 原始HTML Attribute绑定 简写 同名简写 布尔型Attribute 动态绑定多个值 使用JavaScript表达式 仅支持表达式 指令 Directives 指令…

COLORmap

在这段MATLAB代码中&#xff0c;surf(peaks)、map的定义以及colormap(map)的调用共同完成了以下任务&#xff1a; 1. **绘制曲面图**&#xff1a; - surf(peaks)&#xff1a;这个函数调用了MATLAB内置的peaks函数来生成数据&#xff0c;并使用surf函数将这些数据绘制成一个…

双向链表:实现、操作与分析【算法 17】

双向链表&#xff1a;实现、操作与分析 引言 双向链表&#xff08;Doubly Linked List&#xff09;是链表数据结构的一种重要形式&#xff0c;它允许节点从两个方向进行遍历。与单向链表相比&#xff0c;双向链表中的每个节点不仅包含指向下一个节点的指针&#xff08;或引用&…

蓝桥杯嵌入式的学习总结

一. 前言 嵌入式竞赛实训平台(CT117E-M4) 是北京国信长天科技有限公司设计&#xff0c;生产的一款 “ 蓝桥杯全国软件与信息技术专业人才大赛-嵌入式设计与开发科目 “ 专用竞赛平台&#xff0c;平台以STM32G431RBT6为主控芯片&#xff0c;预留扩展板接口&#xff0c;可为用户提…

数据结构篇--顺序查找【详解】

概念章 查找就是在数据集合中寻找某种条件的数据元素的过程。 查找表是指用于查找同一类型的数据元素集合。 找到了满足条件的数据元素&#xff0c;就是查找成功&#xff0c;否则就是称为查找失败。 关键字是指数据元素的某个数据项的值&#xff0c;可用于标识或者记录&…

【Java】线程暂停比拼:wait() 和 sleep()的较量

欢迎浏览高耳机的博客 希望我们彼此都有更好的收获 感谢三连支持&#xff01; 在Java多线程编程中&#xff0c;合理地控制线程的执行是至关重要的。wait()和sleep()是两个常用的方法&#xff0c;它们都可以用来暂停线程的执行&#xff0c;但它们之间存在着显著的差异。本文将详…

【AI学习笔记】初学机器学习西瓜书概要记录(二)常用的机器学习方法篇

初学机器学习西瓜书的概要记录&#xff08;一&#xff09;机器学习基础知识篇(已完结) 初学机器学习西瓜书的概要记录&#xff08;二&#xff09;常用的机器学习方法篇(持续更新) 初学机器学习西瓜书的概要记录&#xff08;三&#xff09;进阶知识篇(待更) 文字公式撰写不易&am…

Django 基础之启动命令和基础配置

Django启动 django启动一般可以通过ide或者命令启动 ide启动&#xff1a; 启动命令&#xff1a; python manage.py runserver该命令后续可以增加参数&#xff0c;如&#xff1a; python manage.py runserver 8081 python manage.py runserver 127.0.0.1:8082 注意&#xff1…

StopIteration: 迭代停止完美解决方法 ️

&#x1f504; StopIteration: 迭代停止完美解决方法 &#x1f6e0;️ &#x1f504; StopIteration: 迭代停止完美解决方法 &#x1f6e0;️摘要引言正文1. 什么是StopIteration异常&#xff1f;&#x1f4dc;2. StopIteration在for循环中的处理机制&#x1f6a6;3. 如何自定…