详解Spring的循环依赖问题、三级缓存解决方案源码分析

2023-11-16

0、基础:Bean的生命周期

        在Spring中,由于IOC的控制反转,创建对象不再是简单的new出来,而是交给Spring去创建,会经历一系列Bean的生命周期才创建出相应的对象。而循环依赖问题也是由Bean的生命周期过程导致的问题,因此我们首先需要了解Bean的生命周期。

Bean的生命周期可以概括为4步:

实例化----属性注入----初始化----销毁

详细的讲,步骤如下:

        ******************************************实例化123*******************************************

  1. 定位:Spring容器会根据配置文件(如XML、注解等)或编程式配置来确定需要创建的Bean。
  2. 加载:Spring容器会加载配置文件并解析其中的Bean定义,将其转换为内部数据结构,例如BeanDefinition。

  3. 实例化:在实例化阶段,Spring容器会根据Bean定义中的信息创建Bean的实例。这个过程可以通过构造函数实例化、工厂方法实例化或者通过反射机制来实现。*****************************************属性注入4*********************************************

  4. 属性注入:在实例化Bean之后,Spring容器会对Bean进行属性注入。这可以通过setter方法注入、构造函数参数注入或字段注入等方式来完成。
    ****************************************初始化5~9********************************************

  5. Aware接口回调:如果Bean实现了Spring的Aware接口,容器会通过回调方式将一些特殊的资源注入到Bean中。例如,如果Bean实现了BeanFactoryAware接口,容器会将当前的BeanFactory实例注入到Bean中。

  6. 初始化前回调:如果Bean实现了InitializingBean接口,容器会在初始化之前调用它的afterPropertiesSet()方法,给Bean一个机会执行一些初始化操作。同时,Spring容器还支持使用自定义的初始化方法。

  7. 初始化后回调:如果Bean配置了初始化回调方法,容器会调用该方法进行一些自定义的初始化逻辑处理。

  8. 后置处理器(BeanPostProcessor):Spring容器会调用注册的Bean后置处理器对Bean进行加工和增强。例如,可以通过AOP技术在这个阶段为Bean动态生成代理对象。

  9. 完成:至此,Bean已经成功创建,并且已经完成了所有的初始化过程。此时可以将Bean提供给其他对象使用。
    ****************************************销毁10、11******************************************

  10. 销毁前回调(PreDestroy):在容器关闭之前,调用Bean的销毁前回调方法,执行一些清理操作和释放资源的任务。

  11. 销毁:容器关闭时,销毁所有Bean实例,包括调用相应Bean的销毁方法,进行最终的清理和资源释放。

 1、循环依赖问题

        例如下面的代码,A和B类就构成了循环依赖,原因如下:

@Component
public class A {
   
   @Autowired
   private B b;
}
@Component
public class B{

   @Autowired
   private A a;
}

创建Bean的步骤: 

  1. Spring 扫描 class 得到 BeanDefinition;
  2. 根据得到的 BeanDefinition 去生成 bean;
  3. 首先根据 class 推断构造方法;
  4. 根据推断出来的构造方法,反射,得到一个对象(我们称为原始对象);
  5. 填充原始对象中的属性(依赖注入);
  6. 如果原始对象中的某个方法被 AOP 了,那么则需要根据原始对象生成一个代理对象;
  7. 把最终生成的代理对象放入单例池(源码中叫做 singletonObjects)中,下次 getBean 时就直接从单例池拿即可;

        对于上述步骤的第4步, 得到原始对象后需要注入属性,A 类中存在一个 B 类的 b 属性,此时就会根据 b 属性的类型和属性名去 BeanFactory 中去获取 B 类所对应的单例bean
        如果此时 B 类在 BeanFactory 中还没有生成对应的 Bean,那么就需要去生成,就会经过 B 的 Bean 的生命周期,也就会同样的,需要A类的Bean,就发生了循环依赖,导致A和B的bean都创建不出来。

 

        概括而言:
        A Bean创建–>依赖了 B 属性–>触发 B Bean创建—>B 依赖了 A 属性—>需要 A Bean(但A Bean还在创建过程中)

        然而实际上,Spring通过三级缓存的方式自动解决了这个问题 。

