logo

Spring循环依赖:@Async为何引发注入异常

作者:carzy2026.11.07 00:10浏览量:0

简介:本文从一次真实启动失败切入,系统剖析Spring三级缓存解决循环依赖的机制,重点解释为何在循环依赖链上叠加@Async等代理增强后,原始实例与最终代理实例不一致会触发BeanCurrentlyInCreationException,并给出可落地的排查思路与规避方案,帮助读者彻底理解代理时机与缓存暴露的边界。

Spring 循环依赖:@Async 为何引发注入异常

一、从一个启动失败说起

在一次常规的项目启动中,两个互相注入的 Service 构成了典型的循环依赖:

  1. @Service
  2. public class TestService1 {
  3. @Autowired
  4. private TestService2 testService2;
  5. @Async
  6. public void test1() {
  7. }
  8. }
  9. @Service
  10. public class TestService2 {
  11. @Autowired
  12. private TestService1 testService1;
  13. public void test2() {
  14. }
  15. }

容器启动后直接抛出异常:

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

令人困惑的是:把 test1() 方法上的 @Async 去掉,同样的循环依赖结构却能正常启动。这说明问题并不在于“循环依赖”本身,而在于 循环依赖与代理增强叠加 时的时序冲突。要解释清楚这一点,必须先理解 Spring 是如何“容忍”循环依赖的。

二、三级缓存:Spring 解决循环依赖的核心机制

Spring 通过三级缓存来处理单例 Bean 的循环依赖,三者在 DefaultSingletonBeanRegistry 中定义:

缓存 名称 存放内容
一级 singletonObjects 完全初始化好的成品 Bean
二级 earlySingletonObjects 提前暴露的半成品 Bean(尚未填充属性、未初始化)
三级 singletonFactories 用于生成早期引用的 ObjectFactory

创建 Bean 的流程可以简化为:

  1. getBean 时先查一级缓存,命中则直接返回;
  2. 未命中则走创建流程:实例化(instantiate)→ 属性填充(populate)→ 初始化(initialize);
  3. 在实例化之后、属性填充之前,把当前 Bean 的 ObjectFactory 放入三级缓存;
  4. 填充属性时若发现依赖的 Bean 正在创建中,就调用其 ObjectFactory.getObject() 取得早期引用,放入二级缓存并返回;
  5. 初始化完成后,把最终 Bean 放入一级缓存,并清理二、三级缓存。

关键点在于第 3 步:提前暴露的引用是在实例化之后立刻产生的,此时属性还没注入、初始化方法还没执行。之所以敢提前暴露,是因为对于普通 Bean 而言,实例化后的对象就是最终对象——后续的属性填充和初始化都在这同一个对象上进行,引用不会变化。

三、代理改变了“最终对象”

一旦 Bean 需要被 AOP 代理增强(例如 @Async、@Transactional、自定义切面等),情况就不同了:容器最终对外暴露的往往是代理对象,而非原始实例。

代理的创建时机有两种:

  • 初始化后创建:在 initializeBean 的后置处理阶段,由 BeanPostProcessor(如 AbstractAutoProxyCreator)生成代理。这是最常见的方式。
  • 提前创建:在 getEarlyBeanReference 阶段,由 SmartInstantiationAwareBeanPostProcessor 提前生成代理引用,放入三级缓存的 ObjectFactory 中。

AbstractAutoProxyCreator 同时实现了这两个入口:它在 getEarlyBeanReference 中记录“该 Bean 已被提前代理”,并在 postProcessAfterInitialization 中判断——若已经提前代理过,则直接返回已有代理,不再重复创建。

于是可能出现的冲突是:如果 Bean 在循环依赖中被提前暴露了原始实例,而最终又在初始化阶段被包装成代理,那么注入到其他 Bean 中的引用就是原始实例,而容器中实际登记的却是代理。两者不一致,Spring 便在 allowCircularReferences 相关校验中抛出 BeanCurrentlyInCreationException,提示“其他 Bean 持有的不是最终版本”。

四、回到 @Async 的例子

@Async 标注的方法需要通过代理拦截才能异步执行,因此 TestService1 必然被代理。结合上面的机制,分析两类情形:

情形一(去掉 @Async):TestService1 无需代理,提前暴露的原始实例就是最终对象。TestService2 注入到的引用与容器中的成品一致,循环依赖被顺利化解。

情形二(保留 @Async):TestService1 需要代理。启动过程中,TestService1 先实例化并把自己放进三级缓存;随后填充属性时创建 TestService2,TestService2 反过来注入 TestService1,从三级缓存拿到的是原始实例;TestService2 创建完成后,TestService1 继续初始化,在初始化后置处理阶段被包装为代理对象。此时容器发现:TestService2 持有的 TestService1 是原始版本,而不是最终代理版本,于是抛出异常。

换个角度说:循环依赖本身没问题,问题在于“被依赖的 Bean 在循环链上能否稳定地提前暴露最终形态”。代理增强了“最终形态”的不确定性,才使校验失败。

值得补充的是,Spring 并非对所有代理都报错。如果代理能在 getEarlyBeanReference 阶段提前生成(即 ObjectFactory 返回的就是代理),那么提前暴露与最终形态一致,就不会触发该异常。是否提前代理,取决于具体的 BeanPostProcessor 实现与配置。实践中常见的差异,往往来自代理创建时机与 @Async 处理链路的组合方式。

五、排查与规避方案

遇到这类异常,可按以下思路处理:

  1. 确认循环链上是否有代理增强。检查 @Async、@Transactional、自定义切面、缓存注解等是否落在循环依赖的 Bean 上。
  2. 优先打破循环依赖。这是最彻底的方案:把共享逻辑抽取到第三个 Bean,或改为通过 ApplicationContext、事件机制、接口回调等方式解耦,避免 A 依赖 B、B 又依赖 A。
  3. 调整依赖方向。让代理增强的 Bean 只被单向依赖,或让被注入方改为延迟获取(例如注入 ObjectProvider),把解析时机推迟到首次使用。
  4. 审视代理创建时机相关配置。确认是否存在可让代理提前生成的配置项,使提前暴露的引用与最终引用一致;但此类调整需评估对事务、异步语义的影响,不宜盲目开启。
  5. 避免在循环链上做重量级增强。把异步、事务等横切逻辑收敛到不参与循环依赖的边界 Bean 上,从设计上规避冲突。

六、小结

Spring 的三级缓存解决的是“对象何时可被提前引用”的问题,它默认了一个前提:提前暴露的引用与最终对象是同一个。代理增强打破了这个前提,使“半成品”和“成品”可能不是同一个对象,从而触发 BeanCurrentlyInCreationException。理解这一点后,@Async 引发的启动失败就不再神秘:它不是 Spring 解决不了循环依赖,而是循环依赖与代理增强在时序上发生了冲突。工程实践中,最稳妥的做法仍是从设计上消除循环依赖,把代理增强限制在依赖关系清晰的边界之内。

发表评论

活动