Spring的三级缓存如何解决单例Bean循环依赖

Spring的三级缓存是解决单例Bean循环依赖的核心机制。循环依赖指两个或多个Bean相互依赖(如A依赖B,B依赖A),若不处理会导致实例化过程陷入无限循环。三级缓存通过"提前暴露Bean的早期引用",让依赖在Bean完全初始化前即可被访问,从而打破循环。

一、三级缓存的定义与作用

Spring在DefaultSingletonBeanRegistry中定义了三级缓存,均为Map结构,存储单例Bean的不同状态:

缓存级别 变量名 类型 作用
一级缓存 singletonObjects Map<String, Object> 存储完全初始化完成的单例Bean(最终可用的Bean)。
二级缓存 earlySingletonObjects Map<String, Object> 存储提前暴露的未完全初始化的Bean(仅实例化未初始化,用于临时引用)。
三级缓存 singletonFactories Map<String, ObjectFactory<?>> 存储Bean的工厂对象,用于在需要时生成Bean的早期引用(支持AOP代理)。

二、循环依赖的解决过程(以A依赖B,B依赖A为例)

假设场景:AB都是单例Bean,A的字段依赖BB的字段依赖A(字段注入,非构造器注入)。流程如下:

步骤1:实例化A,暴露工厂到三级缓存
  1. 容器启动时,首先尝试获取AgetBean(A))。
  2. 发现A不在一级缓存中,进入创建流程:
    • 实例化A :调用A的构造器,生成A的原始对象(未设置属性,未执行初始化方法)。

    • 暴露工厂到三级缓存 :创建ObjectFactory(工厂对象),用于在需要时生成A的早期引用(若A需要AOP代理,工厂会生成代理对象;否则直接返回原始对象)。

      java 复制代码
      // 简化逻辑:添加到三级缓存
      singletonFactories.put("A", () -> getEarlyBeanReference("A", beanDefinition, rawA));
    • 此时A的状态:仅实例化,未初始化(属性未填充),存在于三级缓存的工厂中。

步骤2:A填充属性时依赖B,触发B的创建
  1. A进入"属性填充"阶段,发现依赖B,于是调用getBean(B)
  2. 容器尝试获取B,发现B不在一级缓存中,进入创建流程:
    • 实例化B :调用B的构造器,生成B的原始对象。
    • 暴露工厂到三级缓存 :同A,将B的工厂放入三级缓存。
    • 此时B的状态:仅实例化,未初始化,存在于三级缓存。
步骤3:B填充属性时依赖A,从三级缓存获取A的早期引用
  1. B进入"属性填充"阶段,发现依赖A,调用getBean(A)
  2. 容器查找A
    • 一级缓存(singletonObjects):无(A未完全初始化)。
    • 二级缓存(earlySingletonObjects):无。
    • 三级缓存(singletonFactories):存在A的工厂。
  3. 获取A的早期引用
    • 调用A的工厂对象,生成A的早期引用(若A需要代理,则此处生成代理对象;否则为原始对象)。
    • A的早期引用从三级缓存移到二级缓存(earlySingletonObjects.put("A", earlyA)),并删除三级缓存中的工厂(避免重复生成)。
  4. 注入A到B :将A的早期引用(earlyA)注入到B的属性中。
步骤4:B完成初始化,放入一级缓存
  1. B的属性填充完成,进入"初始化"阶段(执行@PostConstructafterPropertiesSet()等)。

  2. B完全初始化后,从二级缓存(若有)或直接放入一级缓存:

    java 复制代码
    singletonObjects.put("B", b); // B成为可用Bean
    earlySingletonObjects.remove("B"); // 清除二级缓存
    singletonFactories.remove("B"); // 清除三级缓存
步骤5:A获取B的引用,完成初始化
  1. A的属性填充阶段继续:此时B已在一级缓存中,直接从singletonObjects获取B,注入到A的属性中。

  2. A进入"初始化"阶段(执行初始化方法)。

  3. A完全初始化后,放入一级缓存:

    java 复制代码
    singletonObjects.put("A", a); // A成为可用Bean
    earlySingletonObjects.remove("A"); // 清除二级缓存

三、关键细节:为什么需要三级缓存?

核心原因是支持AOP代理

  • 若Bean需要被AOP代理(如被@Transactional注解),最终注入给依赖的应该是代理对象,而非原始对象。
  • 三级缓存的ObjectFactory的作用是:在需要时(如被其他Bean依赖时)才生成代理对象,而非在实例化后立即生成。
  • 若只有二级缓存(直接存储早期引用),无法动态生成代理对象(实例化时还不确定是否需要代理,代理逻辑可能依赖其他Bean)。

四、无法解决的循环依赖:构造器注入

三级缓存仅能解决字段注入setter注入 的循环依赖,无法解决构造器注入的循环依赖。原因是:

  • 构造器注入在实例化阶段 就需要依赖对象(调用构造器时必须传入参数),而三级缓存是在实例化后才暴露早期引用。
  • 例如:A的构造器依赖BB的构造器依赖A,此时AB在实例化时就需要对方,而缓存中尚无任何引用,导致循环依赖无法解决(抛出BeanCurrentlyInCreationException)。

总结

三级缓存通过"提前暴露Bean的工厂→按需生成早期引用→逐步完成初始化"的流程,打破了单例Bean的循环依赖。其中:

  • 三级缓存(singletonFactories)负责延迟生成早期引用(支持AOP代理)。
  • 二级缓存(earlySingletonObjects)负责临时存储早期引用,避免重复生成。
  • 一级缓存(singletonObjects)存储最终可用的完全初始化Bean。

这一机制既保证了循环依赖的解决,又兼顾了AOP代理等复杂场景的正确性。

相关推荐
都叫我大帅哥12 小时前
Java JJWT 完全指南:从入门到生产级实践
java
前端开发张小七13 小时前
Java 学习笔记 · 第一课:基础语法与核心概念
java·后端
ai_coder_ai14 小时前
分布式缓存架构设计与实践
分布式·缓存
ThsPool14 小时前
【ENVI二次开发学习整理 04】ENVI功能扩展与二次开发
java·前端·javascript
赤壁小虾14 小时前
【渲染流水线】[逐片元阶段]-[透明度测试]以UnityURP为例
java·前端·数据库
青山木16 小时前
Hot 100 --- 搜索插入位置
java·数据结构·算法·leetcode
Jul1en_16 小时前
【Claude Code Compact】源码级别的学习上下文压缩
java·前端·学习·github·ai编程
摇滚侠16 小时前
《SpringBoot 3:入门与应用实战》第 15 章 生产级特性 管理 Spring Boot 应用 阅读笔记
java·spring boot·笔记
XWalnut16 小时前
SpringBoot快速入门
java·spring boot·后端
前端 贾公子16 小时前
第06章:结构化输出 (上)
java·服务器·前端