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代理等复杂场景的正确性。

相关推荐
失散132 小时前
分布式专题——15 ZooKeeper特性与节点数据类型详解
java·分布式·zookeeper·云原生·架构
菠菠萝宝2 小时前
【Java八股文】12-分布式面试篇
java·分布式·zookeeper·面试·seata·redisson
yk100102 小时前
Spring DefaultSingletonBeanRegistry
java·后端·spring
Metaphor6922 小时前
Java 将 PDF 转换为 HTML:高效解决方案与实践
java·经验分享·pdf·html
yujkss2 小时前
23种设计模式之【原型模式】-核心原理与 Java实践
java·设计模式·原型模式
boy快快长大2 小时前
【场景题】如何解决大文件上传问题
java
qianmo20212 小时前
gpt-4o+deepseek+R生成热力图表
java·数据库·r语言
三角叶蕨2 小时前
spring简单入门和项目创建
spring
失散132 小时前
分布式专题——20 Kafka快速入门
java·分布式·云原生·架构·kafka