当前位置: 首页 > news >正文

Spring循环依赖以及三个级别缓存

Spring循环依赖以及三个级别缓存

什么是循环依赖?

循环依赖,顾名思义,就是指两个或多个 Spring Bean 之间相互依赖,形成一个闭环。

image-20250725163349432

最常见也是 Spring 能够“解决”的循环依赖是构造器注入setter 注入 混合或单独使用时,发生在 单例(Singleton) Bean 上的情况。


什么是三级缓存?

image-20250725165502311

第一级缓存:singletonObjects (一级缓存 / 成品 Bean 缓存)

  • 类型: ConcurrentHashMap<String, Object>

  • 作用: 存放已经完全初始化好,并且可供使用的单例 Bean。当一个 Bean 在这里被找到时,它就是“成品”了,可以直接返回给请求者。

第二级缓存:earlySingletonObjects (二级缓存 / 早期暴露对象缓存)

  • 类型: ConcurrentHashMap<String, Object>

  • 作用: 存放已经实例化但尚未完成属性填充和初始化的 Bean。这些 Bean 是“半成品”,但它们被提前暴露出来,以便解决循环依赖。

第三级缓存:singletonFactories (三级缓存 / 早期单例工厂缓存)

  • 类型: HashMap<String, ObjectFactory<?>>
  • 作用: 存放创建 Bean 的 ObjectFactory。这个工厂负责生产“半成品”的 Bean 实例(可能经过 AOP 代理)。它是解决循环依赖的关键所在,尤其是在涉及到 AOP 代理的场景。
  • 特点: 这里的不是 Bean 实例本身,而是一个能够获取早期 Bean 引用(可能是原始对象,也可能是代理对象)的工厂

我们可以看到三级缓存singletonFactories的类型是HashMap,并且map的value值为ObjectFactory,不同于其他两级缓存。

💠为什么 valueObjectFactory 而不是 Object

  • 这就是第三级缓存的精妙之处,也是它能够解决循环依赖与 AOP 代理同时存在问题的关键:

    1. 延迟生成早期引用:
      • 在 Bean A 实例化后,它会立即将一个 ObjectFactory 放入第三级缓存。
      • 这个工厂只有在另一个 Bean B 发生循环依赖,并且需要提前获取 Bean A 的引用时,才会被调用(singletonFactory.getObject())。
      • 这种延迟机制使得 Spring 可以在真正需要 Bean A 的早期引用时,才决定并生成它。
    2. 处理 AOP 代理:
      • 如果 Bean A 需要进行 AOP 代理(例如,因为它上面有 @Transactional 注解,或者被某个切面匹配到),那么在 ObjectFactorygetObject() 方法被调用时,Spring 的 AOP 逻辑会被触发。
      • 此时,getObject() 方法将不会简单地返回 Bean A 的原始实例,而是会返回 Bean A 的代理实例
      • 这个代理实例随后会被放入第二级缓存 earlySingletonObjects,供依赖方使用。

    总结来说:

    • 一级和二级缓存直接存放Bean 实例(成品或半成品)。
    • 第三级缓存存放的是一个**“生产 Bean 实例的工厂”**。这个工厂在被调用时,能根据 Bean 的特性(特别是是否需要 AOP 代理),决定是返回原始实例还是其代理实例。

    正是因为 singletonFactories 存储的是一个能够“生产”早期 Bean 实例的工厂,而不是直接的 Bean 实例,Spring 才能够在循环依赖的场景下,灵活地提供经过 AOP 代理的早期 Bean 引用,从而保证了 Bean 引用的一致性,解决了复杂场景下的循环依赖问题。

💠为什么 singletonFactories 使用 HashMap?

  • singletonObjectsearlySingletonObjects 需要 ConcurrentHashMap 是因为它们是并发访问的热点,需要内部的并发控制来保证性能和线程安全。

  • singletonFactories 使用 HashMap 是因为它的所有相关操作都已经被外部的 synchronized (this.singletonObjects) 锁保护起来,本身不需要内部的并发机制。 在这个全局锁的保护下,使用 HashMap 既满足了线程安全,又因为其操作频率相对较低而没有性能瓶颈。


