一个 @Async 让循环依赖暴雷:Spring 代理的暗坑
项目跑了半年没报错,加了 @Async 之后启动直接炸,BeanCreationException。循环依赖一直都在,只是 @Async 把它从"隐藏"变成了"致命"。
一、事故现场
周三上线一个新功能:订单完成后异步发送短信通知。改动很小,就在 NotifyService 里加了个 @Async 注解。
java
@Service
public class NotifyService {
@Autowired
private OrderService orderService; // 依赖 OrderService
@Async("notifyThreadPool")
public void sendSms(Long orderId) {
Order order = orderService.getById(orderId);
smsApi.send(order.getPhone(), "订单已完成");
}
}
OrderService 本身依赖 NotifyService(因为 OrderService 完成订单后要调通知):
java
@Service
public class OrderService {
@Autowired
private NotifyService notifyService; // 依赖 NotifyService
public void completeOrder(Long orderId) {
orderMapper.updateStatus(orderId, "COMPLETED");
notifyService.sendSms(orderId); // 调通知
}
}
NotifyService → OrderService → NotifyService,循环依赖。
但这里有个疑问:这个循环依赖之前就存在,项目跑了半年没报错。为什么加了 @Async 就炸了?
启动报错:
vbnet
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 三级缓存是什么
java
// DefaultSingletonBeanRegistry.java
// 一级缓存:完整的 Bean(初始化完成,代理也生成完了)
Map<String, Object> singletonObjects;
// 二级缓存:提前暴露的 Bean(实例化了,但还没初始化完)
Map<String, Object> earlySingletonObjects;
// 三级缓存:Bean 工厂(能生产 Bean 或它的代理)
Map<String, ObjectFactory<?>> singletonFactories;
Spring 创建 Bean 的流程:
markdown
1. 实例化 Bean(new 出来,还没注入属性)
→ 把 ObjectFactory 放入三级缓存
2. 注入属性(处理 @Autowired 依赖)
→ 如果依赖的 Bean 还没创建完,从三级缓存拿 ObjectFactory,调 getObject() 拿到早期引用
→ 拿到的早期引用放入二级缓存
3. 初始化(执行 @PostConstruct、AOP 代理等)
→ AOP 在这一步生成代理对象
4. 放入一级缓存,清理二三级缓存
2.2 正常循环依赖怎么解决的
以我们的场景为例:OrderService 和 NotifyService 互相依赖。
markdown
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)才生成。
这导致一个时间差:
markdown
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 复现代码
java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
@SpringBootApplication
@EnableAsync
public class CircularDepAsyncApp {
public static void main(String[] args) {
try {
ConfigurableApplicationContext ctx = SpringApplication.run(CircularDepAsyncApp.class, args);
System.out.println("启动成功");
ctx.close();
} catch (Exception e) {
System.out.println("启动失败: " + e.getClass().getSimpleName());
System.out.println(e.getMessage());
}
}
}
@Service
class OrderService {
@Autowired
private NotifyService notifyService;
public void completeOrder(Long orderId) {
System.out.println("订单完成: " + orderId);
notifyService.sendSms(orderId);
}
}
@Service
class NotifyService {
@Autowired
private OrderService orderService;
@Async("notifyThreadPool") // 加了这个注解就炸
public void sendSms(Long orderId) {
System.out.println("发送短信: " + orderId);
}
}
运行结果:
csharp
启动失败: 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 也不报错
java
@Service
class NotifyService {
@Autowired
private OrderService orderService;
@Transactional // 换成 @Transactional,不报错
public void sendSms(Long orderId) {
System.out.println("发送短信: " + orderId);
}
}
@Transactional 的代理创建器实现了 getEarlyBeanReference(),循环依赖时提前暴露代理对象,和最终一致,不报错。
五、怎么解决
方案 1:打破循环依赖(最根本)
循环依赖本身就是设计问题,最好的办法是消除它。把互相依赖拆开:
java
// 把通知逻辑拆到独立的 Service,切断循环
@Service
public class SmsService {
@Async("notifyThreadPool")
public void sendSms(Long orderId, String phone) {
smsApi.send(phone, "订单已完成");
}
}
@Service
public class OrderService {
@Autowired
private SmsService smsService; // 依赖 SmsService,不再依赖 NotifyService
@Autowired
private OrderMapper orderMapper;
public void completeOrder(Long orderId) {
Order order = orderMapper.selectById(orderId);
orderMapper.updateStatus(orderId, "COMPLETED");
smsService.sendSms(orderId, order.getPhone());
}
}
@Service
public class NotifyService {
@Autowired
private OrderService orderService;
// NotifyService 可以保留,但不被 OrderService 依赖
// 或者直接把 NotifyService 删掉,逻辑合并到 SmsService
}
OrderService → SmsService,单向依赖,没有循环。@Async 在 SmsService 上,不影响。
这是最推荐的方案。循环依赖本身就应该消除,@Async 只是让它暴露了而已。
方案 2:用 @Lazy 延迟注入
java
@Service
public class OrderService {
@Autowired
@Lazy // 延迟注入,启动时不创建 NotifyService 的真实对象,注入一个代理
private NotifyService notifyService;
public void completeOrder(Long orderId) {
orderMapper.updateStatus(orderId, "COMPLETED");
notifyService.sendSms(orderId);
}
}
@Lazy 让 Spring 注入一个 NotifyService 的代理(注意这个代理跟 @Async 的代理不是一回事)。实际使用 notifyService 时才真正创建,避开了启动时的循环依赖检测。
缺点:@Lazy 只是绕过了问题,循环依赖还在。如果 NotifyService 的初始化也依赖 OrderService,@Lazy 可能导致运行时 NPE。
方案 3:用 ApplicationContext 手动获取
java
@Service
public class NotifyService {
@Autowired
private ApplicationContext applicationContext;
private OrderService orderService;
@Async("notifyThreadPool")
public void sendSms(Long orderId) {
if (orderService == null) {
orderService = applicationContext.getBean(OrderService.class);
}
Order order = orderService.getById(orderId);
smsApi.send(order.getPhone(), "订单已完成");
}
}
不在构造时注入 OrderService,而是运行时从 ApplicationContext 获取。打破了启动时的循环依赖。
缺点:代码不够优雅,而且 ApplicationContext.getBean 有性能开销(虽然很小)。不推荐作为首选方案。
方案 4:用事件驱动替代直接调用
java
// OrderService 发布事件,不直接调 NotifyService
@Service
public class OrderService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void completeOrder(Long orderId) {
orderMapper.updateStatus(orderId, "COMPLETED");
eventPublisher.publishEvent(new OrderCompletedEvent(orderId));
}
}
// NotifyService 监听事件,异步处理
@Service
public class NotifyService {
@Autowired
private OrderService orderService; // 这个依赖可以去掉,直接查 orderMapper
@Async("notifyThreadPool")
@EventListener
public void onOrderCompleted(OrderCompletedEvent event) {
Order order = 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 只是让它暴露了。正确做法是消除循环,用事件驱动解耦
下次遇到"加了注解就启动报错"的问题,先检查是不是循环依赖 + 后置代理的组合。
附录:本地复现完整代码
java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@SpringBootApplication
@EnableAsync
public class CircularDepAsyncApp {
@Bean("notifyThreadPool")
public Executor notifyThreadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("notify-");
executor.initialize();
return executor;
}
public static void main(String[] args) {
try {
ConfigurableApplicationContext ctx = SpringApplication.run(CircularDepAsyncApp.class, args);
System.out.println("✅ 启动成功");
ctx.close();
} catch (Exception e) {
System.out.println("❌ 启动失败: " + e.getClass().getSimpleName());
System.out.println(e.getMessage());
}
}
}
@Service
class OrderService {
@Autowired
private NotifyService notifyService;
public void completeOrder(Long orderId) {
System.out.println("订单完成: " + orderId);
notifyService.sendSms(orderId);
}
}
@Service
class NotifyService {
@Autowired
private OrderService orderService;
// 加了 @Async → 启动报错 BeanCurrentlyInCreationException
// 去掉 @Async → 启动成功
// 换成 @Transactional → 启动成功(代理创建器实现了 getEarlyBeanReference,能提前暴露代理)
@Async("notifyThreadPool")
public void sendSms(Long orderId) {
System.out.println("发送短信: " + orderId);
}
}
运行方式:直接在 Spring Boot 项目里运行 main 方法。
复现要点:
- 保持 @Async 注解 → 启动报错 BeanCurrentlyInCreationException
- 去掉 @Async → 启动成功(循环依赖被三级缓存解决)
- 换成 @Transactional → 启动成功(代理创建器实现了 getEarlyBeanReference,能提前暴露代理)
对比三种情况,理解 @Async 代理创建器缺少 getEarlyBeanReference 跟循环依赖的冲突。