@Async 如何避免内部调用失效?
- 标签:后端、Spring
🏛 业务架构师
@Async 内部调用失效的根因:异步能力是代理对象提供的,this.method() 走的是原始对象,不经过代理,注解自然不生效。避免方式按我推荐的优先级排:
- 重构拆类(根治,首选):把异步方法抽到独立的 Bean(如
NotificationSender),注入后调用——调用的是代理对象,注解生效。顺便修了职责边界:异步逻辑独立成类,测试和复用都更干净; - 注入自身代理:
@Autowired @Lazy private AsyncService self;然后调self.asyncMethod()——改动小,但语义别扭,我视为临时止血; - AopContext.currentProxy():需
@EnableAspectJAutoProxy(exposeProxy=true),框架味太重,可读性差,评审时我会要求注释; - 不绕,直接正面解决:拿不到代理就别用注解异步——注入
TaskExecutor手动提交,把方法体提成普通私有方法:
java
executor.submit(() -> doNotify(orderId));这版反而显式、可靠。
架构师视角的延伸:同一类问题(事务失效、缓存失效)都源于"代理 + 自调用",治理上我把"自调用注解方法"列入静态检查规则(ArchUnit/自定义 lint),用工具替代人的记性——凡是"依赖开发者记住才不踩"的坑,都应该尽量变成 CI 里的自动拦截。
🏗 系统架构师
失效根因:@Async 是代理增强,同类内部 this 调用绕过代理直接执行目标方法,「提交线程池」这一步从未发生。
解法按优先级:
- 重构拆分(正解):把异步方法挪到独立 Bean——自调用本身就是「一个类干两件事」的坏味道信号;
- 事件解耦:发 ApplicationEvent,用
@Async的@EventListener消费——语义从「方法调用」变成「事实通知」,往往更合理; - 自注入代理(兜底):注入自身代理(
ObjectProvider<AsyncService>或字段自注入)后通过代理调用;AopContext.currentProxy()也可,但需要 exposeProxy,侵入性强,不作首选。
治理动作:这类失效编译器不报错、单测不易发现。我在平台里做静态扫描——同类内调用带 @Async 的方法直接 CI 拦截;并要求异步任务有监控埋点(提交数、执行数、队列深度),用运行时数据兜底验证「真的异步了」。
💻 全栈开发专家
失效根因:@Async 靠代理实现,同类内部 this.method() 不经过代理,注解等于没写。四个解法:
1. 拆类(首选)——顺带治类膨胀,职责更清晰:
java
@Service
public class MailService {
@Async("mailExecutor")
public void sendAsync(String to) { ... }
}
@Service
public class OrderService {
private final MailService mailService; // 注入的是代理
public void pay() { mailService.sendAsync(user); }
}2. 自注入代理:
java
@Service
public class OrderService {
@Autowired @Lazy
private OrderService self; // 代理对象
public void pay() { self.notifyAsync(order); }
@Async
public void notifyAsync(Order o) { ... }
}3. AopContext.currentProxy():开启 @EnableAspectJAutoProxy(exposeProxy = true) 后 ((OrderService) AopContext.currentProxy()).notifyAsync(order)。
4. 编程式线程池:注入 ThreadPoolTaskExecutor 直接 submit,不纠结代理。
团队规约:默认方案 1,遗留代码赶修复用 2/3 过渡。验证是否生效最简单:方法里打 Thread.currentThread().getName(),看到 mailExecutor-x 而不是 http-nio-exec-x 就 OK。