循环依赖流程?

image-20250725173258369

这五个是单例 Bean 的创建和缓存紧密相关的核心环节,清晰描述了 Spring 单例 Bean 的生命周期

  1. 缓存查询

​ 这是获取 Bean 的第一步,也是最快、最高效的方式。

  • 目标:检查 Spring IoC 容器中是否已经存在一个完全初始化好的 Bean 实例。

  • 实现:Spring 会首先去它的**一级缓存(singletonObjects)**中查找。

  • 结果

    • 如果找到了,直接返回这个 Bean 实例。这是最理想的情况,省去了后续所有创建和初始化步骤。

    • 如果没找到,并且这个 Bean 当前正在创建中(表示可能存在循环依赖),它会进一步尝试从**二级缓存(earlySingletonObjects三级缓存(singletonFactories)**获取一个早期引用。

  1. 创建对象 (实例化)

​ 如果缓存中没有找到可用的 Bean,那么 Spring 就会开始创建新的 Bean 实例。

  • 目标:根据 Bean 定义(BeanDefinition),通过反射等方式,生成 Bean 的原始实例。
  • 实现
    • 对于普通 Bean,通常是调用其构造函数来创建对象。
    • 在实例化完成后,Bean 的原始实例就存在了,但此时它只是一个“空壳”,没有任何属性被填充,也没有进行任何初始化操作。
  • 关键时机:这个阶段是 Bean 生命周期中首次出现具体对象的地方。也是在这个阶段之后,如果存在循环依赖且允许早期引用,Spring 会将 Bean 的一个早期引用工厂ObjectFactory)放入三级缓存
  1. 填充属性

​ 对象创建后,它需要被注入所依赖的其他 Bean 和配置。

  • 目标:将 Bean 定义中声明的属性(通过 @Autowired@Resource 等注解或 XML 配置)注入到刚创建的 Bean 实例中。
  • 实现
    • Spring IoC 容器会解析 Bean 的依赖关系。
    • 如果是通过 Setter 方法注入字段注入,Spring 会查找对应的依赖 Bean。
    • 如果在这个过程中遇到循环依赖(例如,BeanA 依赖 BeanB,而 BeanB 此时需要 BeanA),Spring 会利用之前放入三级缓存中的 ObjectFactory 来获取 BeanA 的早期引用(可能是代理对象),从而打破循环。
  • 关键时机:循环依赖问题主要发生在这个阶段。
  1. 初始化

​ 属性填充完成后,Bean 实例就具备了它所有的依赖,但可能还需要进行一些自定义的初始化工作。

  • 目标:执行 Bean 的自定义初始化逻辑和生命周期回调。

  • 实现

    • Aware 接口回调:如果 Bean 实现了 BeanNameAwareBeanFactoryAwareApplicationContextAware 等接口,Spring 会调用相应的方法注入名称、工厂或上下文。
    • BeanPostProcessor 前置处理:调用所有注册的 BeanPostProcessorpostProcessBeforeInitialization 方法。
    • @PostConstruct 方法:执行 Bean 中被 @PostConstruct 注解标记的方法。
    • InitializingBean 接口 afterPropertiesSet() 方法:如果 Bean 实现了 InitializingBean 接口,调用其 afterPropertiesSet() 方法。
    • 自定义 init-method:执行 Bean 定义中指定的自定义初始化方法(如 XML 配置中的 init-method)。
    • BeanPostProcessor 后置处理:调用所有注册的 BeanPostProcessorpostProcessAfterInitialization 方法。AOP 代理通常在这个阶段发生postProcessAfterInitialization 方法会返回 Bean 的代理对象。
  • 关键时机:Bean 的最终形态(包含所有代理)通常在这个阶段确定。

  1. 缓存转移 (放入一级缓存)

