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

相关推荐
小天努力学java7 小时前
【面试系列】深入浅出 Spring
java·spring·面试
DashVector8 小时前
如何通过HTTP API插入Doc
数据库·人工智能·http·阿里云·向量检索
DashVector8 小时前
如何通过HTTP API分组检索Doc
服务器·数据库·http·数据库开发·数据库架构
唐 城10 小时前
Solon v3.0.5 发布!(Spring 可以退休了吗?)
java·spring·log4j
Mr.朱鹏10 小时前
操作002:HelloWorld
java·后端·spring·rabbitmq·maven·intellij-idea·java-rabbitmq
小小药11 小时前
009-spring-bean的实例化流程
java·数据库·spring
DashVector12 小时前
如何通过HTTP API插入或更新Doc
大数据·数据库·数据仓库·人工智能·http·数据库架构·向量检索
暗碳13 小时前
macrodroid通过http请求控制手机运行宏
http·智能手机
ChennyWJS13 小时前
03.HTTPS的实现原理-HTTPS的工作流程
网络·网络协议·http·https
ChennyWJS13 小时前
01.HTTPS的实现原理-HTTPS的概念
网络协议·http·https