Spring 的三级缓存,两级够吗

Spring 的三级缓存机制主要是为了解决 循环依赖 问题,同时保证 AOP 代理 的正确性。

结论是:两级缓存不够,必须用三级缓存


一、Spring 三级缓存分别是什么

  • 一级缓存singletonObjects(已经完成初始化的单例 Bean)
  • 二级缓存earlySingletonObjects(提前暴露的 Bean,尚未完全初始化,但已可被引用)
  • 三级缓存singletonFactories(对象工厂 ObjectFactory,用于生成 Bean 的早期引用,通常是为了生成 AOP 代理)

二、为什么两级不够

1. 如果没有 AOP

理论上,仅为了解决循环依赖,二级缓存是足够的:

  • A 创建时,实例化后放到二级缓存(或通过工厂暴露早期对象)
  • B 依赖 A,从缓存拿到未完全初始化的 A
  • B 完成初始化后,A 继续完成初始化

但问题出现在 A 需要被 AOP 代理 时。


2. 有 AOP 时的代理对象生成时机

Spring AOP 通常是在 Bean 初始化后postProcessAfterInitialization)通过后置处理器生成代理对象。

但在循环依赖中:

  • 如果 B 依赖 A,A 必须提前暴露给 B
  • 如果 A 最终要的是一个 代理对象,那么 B 依赖的也必须是同一个代理对象,而不是原始对象
  • 若只用二级缓存,在 A 实例化后直接暴露原始对象,之后 A 初始化完成时生成代理,B 里持有的还是原始对象,就会出现 对象不一致 的问题

三、三级缓存如何解决

一、正常 Bean 的生命周期(无循环依赖)

  1. 实例化createBeanInstance) → 原始对象
  2. 属性填充populateBean) → 依赖注入
  3. 初始化initializeBean):
    • 调用 applyBeanPostProcessorsBeforeInitialization
    • 调用 init-method
    • 调用 applyBeanPostProcessorsAfterInitialization
      这一步中,AOP 后置处理器 会检查 Bean 是否需要代理,如果需要则生成代理对象,并返回代理。
  4. 最终放入一级缓存singletonObjects

正常情况下,AOP 代理确实是在初始化完成后才创建的。


二、循环依赖时的特殊流程(A 需要代理,B 依赖 A)

假设 A 需要 AOP,B 依赖 A。

  1. A 实例化 :创建原始对象 A_raw
  2. 提前暴露addSingletonFactoryA_raw 包装成一个 ObjectFactory 放入三级缓存。
    这个工厂的 getObject 方法内部会调用 getEarlyBeanReference(beanName, mbd, A_raw)
  3. A 属性填充:发现需要 B → 去获取 B。
  4. B 开始创建 :实例化 B → 属性填充时发现需要 A → 从缓存获取 A。
    • 一级缓存无(A 未完成)
    • 二级缓存无
    • 三级缓存有工厂 → 调用工厂 ,即执行 getEarlyBeanReference
  5. getEarlyBeanReference 的作用
    它会遍历所有 SmartInstantiationAwareBeanPostProcessor(AOP 后置处理器实现了该接口),并调用其 getEarlyBeanReference 方法。
    此时,AOP 后置处理器发现 A_raw 需要被代理(比如有 @Transactional@Aspect),就直接生成代理对象 A_proxy 并返回
  6. 返回的代理对象
    • 存入二级缓存 earlySingletonObjects(key=beanName, value=代理对象)
    • 从三级缓存移除工厂
  7. B 获取到 A_proxy,完成 B 的创建。
  8. A 继续初始化
    A 在完成属性填充后,进入初始化阶段。但在 applyBeanPostProcessorsAfterInitialization 中,AOP 后置处理器会检查该 Bean 是否已经存在代理(通过一个缓存标记),发现代理已经提前生成,就不会再生成新的代理,直接返回现有的代理对象。
    最终 A 的代理对象被放入一级缓存。

为什么代理可以提前生成?

这是因为 SmartInstantiationAwareBeanPostProcessor 接口专门定义了一个 getEarlyBeanReference 方法,允许后置处理器在 Bean 实例化后、属性填充前 就提供一个对象引用(通常是代理)。

Spring AOP 的 AbstractAutoProxyCreator 实现了这个方法,它会在循环依赖发生时,提前创建代理,而不是等到初始化之后。

所以代理的创建时机有两个可能路径

  • 正常路径 :在 postProcessAfterInitialization 中创建。
  • 提前路径 :在 getEarlyBeanReference 中创建(仅当发生循环依赖且需要提前暴露时)。

这样设计就是为了保证:即使循环依赖,依赖方拿到的也是最终要用的代理对象,而不是原始对象。


四、两级缓存能否模拟三级

如果强行用两级缓存,有两个方案,但都有问题:

  1. 实例化后立刻生成代理

    会破坏 AOP 的正常顺序,有些后置处理可能依赖 Bean 初始化完成后的状态,提前代理会导致某些增强无法正确应用。

  2. 暴露时用原始对象,初始化后再替换

    需要额外维护引用关系并更新所有依赖方,复杂度极高且容易出错。

三级缓存本质是 将"提前暴露"与"生成最终对象"解耦 ,通过 ObjectFactory 延迟决定早期引用是原始对象还是代理对象,兼顾了循环依赖和 AOP。


=

相关推荐
爱喝一杯白开水2 小时前
Java 定时任务完全指南
java
毕设源码-郭学长2 小时前
【开题答辩全过程】以 高校自动排课系统的设计与实现为例,包含答辩的问题和答案
java
indexsunny2 小时前
互联网大厂Java面试实战:从Spring Boot到微服务架构的深度解析
java·spring boot·spring cloud·kafka·prometheus·security·microservices
SuniaWang2 小时前
《Spring AI + 大模型全栈实战》学习手册系列 ·专题三:《Embedding 模型选型指南:从 MMTEB 排名到实际应用》
人工智能·学习·spring
ChoSeitaku3 小时前
NO.2|proto3语法|消息类型|通讯录|文件读取|enum类型
java·服务器·前端
庞轩px3 小时前
MinorGC的完整流程与复制算法深度解析
java·jvm·算法·性能优化
zhouping@3 小时前
JAVA学习笔记day06
java·笔记·学习
haixingtianxinghai3 小时前
Redis真的是单线程吗?
数据库·redis·缓存
毕设源码-郭学长3 小时前
【开题答辩全过程】以 某某协会管理与展示平台为例,包含答辩的问题和答案
java