logo

探究Spring三级缓存机制:为何两级缓存无法解决循环依赖

作者:rousong2026.11.07 02:04浏览量:0

简介:深入剖析Spring框架处理循环依赖的核心逻辑,通过异常报错引出问题,详细解读为何三级缓存机制不可或缺,以及二级缓存为何无法应对代理对象的早期暴露。帮助开发者深度理解Bean生命周期与AOP代理的底层运作机制。

一、从循环依赖异常说起

在日常的企业级开发中,开发者经常会遇到各类 Bean 装配问题。当在项目中使用依赖注入时,如果两个或多个 Bean 之间相互持有对方的引用,就会构成循环依赖。多数情况下,框架能够自动解决单例模式下的 Setter 注入循环依赖,但在某些特定场景下,依然会抛出 BeanCurrentlyInCreationException 异常。

典型的报错信息如下:

  1. org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1': Bean with name 'testService1' has been injected into other beans [testService2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

很多开发者会对此感到困惑:既然现代框架已经设计了完善的依赖解决机制,为何还会出现此类问题?实际上,这个异常往往发生在 Bean 被提前暴露,但最终又被代理包装(Wrapped)的场景下。比如,当一个 Service 类的方法上添加了 @Async 异步注解时,该类会被 AOP 拦截并生成代理对象。如果在去掉该注解后,仅仅保留相互注入的逻辑,循环依赖能够被正常化解;而加上该注解后,却触发了上述异常。这背后的核心原因,正是框架的三级缓存设计在面对 AOP 代理时的特殊处理逻辑。

二、单例与循环依赖的解决思路

在探究缓存级数之前,有必要先理清框架创建单例 Bean 的基本流程。正常情况下,一个 Bean 的生命周期包含:实例化、属性填充、初始化。在属性填充阶段,框架会去获取该 Bean 所依赖的其他 Bean。

假设存在 TestService1 和 TestService2,它们互相注入对方。如果不使用任何缓存机制,执行流程将是:

  1. 创建 TestService1,发现需要 TestService2。
  2. 去创建 TestService2,发现需要 TestService1。
  3. 再次去创建 TestService1,从而陷入无限循环,导致栈溢出。

为了打破这个死循环,框架引入了“早期暴露”的机制。即在 Bean 实例化完成之后、属性填充之前,先将这个尚未完全初始化的“半成品”放入缓存中。当其他 Bean 试图获取它时,直接从缓存中返回这个半成品。

三、二级缓存为何显得足够?

如果仅仅是为了解决普通单例 Bean 的循环依赖,其实两级缓存就足够了。我们可以设计如下两个缓存:

  • 一级缓存:存放完全初始化好的成品 Bean。
  • 二级缓存:存放实例化完成但尚未注入属性的半成品 Bean。

处理逻辑如下:

  1. 从一级缓存获取 TestService1,没有;从二级缓存获取,也没有。
  2. 实例化 TestService1,将半成品放入二级缓存。
  3. 为 TestService1 注入属性,发现需要 TestService2。
  4. 从一级缓存获取 TestService2,没有;从二级缓存获取,也没有。
  5. 实例化 TestService2,将半成品放入二级缓存。
  6. 为 TestService2 注入属性,需要 TestService1。从二级缓存中获取到 TestService1 的半成品,注入给 TestService2。
  7. TestService2 属性填充完毕,执行初始化,移入一级缓存。
  8. 回到步骤3,TestService1 成功注入 TestService2,执行初始化,移入一级缓存。

通过上述步骤可以看出,如果没有 AOP 代理的介入,二级缓存完全能够闭环解决循环依赖问题。

四、AOP 代理带来的挑战

真正的复杂度来自于 AOP(面向切面编程)。在Spring生态中,事务管理、异步调用、缓存等特性都严重依赖 AOP 代理。当一个 Bean 需要被代理时,其他 Bean 注入的应该是它的“代理对象”,而不是原始对象。