2、三级缓存的引入

2.1 非AOP情况下的解决方案

         根据上文的分析我们发现,出现循环依赖的根本原因,是B的Bean需要注入A属性的时候,Bean A还没有创建出来,导致的。那么相应的,只要:
        在进行依赖注入之前,先把 A 的原始 Bean 放入缓存提早暴露,只要放到缓存了,其他 Bean 需要时就可以从缓存中拿了,这个缓存就应该是earlySingletonObjects),放入缓存后,再进行依赖注入。
        由于提前暴露,在创建B的Bean过程中,当需要注入A的属性时,就可以从缓存中拿到A提前暴露的原始对象(还不是最终Bean),就解决了问题。关键在于全程只有一个A的原始对象,其后续的生命周期没有变化。
        如下图所示:

   2.2 三级缓存具体     

        因此,对于不同时期的Bean,如原始Bean、完整周期的Bean,需要不同的缓存来存放,底层源码中有三级缓存: 

/** Cache of singleton objects: bean name –> bean instance */
private final Map singletonObjects = new ConcurrentHashMap(256);

/** Cache of singleton factories: bean name –> ObjectFactory */
private final Map> singletonFactories = new HashMap>(16);

/** Cache of early singleton objects: bean name –> bean instance */
private final Map earlySingletonObjects = new HashMap(16);
  • 一级缓存:singletonObjects;缓存的是已经经历了完整生命周期的bean对象。
  • 二级缓存:earlySingletonObjects;比 singletonObjects 多了一个 early ,表示缓存的是早期的 bean对象(原始对象)。早期指的是 Bean 的生命周期还没走完就把这个 Bean 放入了 earlySingletonObjects
  • 三级缓存:singletonFactories;缓存的是 ObjectFactory,表示对象工厂,用来创建某个对象的。

3、有AOP情况下使用singletonFactories 

3.1 引入三级缓存

        看似我们只需要1、2级缓存就能够解决问题了,为什么需要三级缓存呢?
        这就需要考虑到AOP代理对象的问题了:

        上文的红字提到,之所以能够提前暴露,是因为假定的A的原始对象始终是同一个对象,但如果有AOP的情况下呢?我们考虑这样的场景:

         按照上文的分析,假设创建B的bean过程中,注入了A的原始对象属性。然后,A的原始对象采用AOP产生了一个代理对象,即,A的Bean变成了 AOP 之后的代理对象。而B中的 属性a对应的并不是 AOP 之后的代理对象,而仍然是原始对象。

        也就是说,这种情况下,B 依赖的 A 和最终的 A 不是同一个对象!

        而解决这个问题的方法,就是引入三级缓存的 singletonFactories 

 3.2 三级缓存具体解析

        实际上,在有AOP的情况下,Spring并没有像第2节所说,直接将示例缓存到二级缓存,而是生成完原始对象之后”多此一举“地将实例先封装到objectFactory中,在需要引用的时候再通过singletonFactory.getObject()获取。

        跟进getObject()方法,其实执行了getEarlyBeanReference这个关键方法。

    this.addSingletonFactory(beanName, () -> {
		return this.getEarlyBeanReference(beanName, mbd, bean);
	});

         也就是说,Spring将当前bean缓存到earlyProxyReferences中,标识提前曝光的bean。而wrapIfNecessary是用于Spring AOP自动代理的,也就是说在被提前引用前,进行了AOP代理,并得到了代理对象。
        此时earlySingletonObjects 缓存中的对象就是代理对象了!

        因此,假设此时有其他对象依赖了A,就可以从earlySingletonObjects中获取到A原始对象的代理对象了,并且和A是同一个对象,实现了目标。

