Spring的三级缓存机制是什么

Spring的三级缓存机制,是Spring框架为了解决在单例(Singleton)模式下Bean之间“循环依赖”问题而设计的一套解决方案。

循环依赖,简单来说,就是两个或多个Bean之间相互持有对方的引用,形成了一个闭环。例如,A对象依赖B对象,同时B对象也依赖A对象。在没有特殊处理的情况下,Spring在创建A对象的过程中发现它需要B对象,于是去创建B对象;在创建B对象的过程中又发现它需要A对象,此时A对象还在创建过程中,尚未完成,这就陷入了一个死循环,导致程序无法正常启动。

Spring的三级缓存正是为了打破这个循环,使得在特定条件下(单例、属性注入)的循环依赖问题能够被优雅地解决。需要强调的是,它无法解决构造器注入的循环依赖,也无法解决多例(Prototype)模式下的循环依赖。

三级缓存的构成

Spring的三级缓存由三个Map对象组成,它们在DefaultSingletonBeanRegistry类中定义:

  1. 第一级缓存: singletonObjects

    • 类型: Map<String, Object>

    • 作用: 这是一个完整的单例池,用于存放已经经历了完整生命周期的Bean实例(即已经实例化、属性填充、初始化都已完成)。我们平时通过getBean()方法获取到的对象,绝大多数都是从这个缓存中直接返回的。它也被称为“单例池”或“成品缓存”。

  2. 第二级缓存: earlySingletonObjects

    • 类型: Map<String, Object>

    • 作用: 用于存放“半成品”的Bean实例。这里的“半成品”指的是已经完成了实例化(即通过构造函数创建了对象),但尚未进行属性填充和初始化的Bean。将这个阶段的对象提前暴露出来,是为了解决循环依赖。它也被称为“半成品缓存”。

  3. 第三级缓存: singletonFactories

    • 类型: Map<String, ObjectFactory<?>>

    • 作用: 这是一个“单例工厂”缓存。它存放的不是Bean的实例,而是一个能够创建Bean实例的工厂对象(ObjectFactory)。当一个Bean被创建且支持循环依赖时,Spring会提前生成一个该Bean的ObjectFactory并放入三级缓存。这个工厂的主要作用是,当其他Bean需要依赖这个尚未完全创建的Bean时,通过调用这个工厂的getObject()方法来获取一个“半成品”的Bean实例。这个过程是获取早期引用的关键,特别是当Bean需要被AOP代理时,工厂会返回代理对象而不是原始对象。

三级缓存解决循环依赖的流程

我们以一个最简单的例子来说明:Bean A依赖Bean B,同时Bean B也依赖Bean A。

  1. 创建A:

    • getBean("a")被调用,Spring开始创建Bean A。

    • 首先检查一级缓存singletonObjects,没有A。检查二级缓存earlySingletonObjects,也没有A。

    • Spring准备实例化A。在实例化之前,它会记录"a"正在创建中。

    • A通过构造函数被实例化,此时得到了一个A的原始对象(但属性b还是null)。

    • 关键一步:Spring并不会立即去填充属性,而是为A创建一个ObjectFactory,并将其存入三级缓存singletonFactories中。singletonFactories.put("a", () -> getEarlyBeanReference("a", mbd, A))。这个工厂的getObject方法在被调用时,可能会返回A的原始对象,或者如果A需要被代理,则返回A的代理对象。

    • 接着,Spring开始为A的实例进行属性填充。它发现A依赖于属性b。

  2. 创建B:

    • Spring尝试去获取Bean B,调用getBean("b")

    • 同样,检查一、二级缓存,都找不到B。

    • B通过构造函数被实例化,得到一个B的原始对象(但属性a还是null)。

    • 和A一样,Spring为B也创建一个ObjectFactory并放入三级缓存singletonFactories

    • Spring开始为B的实例进行属性填充,发现B依赖于属性a。

  3. 解决依赖:

    • Spring尝试获取Bean A,调用getBean("a")

    • 此时,Spring发现"a"正在创建中。

    • 它会先去一级缓存找,没有。

    • 然后去二级缓存找,依然没有。

    • 最后去三级缓存singletonFactories查找。成功找到了创建A的ObjectFactory

    • Spring调用这个工厂的getObject()方法。这个方法会执行,可能返回A的原始对象,或者如果需要AOP,则返回A的代理对象。我们称这个返回的对象为A的“早期引用”或“半成品”。

    • 获取到A的早期引用后,Spring会做两件事:

      • 将这个早期引用放入二级缓存earlySingletonObjects中。

      • 从三级缓存singletonFactories中移除A的工厂。

    • 现在,B的属性a成功被注入了A的早期引用。

    • B的后续生命周期(初始化等)继续进行,直到完成。

    • B作为一个完整的Bean,被放入一级缓存singletonObjects中。

  4. 完成A的创建:

    • Spring回到创建A的流程,此时已经成功获取到了B的实例。

    • A的属性b被成功注入了B的实例。

    • A的后续生命周期继续进行,直到完成。

    • A作为一个完整的Bean,被放入一级缓存singletonObjects中。同时,如果二级缓存中有A,会被清除。

至此,A和B的循环依赖被成功解决,两者都成为了功能完整的Bean,并被存放在一级缓存中。

总结一下,三级缓存的核心思想是“提前暴露”。当一个单例Bean被创建但还未完成初始化时,就提前通过一个ObjectFactory将它暴露出去,这样其他依赖它的Bean就能提前拿到它的引用(可能是原始对象或代理对象),从而打破循环等待的僵局。

关于这个机制,我还可以做一些延伸思考:

  1. 为什么必须是三级缓存,二级缓存可以吗?

    这是一个经典问题。如果仅仅是为了解决循环依赖,二级缓存(存放半成品对象)确实够了。但Spring的设计需要考虑AOP。如果一个对象需要被代理,那么注入给其他对象的应该是它的代理对象,而不是原始对象。如果只用二级缓存,Bean在实例化后就直接放入二级缓存,此时放入的是原始对象。当后续AOP生效时,创建了代理对象,但之前被其他Bean依赖注入的那个原始对象已经无法被替换了,这就会导致问题。

    三级缓存通过ObjectFactory延迟了这一过程。只有当这个Bean真的被其他Bean依赖时,才会通过ObjectFactory.getObject()去获取。而这个方法内部封装了AOP的逻辑,可以判断是否需要生成代理对象。这样就保证了注入给其他Bean的引用一定是最终正确的引用(代理对象或原始对象),而不是一个“错误”的原始对象。所以说,第三级缓存(singletonFactories)是确保在循环依赖场景下AOP能够正确应用的关键。

  2. 构造器注入为什么无法解决?

    因为构造器注入要求在对象实例化阶段就必须提供所有依赖的参数。当A的构造器需要B,Spring去创建B,而B的构造器又需要A时,A的对象实例本身都无法通过new关键字创建出来,更谈不上后续的缓存和暴露了。这是一个“先有鸡还是先有蛋”的问题,Spring无法解决。