那么问题来了:代理对象是在什么时候生成的?
按照正规的生命周期,代理对象的生成通常在“初始化之后”的 BeanPostProcessor 阶段。如果依然采用二级缓存方案,那么在上述步骤6中,TestService2 从二级缓存中拿到的是 TestService1 的“原始对象”。而在步骤8中,TestService1 在初始化完成后生成了“代理对象”。这就导致了 TestService2 内部持有的 TestService1 是原始对象,而容器中最终注册的却是代理对象。两者不一致,这就是报错信息中所说的:injected into other beans... in its raw version... but has eventually been wrapped。

五、三级缓存的破局之道

为了解决“代理对象延迟生成”与“循环依赖需要提前暴露”之间的矛盾,框架引入了第三级缓存。其核心设计如下:

  • 一级缓存:存放最终的成品 Bean(可能是代理对象)。
  • 二级缓存:存放早期暴露的 Bean(当 Bean 被提前引用且需要代理时,存放的是提前生成的代理对象)。
  • 三级缓存:存放的是一个 ObjectFactory(对象工厂)。它是一个 Lambda 表达式,在调用时才会真正决定是返回原始对象还是代理对象。

为什么三级缓存是不可或缺的?

1. 延迟代理生成

有了三级缓存,当发生循环依赖时,框架不会直接将半成品暴露出去,而是将一个 ObjectFactory 放入三级缓存。当另一个 Bean 真正需要引用它时,才会调用工厂方法。在调用工厂方法时,框架会判断该 Bean 是否需要被 AOP 代理。如果需要,则在此刻提前生成代理对象;如果不需要,则返回原始对象。

2. 保证对象单例性

如果在 ObjectFactory 调用时提前生成了代理对象,框架必须保证这个提前生成的代理对象最终就是容器中的那个单例。因此,当代理对象生成后,会将其从三级缓存移除,并放入二级缓存。这样,后续如果有其他 Bean(如 TestService3)也需要依赖 TestService1,直接从二级缓存中获取这个已经确认的代理对象即可,避免了重复创建代理。

3. 如果只用二级缓存行不行?

假设我们将三级缓存去掉,直接在二级缓存中存放 ObjectFactory 或者一开始就判断是否需要代理。

  • 如果一开始就生成代理对象并放入二级缓存,这就违背了 Spring AOP 的设计原则。Spring 原本希望在 Bean 的生命周期最后阶段统一进行 AOP 处理。提前生成代理会打乱生命周期顺序,导致一些初始化回调无法作用于代理对象本身。
  • 如果在二级缓存中存放工厂,虽然逻辑上可行,但会导致每次获取都需要判断是否需要执行工厂方法,无法有效地将“已经提前生成代理的半成品”与“完全成品的代理”区分开来,增加状态管理的复杂性。三级缓存的存在,实际上是将“创建对象的逻辑”与“缓存对象的状态”进行了解耦。

六、总结与最佳实践

三级缓存的设计是 Spring 为了兼顾“单例对象的早期暴露”与“AOP代理的合理生命周期”而做出的精妙架构决策。它通过引入 ObjectFactory 实现了代理对象的延迟生成,同时通过二级缓存保证了早期代理对象与最终单例的一致性。

尽管框架提供了如此强大的兜底机制,但在实际开发中,循环依赖本身往往意味着代码架构设计存在瑕疵(职责划分不清、模块过度耦合)。因此,最佳实践应当是:

  1. 尽量避免循环依赖的发生,通过重构提取中间层或使用事件驱动解耦。
  2. 对于必须存在的循环依赖(如某些特殊的领域模型双关联),明确使用 Setter 注入而非构造器注入。
  3. 谨慎使用 @Async 等会触发 AOP 代理的注解,特别是在存在循环依赖的组件中,可以考虑将异步方法抽取到独立的组件中,从根源上规避 BeanCurrentlyInCreationException 的发生。

发表评论

活动