news 2026/7/2 2:05:59

一个 @Async 让循环依赖暴雷:Spring 代理的暗坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一个 @Async 让循环依赖暴雷:Spring 代理的暗坑

一个 @Async 让循环依赖暴雷:Spring 代理的暗坑

项目跑了半年没报错,加了 @Async 之后启动直接炸,BeanCreationException。循环依赖一直都在,只是 @Async 把它从"隐藏"变成了"致命"。


一、事故现场

周三上线一个新功能:订单完成后异步发送短信通知。改动很小,就在 NotifyService 里加了个 @Async 注解。

@ServicepublicclassNotifyService{@AutowiredprivateOrderServiceorderService;// 依赖 OrderService@Async("notifyThreadPool")publicvoidsendSms(LongorderId){Orderorder=orderService.getById(orderId);smsApi.send(order.getPhone(),"订单已完成");}}

OrderService 本身依赖 NotifyService(因为 OrderService 完成订单后要调通知):

@ServicepublicclassOrderService{@AutowiredprivateNotifyServicenotifyService;// 依赖 NotifyServicepublicvoidcompleteOrder(LongorderId){orderMapper.updateStatus(orderId,"COMPLETED");notifyService.sendSms(orderId);// 调通知}}

NotifyService → OrderService → NotifyService,循环依赖。

但这里有个疑问:这个循环依赖之前就存在,项目跑了半年没报错。为什么加了 @Async 就炸了?

启动报错:

BeanCurrentlyInCreationException: Error creating bean with name 'notifyService': Bean with name 'notifyService' has been injected into other beans [orderService] in its raw object 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.

翻译过来:notifyService 在创建过程中被注入到 orderService 的是"原始对象"(还没被代理包装),但最终 notifyService 被 @Async 的 AOP 代理包装了。注入的和最终的不是同一个对象,Spring 认为这是错误的。


二、先搞清楚:Spring 的循环依赖是怎么解决的

Spring 用"三级缓存"解决循环依赖。理解了三级缓存,才能理解为什么 @Async 会让它失效。

2.1 三级缓存是什么

// DefaultSingletonBeanRegistry.java// 一级缓存:完整的 Bean(初始化完成,代理也生成完了)Map<String,Object>singletonObjects;// 二级缓存:提前暴露的 Bean(实例化了,但还没初始化完)Map<String,Object>earlySingletonObjects;// 三级缓存:Bean 工厂(能生产 Bean 或它的代理)Map<String,ObjectFactory<?>>singletonFactories;

Spring 创建 Bean 的流程:

1. 实例化 Bean(new 出来,还没注入属性) → 把 ObjectFactory 放入三级缓存 2. 注入属性(处理 @Autowired 依赖) → 如果依赖的 Bean 还没创建完,从三级缓存拿 ObjectFactory,调 getObject() 拿到早期引用 → 拿到的早期引用放入二级缓存 3. 初始化(执行 @PostConstruct、AOP 代理等) → AOP 在这一步生成代理对象 4. 放入一级缓存,清理二三级缓存

2.2 正常循环依赖怎么解决的

以我们的场景为例:OrderService 和 NotifyService 互相依赖。

1. 创建 OrderService → 实例化 OrderService,ObjectFactory 放入三级缓存 → 注入属性时发现需要 NotifyService 2. 创建 NotifyService → 实例化 NotifyService,ObjectFactory 放入三级缓存 → 注入属性时发现需要 OrderService → 从三级缓存拿到 OrderService 的 ObjectFactory,调 getObject() → 得到 OrderService 的早期引用(还没初始化完),放入二级缓存 → NotifyService 属性注入完成 3. NotifyService 初始化完成,放入一级缓存 4. 回到 OrderService,拿到 NotifyService,属性注入完成 → OrderService 初始化完成,放入一级缓存

关键在第二步:NotifyService 需要 OrderService,但 OrderService 还没创建完。Spring 从三级缓存拿到 OrderService 的早期引用,NotifyService 就能完成创建。然后 OrderService 也能拿到完成的 NotifyService,双方都创建完成。

这个机制能工作的前提是:三级缓存里拿到的早期引用,和最终放入一级缓存的对象是同一个。

2.3 AOP 代理什么时候生成

普通 AOP(比如 @Transactional)的代理在初始化后(postProcessAfterInitialization)生成(第 3 步)。但 Spring 为循环依赖提供了一个提前暴露的机制:如果代理创建器实现了SmartInstantiationAwareBeanPostProcessorgetEarlyBeanReference()方法,就能在第 2 步提前生成代理。

  • @Transactional 的代理创建器(AbstractAutoProxyCreator)实现了这个方法 → 提前生成代理,二级缓存存的是代理
  • @Async 的代理创建器(AsyncAnnotationBeanPostProcessor)没实现 → 不提前生成,二级缓存存的是原始对象

所以 @Transactional 的循环依赖能解决:代理在二级缓存阶段就提前生成了,注入的也是代理对象,跟最终的一致。


三、@Async 为什么让循环依赖失效

@Async 的问题在于:它的代理创建器(AsyncAnnotationBeanPostProcessor)没有实现getEarlyBeanReference()方法,无法在循环依赖时提前暴露代理。代理只在初始化后(postProcessAfterInitialization)才生成。

这导致一个时间差:

1. 创建 NotifyService → 实例化,ObjectFactory 放入三级缓存 → 注入属性,需要 OrderService 2. 创建 OrderService → 实例化,ObjectFactory 放入三级缓存 → 注入属性,需要 NotifyService → 从三级缓存拿 NotifyService 的早期引用 → 三级缓存的 ObjectFactory 调 getObject() → 此时检查:NotifyService 的代理创建器有没有实现 getEarlyBeanReference? → 没有(AsyncAnnotationBeanPostProcessor 没实现这个方法) → 返回原始对象(没有代理) → OrderService 拿到的是 NotifyService 的原始对象,注入完成 3. NotifyService 初始化 → 初始化后,AsyncAnnotationBeanPostProcessor 生效 → 为 NotifyService 生成 @Async 代理对象 → 最终放入一级缓存的是代理对象 4. Spring 检查:注入给 OrderService 的是原始对象,一级缓存里是代理对象 → 不是同一个对象! → 抛出 BeanCurrentlyInCreationException

问题就在这:@Transactional 的代理创建器实现了getEarlyBeanReference(),循环依赖时能提前暴露代理。@Async 的代理创建器没实现,三级缓存暴露的是原始对象,代理在初始化后才生成,两者不一致就报错。

一句话总结:

Spring 三级缓存能解决 @Transactional 的循环依赖,因为它的代理创建器实现了getEarlyBeanReference(),代理能提前暴露。但 @Async 的代理创建器没实现这个方法,三级缓存暴露的是原始对象,注入的和最终的不是同一个,Spring 检测到不一致就报错。


四、5 秒复现:加个注解就炸

4.1 复现代码

importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.context.ConfigurableApplicationContext;importorg.springframework.scheduling.annotation.Async;importorg.springframework.scheduling.annotation.EnableAsync;importorg.springframework.stereotype.Service;importorg.springframework.beans.factory.annotation.Autowired;@SpringBootApplication@EnableAsyncpublicclassCircularDepAsyncApp{publicstaticvoidmain(String[]args){try{ConfigurableApplicationContextctx=SpringApplication.run(CircularDepAsyncApp.class,args);System.out.println("启动成功");ctx.close();}catch(Exceptione){System.out.println("启动失败: "+e.getClass().getSimpleName());System.out.println(e.getMessage());}}}@ServiceclassOrderService{@AutowiredprivateNotifyServicenotifyService;publicvoidcompleteOrder(LongorderId){System.out.println("订单完成: "+orderId);notifyService.sendSms(orderId);}}@ServiceclassNotifyService{@AutowiredprivateOrderServiceorderService;@Async("notifyThreadPool")// 加了这个注解就炸publicvoidsendSms(LongorderId){System.out.println("发送短信: "+orderId);}}

运行结果:

启动失败: BeanCurrentlyInCreationException Error creating bean with name 'notifyService': Bean with name 'notifyService' has been injected into other beans [orderService] in its raw object version, as part of a circular reference, but has eventually been wrapped.

4.2 对比:去掉 @Async 就不报错

@Async("notifyThreadPool")注释掉,启动成功。循环依赖还是那个循环依赖,但没有 @Async 的代理包装,三级缓存暴露的原始对象和最终对象一致,Spring 不报错。

4.3 对比:换成 @Transactional 也不报错

@ServiceclassNotifyService{@AutowiredprivateOrderServiceorderService;@Transactional// 换成 @Transactional,不报错publicvoidsendSms(LongorderId){System.out.println("发送短信: "+orderId);}}

@Transactional 的代理创建器实现了getEarlyBeanReference(),循环依赖时提前暴露代理对象,和最终一致,不报错。


五、怎么解决

方案 1:打破循环依赖(最根本)

循环依赖本身就是设计问题,最好的办法是消除它。把互相依赖拆开:

// 把通知逻辑拆到独立的 Service,切断循环@ServicepublicclassSmsService{@Async("notifyThreadPool")publicvoidsendSms(LongorderId,Stringphone){smsApi.send(phone,"订单已完成");}}@ServicepublicclassOrderService{@AutowiredprivateSmsServicesmsService;// 依赖 SmsService,不再依赖 NotifyService@AutowiredprivateOrderMapperorderMapper;publicvoidcompleteOrder(LongorderId){Orderorder=orderMapper.selectById(orderId);orderMapper.updateStatus(orderId,"COMPLETED");smsService.sendSms(orderId,order.getPhone());}}@ServicepublicclassNotifyService{@AutowiredprivateOrderServiceorderService;// NotifyService 可以保留,但不被 OrderService 依赖// 或者直接把 NotifyService 删掉,逻辑合并到 SmsService}

OrderService → SmsService,单向依赖,没有循环。@Async 在 SmsService 上,不影响。

这是最推荐的方案。循环依赖本身就应该消除,@Async 只是让它暴露了而已。

方案 2:用 @Lazy 延迟注入

@ServicepublicclassOrderService{@Autowired@Lazy// 延迟注入,启动时不创建 NotifyService 的真实对象,注入一个代理privateNotifyServicenotifyService;publicvoidcompleteOrder(LongorderId){orderMapper.updateStatus(orderId,"COMPLETED");notifyService.sendSms(orderId);}}

@Lazy 让 Spring 注入一个 NotifyService 的代理(注意这个代理跟 @Async 的代理不是一回事)。实际使用 notifyService 时才真正创建,避开了启动时的循环依赖检测。

缺点:@Lazy 只是绕过了问题,循环依赖还在。如果 NotifyService 的初始化也依赖 OrderService,@Lazy 可能导致运行时 NPE。

方案 3:用 ApplicationContext 手动获取

@ServicepublicclassNotifyService{@AutowiredprivateApplicationContextapplicationContext;privateOrderServiceorderService;@Async("notifyThreadPool")publicvoidsendSms(LongorderId){if(orderService==null){orderService=applicationContext.getBean(OrderService.class);}Orderorder=orderService.getById(orderId);smsApi.send(order.getPhone(),"订单已完成");}}

不在构造时注入 OrderService,而是运行时从 ApplicationContext 获取。打破了启动时的循环依赖。

缺点:代码不够优雅,而且 ApplicationContext.getBean 有性能开销(虽然很小)。不推荐作为首选方案。

方案 4:用事件驱动替代直接调用

// OrderService 发布事件,不直接调 NotifyService@ServicepublicclassOrderService{@AutowiredprivateApplicationEventPublishereventPublisher;publicvoidcompleteOrder(LongorderId){orderMapper.updateStatus(orderId,"COMPLETED");eventPublisher.publishEvent(newOrderCompletedEvent(orderId));}}// NotifyService 监听事件,异步处理@ServicepublicclassNotifyService{@AutowiredprivateOrderServiceorderService;// 这个依赖可以去掉,直接查 orderMapper@Async("notifyThreadPool")@EventListenerpublicvoidonOrderCompleted(OrderCompletedEventevent){Orderorder=orderService.getById(event.getOrderId());smsApi.send(order.getPhone(),"订单已完成");}}

用 Spring 事件机制解耦。OrderService 不直接调 NotifyService,而是发布"订单完成"事件。NotifyService 监听事件并异步处理。OrderService 不需要依赖 NotifyService,循环依赖自然消失。

这是最优雅的方案。不仅解决了循环依赖,还解耦了业务逻辑。如果后续加更多通知方式(邮件、推送),只需要加新的 @EventListener,不用改 OrderService。


六、@Transactional vs @Async 循环依赖对比

@Transactional@Async
代理创建器AbstractAutoProxyCreatorAsyncAnnotationBeanPostProcessor
是否实现 getEarlyBeanReference是,循环依赖时提前暴露代理否,暴露的是原始对象
代理实际生成时机初始化后(postProcessAfterInitialization),循环依赖时提前到属性注入阶段仅初始化后(postProcessAfterInitialization)
循环依赖是否报错不报错报错 BeanCurrentlyInCreationException
解决方式不用处理打破循环 / @Lazy / 事件驱动

核心区别:@Transactional 的代理创建器实现了getEarlyBeanReference(),循环依赖时能提前暴露代理。@Async 的代理创建器没实现,三级缓存暴露的是原始对象,代理在初始化后才生成,两者不一致就报错。


七、CheckList:循环依赖 + AOP 排查

#检查项风险点正确做法
1@Async 方法所在 Bean 参与循环依赖启动报错打破循环或用事件驱动
2循环依赖 + 任何后置 BeanPostProcessor同样可能报错检查是否有自定义 BeanPostProcessor
3用 @Lazy 绕过循环依赖运行时可能 NPE优先打破循环而非 @Lazy
4@Async 自调用AOP 代理失效,异步变同步拆到另一个 Bean
5循环依赖本身设计问题,应该消除用事件驱动或重新划分 Service 职责
6Spring Boot 2.6+ 默认禁止循环依赖启动直接报错spring.main.allow-circular-references=true 或消除循环

注意第 6 条:Spring Boot 2.6 开始默认禁止循环依赖。如果你的项目升级到 2.6+,之前"跑得好好的"循环依赖会直接启动报错。@Async 只是提前暴露了这个问题,不升级也会有隐患。


八、总结

回到这次事故:NotifyService 和 OrderService 互相依赖,跑了半年没报错。加了 @Async 之后,因为 @Async 的代理创建器没实现getEarlyBeanReference(),循环依赖时三级缓存暴露的是原始对象,跟最终代理对象不一致,Spring 检测到后报错。

记住这三点:

  1. Spring 三级缓存能解决普通 AOP(@Transactional)的循环依赖,但不能解决 @Async
  2. 区别在于代理创建器是否实现了getEarlyBeanReference():@Transactional 实现了,能提前暴露代理;@Async 没实现,注入的是原始对象,和最终代理不一致
  3. 循环依赖本身是设计问题,@Async 只是让它暴露了。正确做法是消除循环,用事件驱动解耦

下次遇到"加了注解就启动报错"的问题,先检查是不是循环依赖 + 后置代理的组合。


附录:本地复现完整代码

importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.context.ConfigurableApplicationContext;importorg.springframework.scheduling.annotation.Async;importorg.springframework.scheduling.annotation.EnableAsync;importorg.springframework.stereotype.Service;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Bean;importorg.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;importjava.util.concurrent.Executor;@SpringBootApplication@EnableAsyncpublicclassCircularDepAsyncApp{@Bean("notifyThreadPool")publicExecutornotifyThreadPool(){ThreadPoolTaskExecutorexecutor=newThreadPoolTaskExecutor();executor.setCorePoolSize(4);executor.setMaxPoolSize(8);executor.setQueueCapacity(100);executor.setThreadNamePrefix("notify-");executor.initialize();returnexecutor;}publicstaticvoidmain(String[]args){try{ConfigurableApplicationContextctx=SpringApplication.run(CircularDepAsyncApp.class,args);System.out.println("✅ 启动成功");ctx.close();}catch(Exceptione){System.out.println("❌ 启动失败: "+e.getClass().getSimpleName());System.out.println(e.getMessage());}}}@ServiceclassOrderService{@AutowiredprivateNotifyServicenotifyService;publicvoidcompleteOrder(LongorderId){System.out.println("订单完成: "+orderId);notifyService.sendSms(orderId);}}@ServiceclassNotifyService{@AutowiredprivateOrderServiceorderService;// 加了 @Async → 启动报错 BeanCurrentlyInCreationException// 去掉 @Async → 启动成功// 换成 @Transactional → 启动成功(代理创建器实现了 getEarlyBeanReference,能提前暴露代理)@Async("notifyThreadPool")publicvoidsendSms(LongorderId){System.out.println("发送短信: "+orderId);}}

运行方式:直接在 Spring Boot 项目里运行 main 方法。

复现要点:

  1. 保持 @Async 注解 → 启动报错 BeanCurrentlyInCreationException
  2. 去掉 @Async → 启动成功(循环依赖被三级缓存解决)
  3. 换成 @Transactional → 启动成功(代理创建器实现了 getEarlyBeanReference,能提前暴露代理)

对比三种情况,理解 @Async 代理创建器缺少 getEarlyBeanReference 跟循环依赖的冲突。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/27 19:17:43

基于Chrome140的Threads账号自动化——需求分析环境搭建(一)

引言 随着社交媒体营销的兴起&#xff0c;Threads作为全球最大的社区论坛平台之一&#xff0c;已成为企业和个人进行品牌推广、社区运营的重要阵地。然而&#xff0c;手动管理Threads账号很是麻烦&#xff0c;包括发帖、回复、投票、社区互动等繁琐操作。RPA&#xff08;机器人…

作者头像 李华
网站建设 2026/6/27 19:14:39

性能分析贴身助手:Claude Code 自动化解析火焰图并给出精准优化建议

引言:当火焰图不再是“天书” 周五下午四点,你的服务P99延迟突然飙到2.3秒。你打开性能剖析器,面对一片红黄交织的火焰图——每个函数调用栈都像一团乱麻。你花了两个小时逐层展开,终于定位到一个嵌套循环在处理大批量数据时触发了O(n)复杂度。但这是对的吗?还有没有更深…

作者头像 李华
网站建设 2026/6/27 19:13:36

RDP Wrapper终极指南:让Windows家庭版支持多用户远程桌面

RDP Wrapper终极指南&#xff1a;让Windows家庭版支持多用户远程桌面 【免费下载链接】rdpwrap RDP Wrapper Library 项目地址: https://gitcode.com/gh_mirrors/rd/rdpwrap 你是否在使用Windows家庭版时&#xff0c;渴望像专业版一样享受多用户远程桌面功能&#xff1f…

作者头像 李华
网站建设 2026/6/29 10:34:54

电缆在线监测系统开发指南:从原理到应用

一、产品概述地下电力电缆是电网与工业生产的电能大动脉。沃伦森WRS-CBS11X系统依托自研行波检测与多模态传感融合技术&#xff0c;实现电缆全维度状态实时感知、隐患提前预警、故障精准定位。七大监测维度&#xff1a;1.电压电流暂态行波采集&#xff08;50MHz高频采样&#x…

作者头像 李华
网站建设 2026/6/27 19:03:11

从珍珠到Titan,舞台灯光的“百兽之王”如何炼成?

如果你是灯光师&#xff0c;或者混迹于演出、酒吧、剧场后台&#xff0c;那你一定对“老虎控台”不陌生。它几乎成为了当今中小型演出灯光控制台的代名词。但很多人并不清楚&#xff0c;这台印着酷炫橙色虎纹、带着触摸屏的控台&#xff0c;究竟是如何一步步占领市场&#xff0…

作者头像 李华
网站建设 2026/6/27 19:00:08

WeChat-Need-Web:打破微信网页版访问限制的终极解决方案

WeChat-Need-Web&#xff1a;打破微信网页版访问限制的终极解决方案 【免费下载链接】wechat-need-web 让微信网页版可用 / Allow the use of WeChat via webpage access 项目地址: https://gitcode.com/gh_mirrors/we/wechat-need-web 你是否曾经在办公电脑上需要快速登…

作者头像 李华