Spring MVC的异步模式(ResponseBodyEmitter、SseEmitter、StreamingResponseBody)

SpringBoot异步接口实现:提高系统的吞吐量中,讲到使用Callable、WebAsyncTask、DeferredResult来提供异步接口,但是他们仅用于返回单个异步值------即返回一个值之后,就不能再返回值了。如果想生成多个异步值并将这些值写入响应,那么可以使用HTTP的流式传输。

在spring mvc中提供了3种http的流式传输:ResponseBodyEmitterSseEmitterStreamingResponseBody。只要在接口中,返回这三个对象中的一个,那么该接口就是异步接口。

特别说明一下:除了直接返回这三个对象外,返回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没法为这个接口单独设置超时时间,只能通过全局设置。这肯定不合理------因为这一个接口,改变了全局异步接口的超时时间。

相关推荐
止水编程 water_proof7 小时前
Java-HTTP响应以及HTTPS(下)
网络·网络协议·http
葡萄城技术团队7 小时前
迎接下一代 React 框架:Next.js 16 核心能力解读
javascript·spring·react.js
灰小猿8 小时前
Spring前后端分离项目时间格式转换问题全局配置解决
java·前端·后端·spring·spring cloud
0和1的舞者11 小时前
网络通信的奥秘:HTTP详解 (七)
服务器·网络·网络协议·http·okhttp·软件工程·1024程序员节
huangdengji13 小时前
基于openresty反向代理、dns劫持、实现对http请求、响应内容抓包
网络协议·http·openresty
知其然亦知其所以然14 小时前
这波AI太原生了!SpringAI让PostgreSQL秒变智能数据库!
后端·spring·postgresql
神仙别闹17 小时前
基于C语言 HTTP 服务器客户端的实验
服务器·c语言·http
zhaomx198917 小时前
Spring 事务管理 Transaction rolled back because it has been marked as rollback-only
数据库·spring
曹朋羽17 小时前
Spring EL 表达式
java·spring·el表达式
亚林瓜子19 小时前
Spring中的异步任务(CompletableFuture版)
java·spring boot·spring·async·future·异步