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

发布时间:2026/6/27 19:23:11
一个 @Async 让循环依赖暴雷:Spring 代理的暗坑 一个 Async 让循环依赖暴雷Spring 代理的暗坑项目跑了半年没报错加了 Async 之后启动直接炸BeanCreationException。循环依赖一直都在只是 Async 把它从隐藏变成了致命。一、事故现场周三上线一个新功能订单完成后异步发送短信通知。改动很小就在 NotifyService 里加了个 Async 注解。ServicepublicclassNotifyService{AutowiredprivateOrderServiceorderService;// 依赖 OrderServiceAsync(notifyThreadPool)publicvoidsendSms(LongorderId){OrderorderorderService.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初始化完成代理也生成完了MapString,ObjectsingletonObjects;// 二级缓存提前暴露的 Bean实例化了但还没初始化完MapString,ObjectearlySingletonObjects;// 三级缓存Bean 工厂能生产 Bean 或它的代理MapString,ObjectFactory?singletonFactories;Spring 创建 Bean 的流程1. 实例化 Beannew 出来还没注入属性 → 把 ObjectFactory 放入三级缓存 2. 注入属性处理 Autowired 依赖 → 如果依赖的 Bean 还没创建完从三级缓存拿 ObjectFactory调 getObject() 拿到早期引用 → 拿到的早期引用放入二级缓存 3. 初始化执行 PostConstruct、AOP 代理等 → AOP 在这一步生成代理对象 4. 放入一级缓存清理二三级缓存2.2 正常循环依赖怎么解决的以我们的场景为例OrderService 和 NotifyService 互相依赖。1. 创建 OrderService → 实例化 OrderServiceObjectFactory 放入三级缓存 → 注入属性时发现需要 NotifyService 2. 创建 NotifyService → 实例化 NotifyServiceObjectFactory 放入三级缓存 → 注入属性时发现需要 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;SpringBootApplicationEnableAsyncpublicclassCircularDepAsyncApp{publicstaticvoidmain(String[]args){try{ConfigurableApplicationContextctxSpringApplication.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不再依赖 NotifyServiceAutowiredprivateOrderMapperorderMapper;publicvoidcompleteOrder(LongorderId){OrderorderorderMapper.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{AutowiredLazy// 延迟注入启动时不创建 NotifyService 的真实对象注入一个代理privateNotifyServicenotifyService;publicvoidcompleteOrder(LongorderId){orderMapper.updateStatus(orderId,COMPLETED);notifyService.sendSms(orderId);}}Lazy 让 Spring 注入一个 NotifyService 的代理注意这个代理跟 Async 的代理不是一回事。实际使用 notifyService 时才真正创建避开了启动时的循环依赖检测。缺点Lazy 只是绕过了问题循环依赖还在。如果 NotifyService 的初始化也依赖 OrderServiceLazy 可能导致运行时 NPE。方案 3用 ApplicationContext 手动获取ServicepublicclassNotifyService{AutowiredprivateApplicationContextapplicationContext;privateOrderServiceorderService;Async(notifyThreadPool)publicvoidsendSms(LongorderId){if(orderServicenull){orderServiceapplicationContext.getBean(OrderService.class);}OrderorderorderService.getById(orderId);smsApi.send(order.getPhone(),订单已完成);}}不在构造时注入 OrderService而是运行时从 ApplicationContext 获取。打破了启动时的循环依赖。缺点代码不够优雅而且 ApplicationContext.getBean 有性能开销虽然很小。不推荐作为首选方案。方案 4用事件驱动替代直接调用// OrderService 发布事件不直接调 NotifyServiceServicepublicclassOrderService{AutowiredprivateApplicationEventPublishereventPublisher;publicvoidcompleteOrder(LongorderId){orderMapper.updateStatus(orderId,COMPLETED);eventPublisher.publishEvent(newOrderCompletedEvent(orderId));}}// NotifyService 监听事件异步处理ServicepublicclassNotifyService{AutowiredprivateOrderServiceorderService;// 这个依赖可以去掉直接查 orderMapperAsync(notifyThreadPool)EventListenerpublicvoidonOrderCompleted(OrderCompletedEventevent){OrderorderorderService.getById(event.getOrderId());smsApi.send(order.getPhone(),订单已完成);}}用 Spring 事件机制解耦。OrderService 不直接调 NotifyService而是发布订单完成事件。NotifyService 监听事件并异步处理。OrderService 不需要依赖 NotifyService循环依赖自然消失。这是最优雅的方案。不仅解决了循环依赖还解耦了业务逻辑。如果后续加更多通知方式邮件、推送只需要加新的 EventListener不用改 OrderService。六、Transactional vs Async 循环依赖对比TransactionalAsync代理创建器AbstractAutoProxyCreatorAsyncAnnotationBeanPostProcessor是否实现 getEarlyBeanReference是循环依赖时提前暴露代理否暴露的是原始对象代理实际生成时机初始化后postProcessAfterInitialization循环依赖时提前到属性注入阶段仅初始化后postProcessAfterInitialization循环依赖是否报错不报错报错 BeanCurrentlyInCreationException解决方式不用处理打破循环 / Lazy / 事件驱动核心区别Transactional 的代理创建器实现了getEarlyBeanReference()循环依赖时能提前暴露代理。Async 的代理创建器没实现三级缓存暴露的是原始对象代理在初始化后才生成两者不一致就报错。七、CheckList循环依赖 AOP 排查#检查项风险点正确做法1Async 方法所在 Bean 参与循环依赖启动报错打破循环或用事件驱动2循环依赖 任何后置 BeanPostProcessor同样可能报错检查是否有自定义 BeanPostProcessor3用 Lazy 绕过循环依赖运行时可能 NPE优先打破循环而非 Lazy4Async 自调用AOP 代理失效异步变同步拆到另一个 Bean5循环依赖本身设计问题应该消除用事件驱动或重新划分 Service 职责6Spring Boot 2.6 默认禁止循环依赖启动直接报错spring.main.allow-circular-referencestrue 或消除循环注意第 6 条Spring Boot 2.6 开始默认禁止循环依赖。如果你的项目升级到 2.6之前跑得好好的循环依赖会直接启动报错。Async 只是提前暴露了这个问题不升级也会有隐患。八、总结回到这次事故NotifyService 和 OrderService 互相依赖跑了半年没报错。加了 Async 之后因为 Async 的代理创建器没实现getEarlyBeanReference()循环依赖时三级缓存暴露的是原始对象跟最终代理对象不一致Spring 检测到后报错。记住这三点Spring 三级缓存能解决普通 AOPTransactional的循环依赖但不能解决 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;SpringBootApplicationEnableAsyncpublicclassCircularDepAsyncApp{Bean(notifyThreadPool)publicExecutornotifyThreadPool(){ThreadPoolTaskExecutorexecutornewThreadPoolTaskExecutor();executor.setCorePoolSize(4);executor.setMaxPoolSize(8);executor.setQueueCapacity(100);executor.setThreadNamePrefix(notify-);executor.initialize();returnexecutor;}publicstaticvoidmain(String[]args){try{ConfigurableApplicationContextctxSpringApplication.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 跟循环依赖的冲突。