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

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

什么是循环依赖

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

代码大致如下:

@Service
public class AService {@Autowiredprivate BService bService;
}@Service
public class BService {@Autowiredprivate CService cService;
}@Service
public class CService {@Autowiredprivate 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 {@Autowiredprivate BService bService;
}

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

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

@Service
public class AService {private BService bService;@Autowiredpublic AService(BService bService) {this.bService = bService;}
}@Service
public class BService {@Autowiredprivate CService cService;
}@Service
public class CService {private AService aService;@Autowiredpublic 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 创建完成后才能获取引用

代码如下:

  @Nullableprotected Object getSingleton(String beanName, boolean allowEarlyReference) {// 从一级缓存中获取完整的 BeanObject 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.xdnf.cn/news/147996.html

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

相关文章

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

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

移动技术开发:RecyclerView瀑布流水果列表

1 实验名称 RecyclerView瀑布流水果列表 2 实验目的 掌握RecyclerView控件的实现方法和基本应用 3 实验源代码 布局文件代码&#xff1a; activity_main&#xff1a; <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android&q…

Mac系统Docker中SQLserver数据库文件恢复记录

Mac系统Docker中SQLserver数据库文件恢复记录 Mac想要安装SQLsever&#xff0c;通过docker去拉去镜像是最简单方法。 一、下载Docker Docker 下载安装&#xff1a; 需要‘科学上网’ 才能访问到docker官网。&#xff08; https://docs.docker.com/desktop/install/mac-ins…

18.2K Star,AI 高效视频监控摄像头

Hi&#xff0c;骚年&#xff0c;我是大 G&#xff0c;公众号「GitHub 指北」会推荐 GitHub 上有趣有用的项目&#xff0c;一分钟 get 一个优秀的开源项目&#xff0c;挖掘开源的价值&#xff0c;欢迎关注。 导语 在家庭和企业安防领域&#xff0c;实时视频监控是保障安全的核…

2024 SNERT 预备队招新 CTF 体验赛-Web

目录 1、robots 2、NOF12 3、get_post 4、好事慢磨 5、uploads 6、rce 7、ezsql 8、RCE 1、robots robots 协议又叫爬虫协议&#xff0c;访问 robots.txt 继续访问 /JAY.php 拿到 flag&#xff1a;flag{hello_Do_YOU_KONw_JAY!} 2、NOF12 F12 和右键都被禁用 方法&#…

22、Raven2

难度 中 目标 root权限 4个flag 使用Virtualbox启动 kali 192.168.86.105 靶机 192.168.86.106 信息收集 看到111端口有一个rpc相关的东西&#xff0c;去网上查看了一下是什么服务 通过在网上搜索发现这是一个信息泄露的漏洞&#xff0c;上面的这个端口其实就是泄露的端口和…

【Spring 底层原理】手搓一个Spring框架

文章目录 准备工作Spring 框架到底在干啥&#xff1f;几个概念辨析注解的定义自定义核心注解配置类启动类辅助类 Spring 容器XxxAware 回调机制初始化机制前置、后置处理器完整的容器代码源码下载 最近工作接触到的知识比较底层&#xff0c;因此为了突破瓶颈&#xff0c;彻底搞…

ubuntu+MobaXterm+ssh+运行Qt(成功版)

点击上方"蓝字"关注我们 01、ubuntu连接SSH >>> 通过串口工具连接ubuntu 登录 解决连接不上的问题 检查 SSH 服务:确保目标机器上 SSH 服务已启动。你可以在目标机器上运行以下命令: sudo systemctl status ssh 如果没有运行,可以使用以下命令启动 SSH …

英特尔AI加速器Gaudi 3下周发布,挑战NVIDIA统治地位!

英特尔正稳步推进其2024年计划&#xff0c;备受瞩目的AI加速器Gaudi3预计将于下周震撼登场。这款被誉为英特尔AI英雄的产品&#xff0c;专注于处理大规模训练和推理任务&#xff0c;拥有无与伦比的扩展能力。面对市场对高效能半导体的旺盛需求&#xff0c;英特尔首席执行官帕特…

FX5 CPU模块和以太网模块的以太网通信功能

FX5 CPU模块和以太网模块的以太网通信功能的概要如下所示。 CPU模块的内置以太网端口的通信规格如下所示。 1、与MELSOFT的直接连接 不使用集线器&#xff0c;用1根以太网电缆直接连接以太网搭载模块与工程工具(GX Torks3)。无需设定IP地址&#xff0c;仅连接目标指定即可进行…

无服务器计算构建人工智能管理区块链系统

图片发自简书App 图片发自简书App 本发明属于网络版权管理技术领域&#xff0c;特别涉及一种以交易信息作 为唯一标准发行虚拟币的区块链系统。 背景技术 数字代币如比特币、以太坊等是区块链技术的实现方式之一&#xff0c;目 标是取代法定货币流通&#xff0c;通过“挖矿”的…

前端-js例子:收钱转账

支付宝转账 在这里用到周期定时器setInterval(function,time)&#xff0c;设置达到目标钱数时停止定时器。 点击转账按钮时&#xff0c;开始函数显示。 同时要确定输入框里输入的是数字。&#xff08;有一定容错&#xff09; window.onloadfunction(){var btn document.que…

什么是慢充优惠话费充值api?如何选择平台

一、话费充值api的定义 话费充值api是一种能够让开发者将话费充值功能集成到自己的平台的接口。通过接入话费充值api接口&#xff0c;就能够实现话费充值平台的搭建&#xff0c;从而为用户提供话费充值服务&#xff0c;这一接口主要适用于对话费充值有长期稳定需求的企业或者商…

K8s容器运行时,移除Dockershim后存在哪些疑惑?

K8s容器运行时&#xff0c;移除Dockershim后存在哪些疑惑&#xff1f; 大家好&#xff0c;我是秋意零。 K8s版本截止目前&#xff08;24/09&#xff09;已经发布到了1.31.x版本。早在K8s版本从1.24.x起&#xff08;22/05&#xff09;&#xff0c;默认的容器运行时就不再是Doc…

排序-----归并排序(递归版)

核心思想&#xff1a;假设数组前后两部分各自有序&#xff0c;然后各定义两个指针&#xff0c;谁小谁放到新开辟的数组里面&#xff0c;最后把新开辟的数组赋值给原数组就完成了。要使前后两部分有序就采用递归的方式&#xff0c;不断往下划分块&#xff0c;最后一层划分为两个…

01 基础request

目录 类 WxRequest 的定义 静态属性 default 构造函数 constructor 方法 request HTTP 方法封装 创建 WxRequest 实例并导出 完整代码&#xff1a; 类 WxRequest 的定义 创建一个 WxRequest 类包含一个静态属性 default 和几个方法&#xff0c;用于处理网络请求。 静态…

【后端开发】JavaEE初阶—Theard类及常见方法—线程的操作(超详解)

前言&#xff1a; &#x1f31f;&#x1f31f;本期讲解多线程的知识哟~~~&#xff0c;希望能帮到屏幕前的你。 &#x1f308;上期博客在这里&#xff1a;【后端开发】JavaEE初阶—线程的理解和编程实现-CSDN博客 &#x1f308;感兴趣的小伙伴看一看小编主页&#xff1a;GGBondl…

计算机毕业设计之:基于深度学习的路面检测系统(源码+部署文档+讲解)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

鸿蒙OpenHarmony【轻量系统内核扩展组件(CPU占用率)】子系统开发

基本概念 CPU&#xff08;中央处理器&#xff0c;Central Processing Unit&#xff09;占用率分为系统CPU占用率和任务CPU占用率。 系统CPU占用率&#xff1a;是指周期时间内系统的CPU占用率&#xff0c;用于表示系统一段时间内的闲忙程度&#xff0c;也表示CPU的负载情况。系…

INIT与init_array

INIT与init array 1.so执行JNI_OnLoad之前&#xff0c;还会执行俩个构造函数init 和init array 在so加载时候有这个过程&#xff1a; .init -> .init array -> JNI_Onload -> java_com_xxx 在脱壳的过程中会在一些系统级的.so中下断点比如&#xff1a;fopen&#x…