Spring Boot 中实现异步调用,核心是利用 Spring 的 @Async 注解,它通过 AOP 代理把方法调用从同步执行切换为线程池执行。很多团队在接口变慢时,会把短信通知、邮件发送、操作日志、统计埋点等逻辑拆成异步任务,主线程只负责核心流程,这对提升响应速度非常直观。但 Async 整合并不是在启动类加一个 @EnableAsync 就能高枕无忧,默认的执行器、线程池参数、代理方式、异常捕获都会直接影响最终效果。下面先说明基础整合方式,再逐步展开线程池配置、返回值处理以及失效排查。

一、开启异步支持与基本调用方式
Spring Boot 项目中要启用异步能力,需要在配置类或启动类上添加 @EnableAsync 注解。该注解会扫描容器中标注了 @Async 的方法,并为它们创建代理对象。调用代理方法时,实际执行会被提交到异步执行器,调用方不会等待方法体执行完毕,而是立即拿到返回值或者继续向下执行。
最基础的配置如下,单独定义一个配置类,同时开启异步支持:
@Configuration
@EnableAsync
public class AsyncConfig {
}
然后在一个业务组件中标注 @Async。无返回值的异步方法只需要写 void,调用时会立即返回,方法内部的逻辑在线程池中异步执行。示例:
@Service
public class NotificationService {
@Async
public void sendSms(String mobile, String content) {
// 模拟短信发送耗时
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("短信已发送:" + mobile);
}
}
这段代码表面上能跑通,但存在明显隐患。@EnableAsync 默认使用的执行器是 SimpleAsyncTaskExecutor,每提交一个任务都会新建一个线程,任务执行完后线程不会复用。高并发时线程数量会暴涨,容易耗尽系统资源。因此正式项目里必须替换为基于线程池的执行器,否则异步改造带来的性能提升会被线程创建开销抵消。
二、自定义线程池与多执行器配置
Spring Boot 推荐使用 ThreadPoolTaskExecutor 作为异步任务执行器,它底层封装了 Java 的 ThreadPoolExecutor,支持设置核心线程数、最大线程数、队列容量和拒绝策略。配置时通常会创建多个执行器 Bean,分别给不同业务使用,避免短信任务占用线程导致订单统计任务一直排队。
下面是一个比较完整的配置,包含两个线程池:一个用于订单相关异步任务,一个用于日志和通知类任务。
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("orderExecutor")
public ThreadPoolTaskExecutor orderExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("order-async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Bean("notifyExecutor")
public ThreadPoolTaskExecutor notifyExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("notify-async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
executor.initialize();
return executor;
}
}
在线程池参数中,核心线程数决定常驻线程数量,最大线程数决定高负载下能创建多少线程,队列容量决定任务等待区大小。拒绝策略在队列满且线程数达到最大值时触发,CallerRunsPolicy 会让提交任务的线程自己执行任务,适合不能丢任务的场景;DiscardOldestPolicy 会丢弃等待最久的任务,适合通知类允许丢失的场景。根据业务特点选择策略非常重要。
配置好执行器后,在 @Async 注解中指定 Bean 名称即可使用对应线程池。例如订单流程中的统计上报可以指定 orderExecutor:
@Service
public class OrderAsyncService {
@Async("orderExecutor")
public void reportOrderCreated(Long orderId) {
// 上报订单创建事件到数据中心
System.out.println("订单事件上报:" + orderId);
}
}
如果不指定线程池名称,@Async 会使用默认执行器。建议所有异步方法都显式指定线程池,这样调用关系一目了然,也方便后续监控每个线程池的队列长度和拒绝次数。
三、带返回值的异步任务与异常处理
有些异步任务需要拿到执行结果,比如批量导入后返回成功条数,或者调用外部接口后返回处理状态。这时方法不能再声明为 void,可以返回 Future 或 CompletableFuture。Future 是比较传统的方式,调用方通过 get 方法阻塞等待结果;CompletableFuture 则支持更灵活的链式处理和组合任务。
返回 Future 的示例:
@Service
public class ImportService {
@Async("orderExecutor")
public Future<Integer> importData(List<String> rows) {
int success = 0;
for (String row : rows) {
// 处理每一行数据
success++;
}
return new AsyncResult<>(success);
}
}
调用方需要接收 Future 对象,并在合适的时机调用 get。get 方法会阻塞当前线程直到异步任务完成,建议搭配超时时间,防止异步任务因为外部依赖卡住而拖垮主流程。示例如下:
public void doImport() {
List<String> rows = loadRows();
Future<Integer> future = importService.importData(rows);
try {
Integer result = future.get(10, TimeUnit.SECONDS);
System.out.println("导入成功条数:" + result);
} catch (TimeoutException e) {
System.out.println("导入任务超时");
} catch (Exception e) {
System.out.println("导入任务失败");
}
}
异步方法如果抛出异常,调用方在 get 时会收到 ExecutionException,但真正的异常信息被包了一层,排查起来比较麻烦。更推荐使用 CompletableFuture,它可以通过 exceptionally 或 whenComplete 直接处理异常,代码可读性更好。示例:
@Async("notifyExecutor")
public CompletableFuture<String> sendNotify(String userId) {
try {
// 模拟发送站内信
return CompletableFuture.completedFuture("发送成功");
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
调用方可以使用 whenComplete 处理成功和失败两种情况:
sendNotify("1001").whenComplete((result, ex) -> {
if (ex != null) {
System.out.println("通知发送失败:" + ex.getMessage());
} else {
System.out.println("通知发送结果:" + result);
}
});
另外,对于无返回值异步方法抛出的异常,调用方无法直接捕获,需要配置 AsyncUncaughtExceptionHandler 统一记录日志。否则异常会被线程池吞掉,线上问题很难排查。
四、@Async 失效场景与调优建议
@Async 失效最常见的场景是同类内部调用。如果一个类里的普通方法调用了同一个类中标注 @Async 的方法,这次调用不会经过 Spring 代理,@Async 也就不会生效。原因是 Spring AOP 基于代理对象工作,而内部调用使用的是目标对象本身的方法引用。解决方法是将异步方法拆分到独立的 Service 中,或者通过 AopContext.currentProxy 获取代理对象后再调用。
另一个常见问题是方法被 private 或 static 修饰。private 方法无法被代理拦截,static 方法也不受实例代理影响,因此 @Async 标注在这两类方法上都会失效。此外,如果异步方法所在的类没有被 Spring 管理,或者 @EnableAsync 没有正确加载,同样不会生效。排查时可以先看日志中的线程名前缀,如果始终是 http-nio 线程而不是自己配置的 order-async 或 notify-async,基本可以确定没有走异步逻辑。
线程池参数调优没有一套固定公式,但可以遵循几个原则。核心线程数可以按 CPU 核数和任务类型估算:IO 密集型任务可以设置为核心线程数等于 CPU 核数乘以 2 或更高,CPU 密集型任务则接近 CPU 核数即可。队列容量不宜设置过大,过大的队列会掩盖处理能力不足的问题,导致任务长时间等待。拒绝策略要结合业务可容忍度选择,核心业务用 CallerRunsPolicy,非核心通知可以用 DiscardOldestPolicy 或 AbortPolicy。
最后整理一份可复用的调优检查清单:确认所有 @Async 方法显式指定线程池;确认线程池 Bean 调用了 initialize 方法;确认无返回值方法的异常配置了统一处理器;确认没有被同类内部调用;确认核心线程数、最大线程数和队列容量基于压测结果调整。做到这些,Spring Boot Async 的整合基本就能稳定运行,并且在接口响应时间优化上看到明显效果。

