Spring循环依赖:代理增强下的失效解析

作者:暴富20212026.11.07 02:15浏览量:0

简介:本文从一次真实的循环依赖异常出发,系统剖析Spring解决循环依赖的底层机制,重点解释为何普通循环依赖可以正常注入,而叠加@Async等代理增强后却抛出BeanCurrentlyInCreationException。读者将掌握三级缓存的工作原理、提前暴露原始对象与最终代理对象之间的矛盾,以及AOP场景下循环依赖失效的根本原因与规避策略。

Spring 循环依赖:代理增强下的失效解析

一、从一次异常说起

在一次常规的代码审查中,团队遇到了一种颇为典型却又容易被忽视的场景:两个服务类互相注入对方实例,形成闭环依赖。简化后的代码如下:

  1. @Service
  2. public class OrderService {
  3. @Autowired
  4. private PayService payService;
  5. @Async
  6. public void submitOrder() {
  7. // 异步处理下单逻辑
  8. }
  9. }
  10. @Service
  11. public class PayService {
  12. @Autowired
  13. private OrderService orderService;
  14. public void pay() {
  15. // 支付逻辑
  16. }
  17. }

启动时抛出异常:

  1. BeanCurrentlyInCreationException: Error creating bean with name 'orderService':
  2. Bean with name 'orderService' has been injected into other beans [payService]
  3. in its raw version as part of a circular reference, but has eventually been
  4. wrapped. This means that said other beans do not use the final version of the bean.

将 @Async 注解去掉后,同样的循环依赖结构却能正常启动。这一差异引出了本文要讨论的核心问题:Spring 号称能够解决循环依赖,为什么加了代理增强之后就不行了?

二、循环依赖的基本概念

循环依赖指的是一个或多个 Bean 之间形成直接或间接的引用闭环。例如 A 依赖 B,B 又依赖 A;或者 A→B→C→A 这样的长链路闭环。从依赖注入的时机来看,可以分为三类:

  • 构造器循环依赖:通过构造函数注入形成的闭环,Spring 无法解决,会直接抛出异常。
  • Setter/字段循环依赖:通过 setter 方法或字段注入形成的闭环,Spring 可以通过提前暴露引用解决。
  • 原型(prototype)作用域的循环依赖:Spring 不会缓存原型 Bean,无法解决,直接抛异常。

本文讨论的 @Autowired 字段注入属于第二类,也是 Spring 能够处理的那一类。

三、三级缓存的核心机制

Spring 解决单例循环依赖的关键,是 IoC 容器中的三级缓存结构:

缓存层级 名称 存储内容
一级 singletonObjects 完全初始化好的成品 Bean
二级 earlySingletonObjects 提前暴露的半成品 Bean(尚未填充属性、未执行初始化方法)
三级 singletonFactories 生成早期 Bean 引用的对象工厂(ObjectFactory)

创建 Bean 时的核心流程如下:

  1. 实例化(instantiate):通过反射创建对象实例,此时所有字段都是默认值。
  2. 属性填充(populateBean):注入依赖的 Bean。
  3. 初始化(initializeBean):执行 BeanPostProcessor、@PostConstruct、InitializingBean 等方法,并在此阶段生成 AOP 代理。
  4. 放入一级缓存。

当 A 创建到第 2 步发现需要 B,而 B 又需要 A 时,Spring 会:

  • 把 A 的对象工厂放入三级缓存;
  • 创建 B,B 从三级缓存拿到工厂,调用 getObject() 获取 A 的早期引用,放入二级缓存,完成 B 的创建;
  • 回到 A,继续属性填充和初始化,最终将完整 A 放入一级缓存。

这一机制的关键前提是:B 注入的“A”和最终放入一级缓存的“A”可以是同一个对象,也可以不是同一个对象——只要 B 不关心里面持有的是不是增强后的代理即可。

四、@Async 为什么破坏了这个前提

@Async 的底层实现依赖 Spring 的 AOP 代理机制。当 Bean 被 @Async 标记的方法所在类需要被代理时,Spring 会在 初始化后的 postProcessAfterInitialization 阶段 为其生成代理对象。