3.3 后续依赖问题

        当 B 创建完了之后,A 继续进行生命周期,而 A 在完成属性注入后,会按照它本身的逻辑去进行AOP,而此时我们知道 A 原始对象已经经历过了 AOP ,所以对于 A 本身而言,不会再去进行 AOP了,那么怎么判断一个对象是否经历过了 AOP 呢?

         注意postProcessAfterInitialization方法,会当前beanName是否在earlyProxyReferences中,如果在就AOP过了,不在则执行AOP方法。

         此时对于Bean A对象而言已经完成创建了,可以把它放入缓存singletonObjects中了,因此从earlySingletonObjects 中得到代理对象,然后入 singletonObjects 中。

        至此,整个循环依赖解决完毕。

4、总结 

        这里引用看到的一篇写的很清晰的博客http://t.csdn.cn/GeHkA的图来说明具体流程:

Spring AOP循环依赖 

         对于三级缓存的singletonFactories,总结而言

        缓存的是一个 ObjectFactory ,主要用来去生成原始对象进行了 AOP之后得到的「代理对象」。
        在每个 Bean 的生成过程中,都会提前暴露一个工厂,这个工厂可能用到,也可能用不到:
(1)如果没有出现循环依赖依赖本 bean,那么这个工厂无用,本 bean 按照自己的生命周期执行,执行完后直接把本 bean 放入 singletonObjects 中即可(对应本文章的第1节)
(2)如果出现了循环依赖依赖了本 bean,则:
        <2.1>如果有 AOP 的话,另外那个 bean 执行 ObjectFactory 提交得到一个 AOP 之后的代理对象。(对应本文章第3节)
        <2.2>如果无需 AOP ,则直接得到一个原始对象。(对应本文章第2节)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

详解Spring的循环依赖问题、三级缓存解决方案源码分析 的相关文章

