深入浅出:Spring三级缓存如何巧妙化解Bean循环依赖?

一、 循环依赖的困境

设想两个Bean:BeanA 和 BeanB

  • BeanA 在其属性中需要注入 BeanB 的实例。

  • BeanB 在其属性中也需要注入 BeanA 的实例。

Spring容器在启动时,需要创建这些单例Bean。如果按照严格的顺序创建:

  1. 开始创建 BeanA -> 发现需要 BeanB -> 暂停 BeanA 的创建,转而去创建 BeanB

  2. 开始创建 BeanB -> 发现需要 BeanA -> 但此时 BeanA 尚未创建完成(卡在等待 BeanB 的阶段) -> 陷入死锁状态。

这就是经典的循环依赖(Circular Dependency)问题。Spring的三级缓存机制就是为了打破这种僵局。


二、 三级缓存:Spring的解决之道

Spring通过三个不同的缓存区域(通常命名为 singletonObjectsearlySingletonObjectssingletonFactories)协作解决循环依赖。它们的作用和生命周期如下:

  1. 一级缓存 (成品池 - singletonObjects):

    • 作用: 存放已经完全初始化完成的单例Bean对象。这是最终的、可以直接使用的Bean。

    • 生命周期: Bean完成所有步骤(实例化、属性填充、初始化)后放入此缓存。后续所有获取该Bean的请求都直接从这里返回。

  2. 二级缓存 (半成品池 - earlySingletonObjects):

    • 作用: 存放已被实例化但尚未完成属性填充和初始化的Bean的早期引用(Early Reference)。这是打破循环依赖的关键。

    • 生命周期: 当一个Bean被实例化(调用构造器 new 出来)后,Spring会立即将一个指向这个“半成品”对象的引用放入二级缓存(如果当前存在循环依赖的可能性)。当其他Bean在创建过程中需要依赖这个Bean时,可以从这里获取到它的早期引用进行注入。一旦该Bean最终完成初始化,它的引用会从二级缓存移入一级缓存。

  3. 三级缓存 (对象工厂池 - singletonFactories):

    • 作用: 存放用于创建Bean的对象工厂(ObjectFactory<?>)。这个工厂的核心方法是 getObject(),它有能力返回Bean的早期引用,并且在必要时提前生成代理对象

    • 生命周期: 在Bean被实例化后,Spring会立即将一个能生产该Bean(可能是原始对象,也可能是代理对象)的 ObjectFactory 放入三级缓存。当需要从二级缓存获取早期引用时,如果二级缓存没有,则会调用三级缓存中对应工厂的 getObject() 方法来获取(并可能同时将结果升级放入二级缓存)。当Bean最终放入一级缓存后,其对应的工厂会从三级缓存移除。


三、 核心流程解析:三级缓存如何协作

让我们结合 BeanA 和 BeanB 的循环依赖场景,一步步拆解三级缓存的协作过程:

  1. 开始创建 BeanA:

    • Spring 调用 BeanA 的构造器进行实例化,得到一个原始 beanA 对象(此时属性为 null)。

    • 关键动作1: 创建一个能生产 beanA 的 ObjectFactory,并将其放入三级缓存(singletonFactories)

    • 关键动作2: (注意:此时通常 不会 直接将对象引用放入二级缓存!这是与早期简化理解的重要区别)。

  2. 填充 BeanA 属性(发现依赖 BeanB):

    • Spring 准备为 beanA 注入属性。

    • 发现 beanA 依赖 BeanB

    • 于是 Spring 暂停 BeanA 的装配,转而去创建 BeanB

  3. 开始创建 BeanB:

    • 调用 BeanB 的构造器进行实例化,得到一个原始 beanB 对象。

    • 关键动作3: 创建一个能生产 beanB 的 ObjectFactory,放入三级缓存(singletonFactories)

  4. 填充 BeanB 属性(发现依赖 BeanA):

    • Spring 准备为 beanB 注入属性。

    • 发现 beanB 依赖 BeanA

    • Spring 开始查找 BeanA 的实例:

      • 一级缓存(singletonObjects):没有(BeanA 还没创建完)。

      • 二级缓存(earlySingletonObjects):没有(此时还未放入)。

      • 三级缓存(singletonFactories)找到 BeanA 的 ObjectFactory

    • 关键动作4: 调用 BeanA 的 ObjectFactory.getObject() 方法。

      • 如果 BeanA 不需要 AOP 代理:这个方法直接返回步骤1中创建的原始 beanA 引用。

      • 如果 BeanA 需要 AOP 代理:Spring 会在此时(提前)生成 beanA 的代理对象 proxyA(这是三级缓存的核心价值所在!)

      • 无论返回的是原始对象 beanA 还是代理对象 proxyA,Spring 都会将这个对象引用放入二级缓存(earlySingletonObjects),并从三级缓存移除该 ObjectFactory

    • Spring 将获取到的 BeanA 的引用(可能是 beanA 或 proxyA)注入到 beanB 的属性中。

    • BeanB 继续完成后续的属性注入(如果还有其他依赖)和初始化方法。

    • 关键动作5: BeanB 完全创建完成,将其最终对象放入一级缓存(singletonObjects),并从二级缓存三级缓存中移除其相关引用。

  5. 回到 BeanA 的创建:

    • 此时 BeanB 已创建完成并在一级缓存中。

    • Spring 从一级缓存(singletonObjects) 中获取到 BeanB 的完成品对象。

    • 将 BeanB 注入到 beanA 的属性中。

    • BeanA 继续完成后续的属性注入(如果还有其他依赖)和初始化方法。

    • 关键动作6: BeanA 完全创建完成。Spring 需要将其放入一级缓存:

      • 检查二级缓存(earlySingletonObjects):发现里面存放着之前通过 ObjectFactory.getObject() 得到的引用(可能是原始 beanA 或 proxyA)。

      • 一致性保证: 最终放入一级缓存的 BeanA 必须与 之前注入给 BeanB 的那个引用是同一个对象

      • 如果二级缓存中的是原始对象 beanA,且 BeanA 最终需要代理,Spring 会在此刻生成代理对象 proxyA,放入一级缓存。注意: 这会导致注入给 BeanB 的是原始对象 beanA,而最终容器管理的是 proxyA,两者不一致!这就是为什么需要三级缓存在步骤4提前处理代理。

      • 由于三级缓存在步骤4已经处理了代理(如果需要),此时二级缓存中的引用 proxyA 就是最终需要的代理对象。Spring 直接将其从二级缓存移入一级缓存。

    • 关键动作7: 将最终对象(proxyA 或原始 beanA)放入一级缓存(singletonObjects),并从二级缓存三级缓存中移除其相关引用。


