一个 @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 为循环依赖提供了一个提前暴露的机制:如果代理创建器实现了SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference()方法,就能在第 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 | |
|---|---|---|
| 代理创建器 | AbstractAutoProxyCreator | AsyncAnnotationBeanPostProcessor |
| 是否实现 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 职责 |
| 6 | Spring Boot 2.6+ 默认禁止循环依赖 | 启动直接报错 | spring.main.allow-circular-references=true 或消除循环 |
注意第 6 条:Spring Boot 2.6 开始默认禁止循环依赖。如果你的项目升级到 2.6+,之前"跑得好好的"循环依赖会直接启动报错。@Async 只是提前暴露了这个问题,不升级也会有隐患。
八、总结
回到这次事故:NotifyService 和 OrderService 互相依赖,跑了半年没报错。加了 @Async 之后,因为 @Async 的代理创建器没实现getEarlyBeanReference(),循环依赖时三级缓存暴露的是原始对象,跟最终代理对象不一致,Spring 检测到后报错。
记住这三点:
- Spring 三级缓存能解决普通 AOP(@Transactional)的循环依赖,但不能解决 @Async
- 区别在于代理创建器是否实现了
getEarlyBeanReference():@Transactional 实现了,能提前暴露代理;@Async 没实现,注入的是原始对象,和最终代理不一致- 循环依赖本身是设计问题,@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 方法。
复现要点:
- 保持 @Async 注解 → 启动报错 BeanCurrentlyInCreationException
- 去掉 @Async → 启动成功(循环依赖被三级缓存解决)
- 换成 @Transactional → 启动成功(代理创建器实现了 getEarlyBeanReference,能提前暴露代理)
对比三种情况,理解 @Async 代理创建器缺少 getEarlyBeanReference 跟循环依赖的冲突。