结合循环依赖场景,问题就显现了:

  1. A 实例化后,将原始对象的工厂放入三级缓存。
  2. B 创建时从三级缓存取到的是原始 A(未经过任何增强),并成功注入。
  3. A 继续走到初始化阶段,AsyncAnnotationBeanPostProcessor 发现 A 需要异步增强,于是生成代理 A,并将其放入一级缓存。
  4. 最终容器中,一级缓存是代理 A,而 B 中持有的却是原始 A。

Spring 在 DefaultSingletonBeanRegistry 与 AbstractAutowireCapableBeanFactory 的协作中,会对这种“早期引用被注入,但最终被包装成另一个对象”的情况进行检测,一旦发现不一致就抛出 BeanCurrentlyInCreationException,避免应用中出现“部分依赖拿到代理、部分依赖拿到原始对象”的隐性 bug。

五、解开矛盾的两条思路

思路一:提前暴露代理对象

如果 A 在放入三级缓存之前就完成了代理包装,B 拿到的就是代理对象,最终一致。但 AOP 通常发生在初始化之后,除非用 @Lazy 或自定义 SmartInstantiationAwareBeanPostProcessor 提前决定代理。这也是 Spring 提供 getEarlyBeanReference 这一扩展点的意义所在:

  1. public interface SmartInstantiationAwareBeanPostProcessor extends InstantiationAwareBeanPostProcessor {
  2. default Object getEarlyBeanReference(Object bean, String beanName) {
  3. return bean;
  4. }
  5. }

AbstractAutoProxyCreator 实现了这个方法,理论上可以在此返回代理对象。但注意:这种做法要求 AOP 在早期就具备足够信息(目标类、切点等都已确定),对于某些需要完整初始化后才能判定的增强(比如依赖方法返回值的场景)并不适用。

思路二:打破循环依赖

更稳妥的思路不是让 Spring 在代理和循环依赖之间做取舍,而是从设计上消除闭环:

  • 用 @Lazy 延迟注入:在注入点加上 @Lazy,Spring 会注入一个代理占位符,直到第一次真正调用时才触发目标 Bean 的创建,从而避免创建期的闭环。
  • 抽取第三方协调者:将 A 与 B 共用的逻辑抽到独立服务 C 中,A、B 都依赖 C,依赖图从环形变为有向无环。
  • 使用 ApplicationContext 或 ObjectProvider 延迟获取:避免在字段上直接声明对方类型,改为运行时按需获取。
  • 重构职责:循环依赖往往是职责划分不清晰的信号,从业务建模角度重新划分边界。

六、实践建议与常见误区

  • 不要依赖三级缓存兜底:它能解决的是“普通 setter/字段注入的单例循环依赖”,对于构造器注入、prototype 作用域、AOP 增强等场景都无能为力。
  • 优先使用构造器注入:构造器注入能暴露设计问题,让循环依赖在编译期或启动早期就被发现,而不是被缓存机制“掩盖”。
  • 警惕 @Async、@Transactional、@Cacheable 等代理增强注解:它们都会改变 Bean 的最终形态,与循环依赖叠加时最容易触发异常。
  • @Lazy 是缓解手段而非根治方案:它把依赖解析推迟到运行时,问题可能被隐藏得更深,最好配合日志和测试明确其行为。
  • 理解 BeanCurrentlyInCreationException 的语义:它并非简单的“循环依赖未解决”,而是在提示“早期引用与最终 Bean 不一致”,定位时要优先检查 AOP 相关注解。

七、总结

Spring 解决循环依赖的核心是“提前暴露半成品引用”,但这套机制成立的前提是——被提前暴露的对象与最终放入容器的对象在被依赖方看来必须一致。@Async 等注解会在初始化后生成代理,使得这一前提被打破,Spring 出于安全考虑主动抛出异常。

因此,真正专业的做法不是试图绕开异常,而是把循环依赖本身视为设计信号:能用构造器注入暴露问题就用构造器注入;必须保留循环时,用 @Lazy 或抽取协调者打破闭环;涉及 AOP 增强时,更要从创建时序角度审视依赖关系。理解三级缓存的边界,才能在实际工程中既享受框架带来的便利,又不被它的“智能兜底”所误导。