​ 所有步骤都完成后,这个 Bean 就“大功告成”了。

  • 目标:将完全初始化并可用的 Bean 实例,从所有临时缓存中移除,并放入最终的成品缓存。
  • 实现
    • Spring 会将这个 Bean 实例放入一级缓存(singletonObjects
    • 同时,从**二级缓存(earlySingletonObjects三级缓存(singletonFactories)**中移除该 Bean 的相关条目,因为它现在已经是“成品”了,不再是早期引用或工厂。
  • 结果:此后,任何对该 Bean 的请求都将直接从一级缓存中获取,高效且快速。

来看BeanA和BeanB这个循环依赖流程,在三级缓存中是怎么作用的?

流程参考视频[彻底拿捏Spring循环依赖以及三个级别缓存哔哩哔哩bilibili]

首先来看BeanA的创建过程

1.缓存查询

查询三级缓存,都没有,回到主流程,进行创建对象。

image-20250725211931540

2.创建对象

我们通过反射创建对象,包装成ObjectFactory类型,放到三级缓存singletonFactories中,再回到主流程,进行填充属性。

image-20250725212203302

✨3.填充属性

因为填充的是BeanB,所以要开始创建BeanB,同样走的是BeanA走的那一套流程 。

​ 🎯3.1 进行缓存查询,查询不到,进行下一步创建对象。

image-20250725211931540

​ 🎯3.2 进行通过反射创建对象,此时三级缓存singletonFactories中已经有BeanA了,将BeanB放进去

image-20250725212940589

​ 🎯3.3填充属性,要填充BeanA,又要开始创建BeanA的那一套流程。

​ 3.3.1 进行缓存查询,此时三级缓存中是有的,执行一些额外操作

image-20250725213345428

​ 来看源码,重点看2.3-2.6 ,通过getObject获得对象,放到二级缓存中,并在三级缓存中移除它。

​ 这样BeanB的填充属性就完成了。回到BeanB的主流程中,进行初始化

protected Object getSingleton(String beanName, boolean allowEarlyReference) {// 1. 首先尝试从一级缓存中获取成品 BeanObject singletonObject = this.singletonObjects.get(beanName);// 2. 如果一级缓存中没有,并且该 Bean 正在创建中 (解决循环依赖的关键入口)if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {// 2.1. 尝试从二级缓存中获取早期暴露的 BeansingletonObject = this.earlySingletonObjects.get(beanName);// 2.2. 如果二级缓存中也没有,并且允许早期引用 (allowEarlyReference通常为true,表示允许循环依赖)if (singletonObject == null && allowEarlyReference) {// 对 singletonObjects 进行同步锁,保证线程安全,防止多个线程同时处理同一个Bean的早期引用synchronized(this.singletonObjects) {// 再次检查一级缓存,因为在同步块外可能被其他线程放入singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null) { // 再次确认一级缓存没有// 再次检查二级缓存,确保同步块内没有被其他线程放入singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null) { // 再次确认二级缓存没有// 2.3. 从三级缓存中获取 ObjectFactoryObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);if (singletonFactory != null) {// 2.4. 调用 ObjectFactory 的 getObject() 方法获取早期 Bean 实例//      这可能是原始对象,也可能是代理对象(如果AOP触发)singletonObject = singletonFactory.getObject();// 2.5. 将获取到的早期 Bean 实例放入二级缓存this.earlySingletonObjects.put(beanName, singletonObject);// 2.6. 从三级缓存中移除对应的 ObjectFactory,因为已经使用了this.singletonFactories.remove(beanName);}}}}}}return singletonObject;
}

​ 🎯3.4 初始化,执行 Bean 的自定义初始化逻辑和生命周期回调。

​ 🎯3.5 缓存转移,因为BeanB已经完全创建完毕了,所有要将缓存里面的对象进行转移

image-20250725214714531

​ 看源码,将BeanB放入一级缓存中,从三级缓存中移除,从二级缓存中移除。

protected void addSingleton(String beanName, Object singletonObject) {synchronized(this.singletonObjects) {this.singletonObjects.put(beanName, singletonObject); // 放入一级缓存this.singletonFactories.remove(beanName); // 从三级缓存移除,因为已经不是早期引用了this.earlySingletonObjects.remove(beanName); // 从二级缓存移除,因为已经不是早期引用了this.registeredSingletons.add(beanName); // 记录为已注册的单例}
}
image-20250725215830859

​ 到这里整个BeanB的创建过程就完成了,但这只是BeanA填充属性,我们还要跳回BeanA的创建过程。

image-20250725220101752

✨4.初始化

✨5.缓存转移

同上类似,移除二级和三级缓存中的BeanA,添加一级缓存中

image-20250725220224994 image-20250725220414321

为什么需要第三级缓存,有两个不就行了

简单来说,第三级缓存是为了解决当 Bean 存在循环依赖,并且还需要进行 AOP 代理(或者其他后置处理器处理)时的问题。

如果没有第三级缓存,仅靠两级缓存,Spring 无法在需要 AOP 代理时正确处理循环依赖。

为什么两级缓存不够?

我们来想象一下只有两级缓存(singletonObjectsearlySingletonObjects)的场景:

假设有两个 Bean:BeanABeanB

  • BeanA 依赖 BeanB
  • BeanB 依赖 BeanA
  • 最重要的是:BeanA 需要被 AOP 代理(例如,它上面有 @Transactional 注解,或者自定义的切面)。

流程推演(只有两级缓存):

  1. 创建 BeanA
    • Spring 开始创建 BeanA
    • 实例化 BeanA 的原始对象 a
    • a 放入二级缓存 earlySingletonObjects
  2. BeanA 填充属性,需要 BeanB
    • BeanA 的属性填充过程中,发现需要 BeanB
    • Spring 暂停 BeanA 的创建,转而创建 BeanB
  3. 创建 BeanB
    • Spring 开始创建 BeanB
    • 实例化 BeanB 的原始对象 b
    • b 放入二级缓存 earlySingletonObjects
  4. BeanB 填充属性,需要 BeanA
    • BeanB 的属性填充过程中,发现需要 BeanA
    • Spring 到缓存中查找 BeanA
      • 在一级缓存 singletonObjects 中找不到 BeanA(因为它还没初始化完)。
      • 二级缓存 earlySingletonObjects 中找到了 BeanA 的原始对象 a
      • a 注射到 BeanB 中。
  5. BeanB 完成初始化:
    • BeanB 完成属性填充和所有初始化步骤。
    • BeanB 被放入一级缓存 singletonObjects,完成创建。
  6. BeanA 继续完成初始化:
    • BeanA 现在得到了完整的 BeanB
    • BeanA 继续进行初始化步骤,包括执行 AOP 代理逻辑
    • 此时,问题来了:当 BeanA 执行 AOP 代理时,它会生成一个代理对象 a_proxy。这个 a_proxy 才是最终应该被其他 Bean 引用和使用的对象。
    • 但问题是,BeanB 在第4步中已经获取并使用了 BeanA原始对象 a,而不是 a_proxy

结论: 在这种情况下,BeanB 引用的是未被 AOP 代理的 BeanA 原始对象,而其他后来的 Bean 引用的是 BeanA 的代理对象。这就导致了 Bean 实例的不一致性,后续对 BeanA 的方法调用可能无法触发 AOP 逻辑,从而导致功能异常。

第三级缓存 singletonFactories 的作用

第三级缓存 singletonFactories 存放的不是 Bean 实例,而是一个 ObjectFactory(一个工厂)。这个工厂的 getObject() 方法,会在需要时,动态地生成并返回 Bean 的早期引用

关键在于:这个工厂在生成早期引用时,会判断当前 Bean 是否需要进行 AOP 代理。如果需要,它会直接返回代理对象;如果不需要,它就返回原始对象。

引入第三级缓存后的流程推演:

  1. 创建 BeanA
    • Spring 开始创建 BeanA
    • 实例化 BeanA 的原始对象 a
    • Spring 将一个 ObjectFactory 放入第三级缓存 singletonFactories 这个工厂在被调用时,会负责生成 BeanA 的早期引用(可能是原始对象或代理对象)。
    • 同时,将 BeanAsingletonFactories 中移除,将原始对象 a 放入 earlySingletonObjects (二级缓存)
  2. BeanA 填充属性,需要 BeanB
    • BeanA 的属性填充过程中,发现需要 BeanB
    • Spring 暂停 BeanA 的创建,转而创建 BeanB
  3. 创建 BeanB(步骤同前,不再赘述)。
  4. BeanB 填充属性,需要 BeanA
    • BeanB 的属性填充过程中,发现需要 BeanA
    • Spring 到缓存中查找 BeanA
      • 在一级缓存 singletonObjects 中找不到。
      • 在二级缓存 earlySingletonObjects 中找到了 BeanA 的原始对象 a
      • 如果此时 BeanA 需要被 AOP 代理,并且 AOP 后置处理器已经准备好,那么 earlySingletonObjects 中的 a 将不会直接返回给 BeanB
      • 而是会通过 singletonFactories 中对应的 ObjectFactory 来获取 BeanA 的早期引用。 这个工厂被调用时,会触发 BeanA 的 AOP 代理逻辑,生成 a_proxy
      • a_proxy 会被放入二级缓存 earlySingletonObjects,替换掉原始对象 a(重要:这里原始对象被替换为代理对象)
      • 然后,a_proxy 被注射到 BeanB 中。
  5. BeanB 完成初始化:
    • BeanB 完成属性填充和所有初始化步骤,并被放入一级缓存 singletonObjects
  6. BeanA 继续完成初始化:
    • BeanA 现在得到了完整的 BeanB
    • BeanA 继续进行剩余的初始化步骤。
    • BeanA 的最终代理对象(如果之前生成过,就是那个 a_proxy;如果没有,则在此处生成并最终确定)被放入一级缓存 singletonObjects

核心优势:

通过三级缓存,当一个 Bean(比如 BeanA)在创建过程中被另一个 Bean(比如 BeanB)提前引用时,并且这个 BeanA 需要被 AOP 代理,三级缓存中的 ObjectFactory 能够保证所有获取到的早期引用都是经过 AOP 代理后的对象。这样就确保了 Bean 实例的一致性,无论是在循环依赖中被提前引用,还是在后续正常流程中被引用,都将得到的是同一个 AOP 代理后的实例。

总结: 两级缓存无法处理 AOP 代理的场景,因为在 Bean 完全初始化前无法确定最终是返回原始对象还是代理对象。第三级缓存通过存储一个工厂(ObjectFactory),使得 Spring 能够在需要时才决定并生成最终的早期引用(可以是原始对象,也可以是代理对象),从而保证了在循环依赖和 AOP 代理同时存在时的正确性和一致性。

http://www.dtcms.com/a/299614.html

相关文章:

  • Zama+OpenZeppelin:将机密智能合约带入 DeFi 和数字资产领域
  • ClickHouse高性能实时分析数据库-高性能的模式设计
  • JavaScript中.splice()的用法
  • Vue 插槽
  • 数据结构自学Day14 -- 利用归并排序思想实现“外排序”
  • 【MySQL 数据库】MySQL基本查询(第二节)
  • 达梦[-2894]:间隔表达式与分区列类型不匹配
  • 「iOS」————继承链与对象的结构
  • 全球节点的概念
  • 原理篇..
  • mysql的lts版本与Innovation版本区别
  • 考研复习-数据结构-第八章-排序
  • 【工具类】Linux 环境利用 uv 安装多版本 python
  • AI 编程还有多远?我们如何迎接 AI 编程时代?
  • MGRE综合实验
  • 大模型开发工具的汇总
  • 小架构step系列26:Spring提供的validator
  • 秋招Day19 - 分布式 - 分布式事务
  • Android 修改系统时间源码阅读
  • DeepSeek算法学习笔记
  • RabbitMQ--Springboot解决消息丢失
  • Spring Boot集成RabbitMQ终极指南:从配置到高级消息处理
  • Linux进程控制:掌握系统的核心脉络
  • Git版本控制
  • FC(Function Calling)和MCP详解(区别、作用、运用场景)
  • 2.JVM跨平台原理(字节码机制)
  • 【Bluedroid】btif_av_sink_execute_service之服务器禁用源码流程解析
  • 零基础学后端-PHP语言(第二期-PHP基础语法)(通过php内置服务器运行php文件)
  • 机器语言基本概念
  • android开发中的协程和RxJava对比