跳到主要内容

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 来把结果「一边算一边推」出去。

这样就形成了一个完整的链路:请求进来 → 异步执行 → 分批响应。线程池也不会一直被长任务堵死。

小坑提醒

  1. 线程池别偷懒:默认的 @Async 用的是 SimpleAsyncTaskExecutor,没复用线程,生产环境分分钟爆。记得配个 ThreadPoolTaskExecutor
  2. 超时控制要加:长连接推数据,最好给 emitter 设置超时,或者结合网关超时参数。
  3. 异常兜底:CF 链式调用里,exceptionally / handle 记得加上,否则一个异常直接炸全链路。

如果你遇到那种 “耗时任务 + 大量并发 + 用户不能一直干等” 的场景,可以大胆考虑 CF + RBE 这一套。写法不复杂,但能有效解决线程池压力,还能顺带提升前端体验。