在做 Web 开发的时候,大家多少都遇到过那种「接口一调用就要跑很久」的情况吧。比如导出一个几十万条数据的 Excel,或者调用第三方接口需要好几秒钟。传统 Servlet 模型下,一个请求线程要一直卡在那儿,结果就是线程池很快被占满,后面请求全挂。
Spring 其实早就给我们准备了几套异步工具,这里我就聊聊CompletableFuture(CF)和ResponseBodyEmitter(RBE),它俩搭起来用,可以撑起一整条从服务层到 Controller 的异步链路。
CompletableFuture:服务层的第一块砖
很多人第一反应是@Async,但CompletableFuture更灵活。它不仅能异步执行,还能做组合,比如先查数据库,再调用远程接口,最后合并结果。
举个例子,假设我们要同时拉用户信息和订单信息:
@Service
publicclass UserService {
@Async
public CompletableFuture<User> getUser(Long id) {
return CompletableFuture.supplyAsync(() -> {
// 模拟IO耗时
sleep(500);
returnnew User(id, "张三");
});
}
@Async
public CompletableFuture<List<Order>> getOrders(Long userId) {
return CompletableFuture.supplyAsync(() -> {
sleep(800);
return Arrays.asList(new Order("A123"), new Order("B456"));
});
}
private void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
}
}
这里返回的是CompletableFuture,Spring 能识别它,并且自动异步执行。好处就是:这两个任务并行跑,总耗时只要 800ms 左右。
ResponseBodyEmitter:控制层的“流式出口”
如果你只是想返回 JSON,CF 直接返回就行。但有时候,结果不是一次性拼好的,而是逐步产生的,比如「批量任务进度推送」「大文件分片返回」。这时候ResponseBodyEmitter就派上用场了。
@RestController
@RequestMapping("/export")
publicclass ExportController {
@Autowired
private UserService userService;
@GetMapping("/users")
public ResponseBodyEmitter exportUsers() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter();
CompletableFuture.runAsync(() -> {
try {
for (long i = 1; i <= 5; i++) {
User user = userService.getUser(i).join();
emitter.send("用户: " + user.getName() + "\n");
Thread.sleep(300);
}
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
}
你会发现这个接口一调用,前端不是等几秒才出结果,而是「一点点往下刷数据」,用户体验友好得多。
两者怎么串起来?
其实思路挺简单:
服务层用CompletableFuture去并行跑任务,充分利用多核和线程池。
控制层用ResponseBodyEmitter来把结果「一边算一边推」出去。
这样就形成了一个完整的链路:请求进来 异步执行 分批响应。线程池也不会一直被长任务堵死。
小坑提醒
线程池别偷懒:默认的@Async用的是SimpleAsyncTaskExecutor,没复用线程,生产环境分分钟爆。记得配个ThreadPoolTaskExecutor。
超时控制要加:长连接推数据,最好给 emitter 设置超时,或者结合网关超时参数。
异常兜底:CF 链式调用里,exceptionally/handle记得加上,否则一个异常直接炸全链路。
如果你遇到那种“耗时任务 + 大量并发 + 用户不能一直干等”的场景,可以大胆考虑 CF + RBE 这一套。写法不复杂,但能有效解决线程池压力,还能顺带提升前端体验。