随机推荐

  • 服务器入门

    GPU工作站 服务器 1 cdot 1 1 型号 AMD宵龙 RTX3090 为例 内存类型 REG 内存 8个DIMM DDR4插槽 3200高频内存 gt system长时间稳定运作 存储 8个 3 5英寸驱动 8块3 5存硬盘 2个N
  • C++ 11 新特性之统一初始化语法

    C 之前的初始化语法很乱 有四种初始化方式 而且每种之前甚至不能相互转换 让人有种剪不断 理还乱的感觉 曾经去面试 就有人问我string有几种初始化方式 当时就说出了两种 fuck 面试官还得意的说 你连基本的初始化方式都记不清 还做啥2
  • Qt 中 connect 函数实现信号与槽函数的连接

    目录 简介 connect 函数原型 代码示例 自定义信号和槽函数 信号和槽函数的线程安全性 总结 简介 Qt 是一个功能强大的跨平台应用程序开发框架 其提供了 connect 函数用于信号和槽的连接 实现了对象之间的通信 本文将介绍 co
  • RUST中所有权/生命周期/借用本质探讨

    本书github链接 深入RUST标准库内核 本文摘自以上链接的书籍 如果对本文中涉及的若干知识想进一步了解 请阅读此书 RUST在定义一个变量时 实际上把变量在逻辑上分成了两个部分 变量的内存块与变量内容 变量类型定义了内存块内容的格式
  • vue 源码解读

    前端基本 0基础 尝试从代码入手 不会的直接搜索就行了 成功添加页面 test33 1 在 src config menu js 源码24行增加 title Test33 name Test33 icon 2 在 src routers r
  • pycharm的git密码错误

    原文地址 https www cnblogs com wangjian941118 p 10721650 html pycharm使用gitlab输错密码解决办法 这个问题困扰我两周了 今天抱着试一试的想法 随手搜了一下 出现了新的结果 就
  • 去银行写代码是什么体验?

    最近在知乎上的一个回答火了 关于如何学习操作系统的 分享给大家 如何学会操作系统这门课程 一线互联网岗位和银行 国企还是有点区别的 这篇文章 讲详细讲一讲银行或者金融科技的相关问题 包括面试 待遇等等 虽然前阵子网传几大互联网公司都去掉了大
  • ChatGPT解决了我的出行规划焦虑

    我的五一出行规划 五一旅游季又将到来 许多人为了规划理想的行程而苦恼 需要投入相当时间来筛选各种信息 然而 现在有了Chat GPT 安排美好旅途变得异常简单 只要您告诉GPT您的日期和目的地 不到30秒就可以生成个性化的行程攻略 同时还可
  • APP兼容性测试如何测试?

    随着 APP 应用范围越来越广 用户群体越来越大 终端设备的型号也越来越多 移动终端碎片化加剧 使得 APP兼容性测试成为测试质量保障必须要考虑的环节 APP兼容性的测试主要包含系统兼容 产商ROM 兼容性 屏幕分辨率兼容 网络兼容 其他兼
  • PHP+nginx完成大文件下载处理

    最近在板子上做文件下载的处理 需求相对简单 一个下载请求过来 根据请求的数据决定给那些文件回去 于是采用了php nginx的方式来处理 尝试 nginx用来处理下载请求 拿到请求以后 调用配置好的php文件 php文件中对请求的参数做处理
  • actuator--基础--6.1--端点解析--health端点

    actuator 基础 6 1 端点解析 health端点 代码位置 https gitee com DanShenGuiZu learnDemo tree master actuator learn actuator01 1 health
  • java队列模拟_Java模拟队列

    用Java模拟队列的出队和进队 1 代码 Java 代码 package com stackANDqueue import java io DataInputStream import java io IOException 循环队列的入队
  • redis 十二. 分布式锁

    目录 一 分布式锁概述 二 redis 锁基础版示例 三 redis 锁进阶 Redlock 四 Redlock 分析 解决集群环境master宕机数据不一致锁不住的问题 锁的定时续期 watchdog源码分析 锁的可重入性分析 释放锁分析
  • ES6 for...in 和 for...of 的区别

    for in 和 for of的区别 遍历数组时 当给数组加上属性时 遍历对象时 总结 for of 是ES6新引入的特性 修复了 ES5 引入的 for in 的不足 关于两者之间的区别 下面列举了一些例子 遍历数组时 let arr 1
  • 5g手机怎么开5g网络

    确保您已经办理了5G套餐 且所处地区有5G网络信号 这样才能接收并使用到5G网络 以华为手机为例 打开 设置 移动网络 点击 启用5G 开关 状态栏上的信号图标就会出现一个5G图标 具体介绍如下 1 首先打开手机 设置 找到并点击 移动网络
  • 【状态估计】卡尔曼滤波器、扩展卡尔曼滤波器、双卡尔曼滤波器和平方根卡尔曼滤波器研究(Matlab代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 3 参考文献 4 Matlab代码实现 1 概述 本文包括 1 标准卡尔曼滤波器 2 扩展卡
  • QT打开文件并显示文件内容

    QT打开文件并显示文件内容 功能描述 当点击一个按钮的时候 实现打开指定类型的文件 并在另一个子窗口中显示文件的内容 核心函数分析 QString QFileDialog getOpenFileName QWidget parent Q N
  • pandas、numpy对txt、xls、csv的文件读取总结

    文件读取 1 csv文件读取 import pandas as pd df1 pd read csv r data HeightWeight csv print df1 import numpy as np data np loadtxt
  • 华为OD机试 - 新员工座位(Java)

    题目描述 工位由序列F1 F2 Fn组成 Fi值为0 1或2 其中0代表空置 1代表有人 2代表障碍物 1 某一空位的友好度为左右连续老员工数之和 2 为方便新员工学习求助 优先安排友好度高的空位 给出工位序列 求所有空位中友好度的最大值
  • 详解Spring的循环依赖问题、三级缓存解决方案源码分析

    0 基础 Bean的生命周期 在Spring中 由于IOC的控制反转 创建对象不再是简单的new出来 而是交给Spring去创建 会经历一系列Bean的生命周期才创建出相应的对象 而循环依赖问题也是由Bean的生命周期过程导致的问题 因此我