Spring循环依赖:@Async为何引发注入异常
作者:carzy2026.11.07 00:10浏览量:0简介:本文从一次真实启动失败切入,系统剖析Spring三级缓存解决循环依赖的机制,重点解释为何在循环依赖链上叠加@Async等代理增强后,原始实例与最终代理实例不一致会触发BeanCurrentlyInCreationException,并给出可落地的排查思路与规避方案,帮助读者彻底理解代理时机与缓存暴露的边界。
Spring 循环依赖:@Async 为何引发注入异常
一、从一个启动失败说起
在一次常规的项目启动中,两个互相注入的 Service 构成了典型的循环依赖:
@Servicepublic class TestService1 {@Autowiredprivate TestService2 testService2;@Asyncpublic void test1() {}}@Servicepublic class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}}
容器启动后直接抛出异常:
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.
令人困惑的是:把 test1() 方法上的 @Async 去掉,同样的循环依赖结构却能正常启动。这说明问题并不在于“循环依赖”本身,而在于 循环依赖与代理增强叠加 时的时序冲突。要解释清楚这一点,必须先理解 Spring 是如何“容忍”循环依赖的。
二、三级缓存:Spring 解决循环依赖的核心机制
Spring 通过三级缓存来处理单例 Bean 的循环依赖,三者在 DefaultSingletonBeanRegistry 中定义:
| 缓存 | 名称 | 存放内容 |
|---|---|---|
| 一级 | singletonObjects |
完全初始化好的成品 Bean |
| 二级 | earlySingletonObjects |
提前暴露的半成品 Bean(尚未填充属性、未初始化) |
| 三级 | singletonFactories |
用于生成早期引用的 ObjectFactory |
创建 Bean 的流程可以简化为:
getBean时先查一级缓存,命中则直接返回;- 未命中则走创建流程:实例化(
instantiate)→ 属性填充(populate)→ 初始化(initialize); - 在实例化之后、属性填充之前,把当前 Bean 的
ObjectFactory放入三级缓存; - 填充属性时若发现依赖的 Bean 正在创建中,就调用其
ObjectFactory.getObject()取得早期引用,放入二级缓存并返回; - 初始化完成后,把最终 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 处理链路的组合方式。
五、排查与规避方案
遇到这类异常,可按以下思路处理:
- 确认循环链上是否有代理增强。检查
@Async、@Transactional、自定义切面、缓存注解等是否落在循环依赖的 Bean 上。 - 优先打破循环依赖。这是最彻底的方案:把共享逻辑抽取到第三个 Bean,或改为通过
ApplicationContext、事件机制、接口回调等方式解耦,避免 A 依赖 B、B 又依赖 A。 - 调整依赖方向。让代理增强的 Bean 只被单向依赖,或让被注入方改为延迟获取(例如注入
ObjectProvider),把解析时机推迟到首次使用。 - 审视代理创建时机相关配置。确认是否存在可让代理提前生成的配置项,使提前暴露的引用与最终引用一致;但此类调整需评估对事务、异步语义的影响,不宜盲目开启。
- 避免在循环链上做重量级增强。把异步、事务等横切逻辑收敛到不参与循环依赖的边界 Bean 上,从设计上规避冲突。
六、小结
Spring 的三级缓存解决的是“对象何时可被提前引用”的问题,它默认了一个前提:提前暴露的引用与最终对象是同一个。代理增强打破了这个前提,使“半成品”和“成品”可能不是同一个对象,从而触发 BeanCurrentlyInCreationException。理解这一点后,@Async 引发的启动失败就不再神秘:它不是 Spring 解决不了循环依赖,而是循环依赖与代理增强在时序上发生了冲突。工程实践中,最稳妥的做法仍是从设计上消除循环依赖,把代理增强限制在依赖关系清晰的边界之内。

登录后可评论,请前往 登录 或 注册