在SpringBoot异步接口实现:提高系统的吞吐量中,讲到使用Callable、WebAsyncTask、DeferredResult来提供异步接口,但是他们仅用于返回单个异步值------即返回一个值之后,就不能再返回值了。如果想生成多个异步值并将这些值写入响应,那么可以使用HTTP的流式传输。
在spring mvc中提供了3种http的流式传输:ResponseBodyEmitter 、SseEmitter 、StreamingResponseBody。只要在接口中,返回这三个对象中的一个,那么该接口就是异步接口。
特别说明一下:除了直接返回这三个对象外,返回ResponseEntity(泛型参数为三者中的一个)也是一样的效果,并且推荐使用ResponseEntity,毕竟可以设置很多http相关的信息,比如:header、contentType等等。
ResponseBodyEmitter
假如有这样一个场景:前端发起一个请求查询某些商品信息,后端根据条件查找到结果之后返回给前端。假如后端查找这些商品信息只能一条一条的找,那么找完所有的结果会耗费很长时间,并且前端也不一定想要全部的数据。就像我们问chartgpt一样,有时我们并不想看完整的答案,可能他回答到一半,我们就知道了(chartgpt的回答结果都是一点点蹦出来的,不是一下全部展示出来)。那么这个场景最好的解决方案就是,后端每找到一条数据之后,就给前端展示,前端如果不想要了,就停止接收。
这里我们先写一个接口,模拟前端请求:
java
@GetMapping("/events")
public ResponseEntity<ResponseBodyEmitter> handle() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter(0L);
gEmitter = emitter;
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(emitter);
}
在这个接口中,返回的是ResponseEntity<ResponseBodyEmitter>
,当然也可以直接返回ResponseBodyEmitter
。我用一个全局变量gEmitter
保存了这个返回的ResponseBodyEmitter。
另外,在new ResponseBodyEmitter(0L)中,这个0或者-1代表永不超时。如果在实际使用中,发现超时等问题,请检查服务器配置、网关、nginx等。
现在再写一个接口,模拟查询商品的操作:
java
@GetMapping("/sendEmitter")
public void sendEmitter(@RequestParam("str") String str) throws IOException {
if (Objects.equals("end", str)) {
gEmitter.complete();
} else {
gEmitter.send(str);
}
}
这个接口明显是另一个线程来处理的,在这个接口中,我们获取到之前保存的ResponseBodyEmitter对象。然后通过这个对象发送信息。我们发送消息的频率就类似查询每个符合条件商品的间隔时间。
现在浏览器访问:/events
。然后用postman随意间隔的访问/sendEmitter
接口,此时会发现每访问一次,访问/events
的浏览器页面就会显示出相应的结果。
也就是说,ResponseBodyEmitter可以跨线程的多次向前端输出结果。
SseEmitter
SseEmitter是ResponseBodyEmitter的子类,提供对服务器发送事件的支持,其中从服务器发送的事件根据 W3C SSE 规范进行格式化。两者的使用方式差不多,我在另一篇文章中,有详细的案例说明:SSE:后端向前端发送消息(springboot SseEmitter)
StreamingResponseBody
看官网中说:有时,想绕过消息转换并直接流式传输到响应OutputStream (例如,用于文件下载)很有用。您可以使用StreamingResponseBody返回值类型来执行此操作。
例如如下接口,就可以实现前端每间隔1秒输出一个fyk_test_x,x为数字:
java
@GetMapping("/stream2")
public ResponseEntity<StreamingResponseBody> stream() {
StreamingResponseBody stream = out -> {
String message = "fyk_test_";
for (int i = 0; i < message.length(); i++) {
try {
out.write((message + i).getBytes());
out.write("\r\n".getBytes());
//调用一次flush就会像前端写入一次数据
out.flush();
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(stream);
}
说个题外话,在实际的使用中,我还真没有用过StreamingResponseBody。如果说是信息分段传输,那么另外两个都可以实现了。至于官方说的大文件下载,spring也有FileSystemResource对象来传输,也不会内存溢出,甚至直接以流的方式,向response.getOutputStream()写入文件流也没有问题。
另外,在测试的时候,发现StreamingResponseBody有个缺点:无法很好的修改超时时间导致超时报错。
试想一下,下载一个大文件,那么这个时间怎么定呢?同步接口下载没有超时时间一说,但是异步接口默认的超时时间一般是30秒(关于SpringBoot MVC接口超时时间的分析),30秒没有下载完,就是报错。使用StreamingResponseBody没法为这个接口单独设置超时时间,只能通过全局设置。这肯定不合理------因为这一个接口,改变了全局异步接口的超时时间。