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没法为这个接口单独设置超时时间,只能通过全局设置。这肯定不合理------因为这一个接口,改变了全局异步接口的超时时间。

相关推荐
尘浮生2 小时前
Java项目实战II基于Spring Boot的宠物商城网站设计与实现
java·开发语言·spring boot·后端·spring·maven·intellij-idea
doc_wei3 小时前
Java小区物业管理系统
java·开发语言·spring boot·spring·毕业设计·课程设计·毕设
荆州克莱3 小时前
杨敏博士:基于法律大模型的智能法律系统
spring boot·spring·spring cloud·css3·技术
自身就是太阳4 小时前
Maven的高级特性
java·开发语言·数据库·后端·spring·maven
小堃学编程6 小时前
计算机网络(六) —— http协议详解
网络协议·计算机网络·http
人类群星闪耀时6 小时前
运维的基本概念:基础的网络协议(TCP/IP, HTTP/HTTPS)
运维·网络协议·http
readmancynn7 小时前
XML_Tomcat_HTTP
xml·http·tomcat
胡耀超7 小时前
MyBatis-Plus插入优化:降低IO操作的策略与实践
sql·spring·mybatis
api7710 小时前
1688商品详情API返回值中的售后保障与服务信息
java·服务器·前端·javascript·python·spring·pygame
自身就是太阳10 小时前
深入理解 Spring 事务管理及其配置
java·开发语言·数据库·spring