你可以使用 DeferredResult 和 Callable 来实现一个单一的异步返回值。如果你想产生多个异步值,并让这些值写入响应中,该怎么办?本节描述了如何做到这一点。

Objects / 对象流

你可以使用 ResponseBodyEmitter 的返回值来产生一个对象流,其中每个对象都被 HttpMessageConverter 序列化并写入响应中,如下例所示:

  1. @GetMapping("/events")
  2. public ResponseBodyEmitter handle() {
  3. ResponseBodyEmitter emitter = new ResponseBodyEmitter();
  4. // 将 emitter 保存在其他地方
  5. return emitter;
  6. }
  7. // 在其他线程中调用
  8. emitter.send("Hello once");
  9. // 在其他线程中调用发送数据
  10. emitter.send("Hello again");
  11. // 并在某一时刻完成
  12. emitter.complete();

:::tips 需要注意的是:这种方式其实就相当于是文件流那种样子,可以持续的往前端写消息(但是有一个前提,达到了某些配置阀值,应该是 buffer 的大小,比如你可以将第一个 send 发送上千个字符,然后休眠 5 秒,你就能看到这些信息会先在浏览器显示出来,5 秒后第二个 send 才会显示出来) :::

你也可以使用 ResponseBodyEmitter 作为 ResponseEntity 中的主体,让你自定义响应的状态和头信息。

当发射器抛出一个 IOException 时(例如,如果远程客户端离开了),应用程序不负责清理连接,不应该调用 emitter.complete 或emitter.completeWithError。相反,servlet 容器会自动发起一个 AsyncListener 错误通知,Spring MVC 在其中进行 completeWithError 调用。这个调用反过来又向应用程序执行最后的 ASYNC 派发,在此期间,Spring MVC 会调用配置的异常解析器并完成请求。

SSE / 服务器发送事件流

SseEmitter(ResponseBodyEmitter 的一个子类)提供了对 服务器发送事件 的支持,从服务器发送的事件是按照 W3C SSE 规范格式化的。为了从控制器中产生一个 SSE 流,返回 SseEmitter,如下面的例子所示:

  1. @GetMapping(path="/events", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
  2. public SseEmitter handle() {
  3. SseEmitter emitter = new SseEmitter();
  4. // 将 emitter 保存在其他地方
  5. return emitter;
  6. }
  7. // 在其他线程中调用
  8. emitter.send("Hello once");
  9. // and again later on
  10. emitter.send("Hello again");
  11. // and done at some point
  12. emitter.complete();

注意了:在根本上与前面的是有区别的,浏览器端出现了 EventStream 标签,并且不再有写 buffer 大小的自动刷新限制了,每次 send 都会往前端立即写出消息。下面是我自己测试的一个例子
image.png
虽然 SSE 是流向浏览器的主要选择,但请注意,Internet Explorer 不支持服务器发送事件。可以考虑使用 Spring 的 WebSocket 消息传递与 SockJS fallback 传输(包括 SSE),其目标是广泛的浏览器。

Raw Data / 原始数据流

有时,绕过消息转换,直接流向响应的 OutputStream 是很有用的(例如,对于文件下载)。你可以使用 StreamingResponseBody 返回值类型来做到这一点,正如下面的例子所示:

  1. @GetMapping("/download")
  2. public StreamingResponseBody handle() {
  3. return new StreamingResponseBody() {
  4. @Override
  5. public void writeTo(OutputStream outputStream) throws IOException {
  6. // write...
  7. }
  8. };
  9. }

你可以使用 StreamingResponseBody 作为 ResponseEntity 中的主体,来定制响应的状态和头信息。