Spring-Boot-Async-Response
在做 Web 开发的时候,大家多少都遇到过那种「接口一调用就要跑很久」的情况吧。比如导出一个几十万条数据的 Excel,或者调用第三方接口需要好几秒钟。传统 Servlet 模型下,一个请求线程要一直卡在那儿,结果就是线程池很快被占满,后面请求全挂。Spring 其实早就给我们准备了几套异步工具,这里我就聊聊 CompletableFuture(CF) 和 ResponseBodyEmitter(RBE),它俩搭起来用,可以撑起一整条从服务层到 Controller 的异步链路。
CompletableFuture:服务层的第一块砖
很多人第一反应是 @Async,但 CompletableFuture 更灵活。它不仅能异步执行,还能做组合,比如先查数据库,再调用远程接口,最后合并结果。
举个例子,假设我们要同时拉用户信息和订单信息:
@Service
public class UserService {
@Async
public CompletableFuture<User> getUser(Long id) {
return CompletableFuture.supplyAsync(() -> {
// 模拟IO耗时
sleep(500);
return new 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")
public class 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 这一套。写法不复杂,但能有效解决线程池压力,还能顺带提升前端体验。