四、 为什么是三级?二级缓存不行吗?

理解三级缓存价值的关键在于处理需要AOP代理的Bean的循环依赖

  • 二级缓存的缺陷(假设只有一级和二级缓存):

    1. 创建 BeanA 实例 -> 将原始 beanA 放入二级缓存

    2. 创建 BeanB 发现需要 BeanA -> 从二级缓存获取到原始 beanA -> 注入给 beanB

    3. BeanB 完成 -> 放入一级缓存。

    4. 继续完成 BeanA -> 此时发现 BeanA 需要AOP代理 -> 生成代理对象 proxyA -> 将 proxyA 放入一级缓存

    • 问题: BeanB 中持有的引用是原始 beanA,而Spring容器管理的最终对象是代理对象 proxyABeanB 通过 beanA 调用的方法不会被AOP切面增强!这破坏了AOP的功能和容器内对象的一致性。

  • 三级缓存如何解决(核心在于 ObjectFactory.getObject()):

    • 在步骤4(BeanB 需要注入 BeanA)时,不是直接从二级缓存拿对象,而是通过三级缓存的 ObjectFactory.getObject() 获取。

    • 这个工厂方法在被调用时(即第一次被其他Bean索取依赖时)会进行判断:

      • 如果该Bean不需要代理,返回原始对象引用(并放入二级缓存)。

      • 如果该Bean需要代理,立即生成代理对象并返回(同时放入二级缓存,替换掉等待生成代理的概念)。

    • 这样,无论 BeanA 最终是否需要代理:

      • 注入给 BeanB 的引用(通过工厂获取的)和最终放入一级缓存的引用(代理对象或原始对象)始终是同一个对象

      • 确保了AOP功能在存在循环依赖时也能正确生效。


五、 重要注意事项

  1. 只适用于单例(Singleton) Bean: 三级缓存机制是为解决单例Bean的循环依赖设计的。原型(Prototype) Bean的循环依赖Spring无法解决,会直接抛出 BeanCurrentlyInCreationException

  2. 只适用于Setter注入或字段注入: 该机制依赖于在属性注入阶段能够拿到早期引用。如果循环依赖是通过构造器注入(Constructor Injection) 发生的,在实例化 BeanA 时就需要 BeanB 的实例(此时 BeanB 甚至还没开始创建,更谈不上放入三级缓存),Spring将无法处理,同样会抛出异常。因此,推荐使用Setter或字段注入来避免构造器循环依赖问题。

  3. 并非所有场景都完美: 虽然三级缓存解决了大部分常见的循环依赖,但过于复杂的依赖关系或某些特定的Bean生命周期回调组合仍可能带来挑战。良好的设计应尽量避免循环依赖


六、 总结

Spring的三级缓存机制是其IoC容器设计中一个精妙且核心的部分。它通过:

  1. 提前暴露引用(二级缓存) 打破循环依赖的僵局。

  2. 使用对象工厂(三级缓存) 延迟决策并确保在需要时提前、正确地生成代理对象。

  3. 最终一致性(一级缓存) 保证容器内管理的是完全初始化的Bean。

三者协同工作,高效且优雅地解决了单例Bean通过Setter/Field注入方式产生的循环依赖问题,即使涉及复杂的AOP代理也能保持对象引用的一致性和功能的正确性。理解这一机制,有助于我们更深入地掌握Spring容器的运作原理,编写更健壮的应用程序,并在遇到相关问题时能够快速定位和解决。

思考题:

  1. 如果 BeanA 和 BeanB 都采用构造器注入依赖对方,Spring还能解决吗?为什么?

  2. 在三级缓存的工作流程中,哪个步骤是保证AOP代理对象一致性的最核心环节?

欢迎在评论区交流讨论!

📌 关注我,每天带你掌握底层原理,写出更强健的 Java 代码!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yiridancan

你的鼓励师我创造最大的动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值