关注我的公众号:【编程朝花夕拾】,可获取首发内容。

01 引言
之前专门介绍过流式响应的数据的接收、发送以及使用SSE
由服务端推送数据的文章,但是要求前端必须使用EventSource
订阅实现。
有没有通过直接通过浏览器访问或者Fetch API
直接调用的方式呢?效果还能和ChatGPT
一样,实现打字机的效果呢?
当然有。Spring
框架在4.2
及以后引入了强大额异步响应特性,其中ResponseBodyEmitter
和StreamingResponseBody
是处理 HTTP 响应流式输出的两大核心利器,既不阻塞线程又不影响系统的相应能力。
我们一起了解一下吧!
02 StreamingResponseBody
全限定类名:
org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody
2.1 简介

StreamingResponseBody
是一个函数式接口,适用于需要直接向 HttpServletResponse
的输出流写入原始字节数据的场景。它提供了一种低层次、高效的方式来流式传输数据。
StreamingResponseBody
没有默认的实现,只有一个方法writeTo()
。将文件或者数据直接写入输出流中,可以通过输出流的flush()
方法,将数据刷出磁盘,减少内存的占用。
当我们需要将大量的数据或者文件响应给客户端时,由于处理耗时合作和内存的限制,我们就可以通过这种方式,逐步将数据响应给客户端,既可以提高用户体验又可以节省内存。
2.2 最佳实践
为了演示的方便,我们选择将文本内容逐步输出到浏览器端。
代码案例
间隔1s
将数据包传给客户端:
java
@GetMapping(value = "/srb")
public StreamingResponseBody srb(HttpServletResponse response) {
response.setContentType(MediaType.TEXT_EVENT_STREAM_VALUE);
StreamingResponseBody stream = os -> {
for (int i = 0; i < 5; i++) {
String data = "SRB 数据包【"+(i+1)+"】\n";
os.write(data.getBytes());
System.out.print(data);
// 刷出磁盘
os.flush();
try {
// 间隔1s
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
return stream;
}
因为没有默认的实现,这里使用了匿名内部类实现。
响应结果

03 ResponseBodyEmitter
全限定类名:
org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter
3.1 简介

ResponseBodyEmitter
是一个类,官方还给出了具体的使用案例。该类使用异步线程发送数据,最终需要使用complete()
结束数据的传输,否则就会一直等待的状态。
有没有发现我们之前介绍的SseEmitter
和ResponseBodyEmitter
有点类似,SseEmitter
其实是ResponseBodyEmitter
的子类。

SseEmitter
作为特殊的ResponseBodyEmitter
,专门支持Server-Sent Events
的。
ResponseBodyEmitter
的抽象层次更高。它不仅可以发送字节,更重要的是可以发送对象,这些对象会被配置的 HttpMessageConverter
转换后发送(如转换为 JSON
)。它允许你多次发送数据,非常适合服务器端事件(Server-Sent Events, SSE)或分块 JSON
。
3.2 最佳实践
代码案例
java
@GetMapping(value = "/rbe")
public ResponseBodyEmitter rbe(HttpServletResponse response) {
response.setContentType(MediaType.TEXT_EVENT_STREAM_VALUE);
ResponseBodyEmitter emitter = new ResponseBodyEmitter(0L);
CompletableFuture.runAsync(() -> {
for (int i = 0; i < 5; i++) {
String data = "RBE 数据包【"+(i+1)+"】\n";
try {
emitter.send(data);
System.out.print(data);
// 间隔1s
Thread.sleep(1000);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
emitter.complete();
});
return emitter;
}
响应结果

04 注意事项
两个类的案例中均有一行设置ContentType
类型的代码:
java
response.setContentType(MediaType.TEXT_EVENT_STREAM_VALUE);
设置响应类型为text/event-stream
,这一步很关键。否则直到全部的结果返回才会展示在浏览器端。
@GetMapping
的produces
属性并不能解决该问题,produces
指定返回的内容类型,仅当request
请求头中的(Accept)类型中包含该指定类型才返回。
05 番外
聊到异步线程的响应,又想到了另外一个Spring MVC
专门处理异步请求的类DeferredResult
,用法类似ResponseBodyEmitter
,不同的是DeferredResult
只能设置一次结果。
java
@GetMapping("/async")
public DeferredResult<String> async() {
DeferredResult<String> deferredResult = new DeferredResult<>();
CompletableFuture.runAsync(() -> {
System.out.println("DeferredResult触发,等待结果");
String data = "DeferredResult 数据包";
try {
// 间隔1s
Thread.sleep(2000);
deferredResult.setResult(data);
System.out.println("DeferredResult触发,完结");
} catch (Exception e) {
throw new RuntimeException(e);
}
});
return deferredResult;
}
执行结果发现:他会等待deferredResult.setResult(data)
发送数据之后才会结束响应。
