Spring 实现 3 种异步流式接口,干掉接口超时烦恼

news2024/10/21 13:29:25

大家好,我是小富~

如何处理比较耗时的接口?

这题我熟,直接上异步接口,使用 CallableWebAsyncTaskDeferredResultCompletableFuture等均可实现。

但这些方法有局限性,处理结果仅返回单个值。在某些场景下,如果需要接口异步处理的同时,还持续不断地向客户端响应处理结果,这些方法就不够看了。

Spring 框架提供了多种工具支持异步流式接口,如 ResponseBodyEmitterSseEmitterStreamingResponseBody。这些工具的用法简单,接口中直接返回相应的对象或泛型响应实体 ResponseEntity<xxxx>,如此这些接口就是异步的,且执行耗时操作亦不会阻塞 Servlet 的请求线程,不影响系统的响应能力。

下面将逐一介绍每个工具的使用及其应用场景。

ResponseBodyEmitter

ResponseBodyEmitter适应适合于需要动态生成内容并逐步发送给客户端的场景,例如:文件上传进度、实时日志等,可以在任务执行过程中逐步向客户端发送更新。

举个例子,经常用GPT你会发现当你提问后,得到的答案并不是一次性响应呈现的,而是逐步动态显示。这样做的好处是,让你感觉它在认真思考,交互体验比直接返回完整答案更为生动和自然。

使用ResponseBodyEmitter来实现下这个效果,创建 ResponseBodyEmitter 发送器对象,模拟耗时操作逐步调用 send 方法发送消息。

注意:ResponseBodyEmitter 的超时时间,如果设置为 0-1,则表示连接不会超时;如果不设置,到达默认的超时时间后连接会自动断开。其他两种工具也是同样的用法,后边不在赘述了

@GetMapping("/bodyEmitter")
public ResponseBodyEmitter handle() {
    // 创建一个ResponseBodyEmitter,-1代表不超时
    ResponseBodyEmitter emitter = new ResponseBodyEmitter(-1L);
    // 异步执行耗时操作
    CompletableFuture.runAsync(() -> {
        try {
            // 模拟耗时操作
            for (int i = 0; i < 10000; i++) {
                System.out.println("bodyEmitter " + i);
                // 发送数据
                emitter.send("bodyEmitter " + i + " @ " + new Date() + "\n");
                Thread.sleep(2000);
            }
            // 完成
            emitter.complete();
        } catch (Exception e) {
            // 发生异常时结束接口
            emitter.completeWithError(e);
        }
    });
    return emitter;
}

实现代码非常简单。通过模拟每2秒响应一次结果,请求接口时可以看到页面数据在动态生成。效果与 GPT 回答基本一致。

SseEmitter

SseEmitterResponseBodyEmitter 的一个子类,它同样能够实现动态内容生成,不过主要将它用在服务器向客户端推送实时数据,如实时消息推送、状态更新等场景。在我之前的一篇文章 我有 7种 实现web实时消息推送的方案 中详细介绍了 Server-Sent Events (SSE) 技术,感兴趣的可以回顾下。

SSE在服务器和客户端之间打开一个单向通道,服务端响应的不再是一次性的数据包而是text/event-stream类型的数据流信息,在有数据变更时从服务器流式传输到客户端。

整体的实现思路有点类似于在线视频播放,视频流会连续不断的推送到浏览器,你也可以理解成,客户端在完成一次用时很长(网络不畅)的下载。

客户端JS实现,通过一次 HTTP 请求建立连接后,等待接收消息。此时,服务端为每个连接创建一个 SseEmitter 对象,通过这个通道向客户端发送消息。

<body>
<div id="content" style="text-align: center;">
    <h1>SSE 接收服务端事件消息数据</h1>
    <div id="message">等待连接...</div>
</div>
<script>
    let source = null;
    let userId = 7777

    function setMessageInnerHTML(message) {
        const messageDiv = document.getElementById("message");
        const newParagraph = document.createElement("p");
        newParagraph.textContent = message;
        messageDiv.appendChild(newParagraph);
    }

    if (window.EventSource) {
        // 建立连接
        source = new EventSource('http://127.0.0.1:9033/subSseEmitter/'+userId);
        setMessageInnerHTML("连接用户=" + userId);
        /**
         * 连接一旦建立,就会触发open事件
         * 另一种写法:source.onopen = function (event) {}
         */
        source.addEventListener('open', function (e) {
            setMessageInnerHTML("建立连接。。。");
        }, false);
        /**
         * 客户端收到服务器发来的数据
         * 另一种写法:source.onmessage = function (event) {}
         */
        source.addEventListener('message', function (e) {
            setMessageInnerHTML(e.data);
        });
    } else {
        setMessageInnerHTML("你的浏览器不支持SSE");
    }
</script>
</body>

在服务端,我们将 SseEmitter 发送器对象进行持久化,以便在消息产生时直接取出对应的 SseEmitter 发送器,并调用 send 方法进行推送。

private static final Map<String, SseEmitter> EMITTER_MAP = new ConcurrentHashMap<>();

@GetMapping("/subSseEmitter/{userId}")
public SseEmitter sseEmitter(@PathVariable String userId) {
    log.info("sseEmitter: {}", userId);
    SseEmitter emitterTmp = new SseEmitter(-1L);
    EMITTER_MAP.put(userId, emitterTmp);
    CompletableFuture.runAsync(() -> {
        try {
            SseEmitter.SseEventBuilder event = SseEmitter.event()
                    .data("sseEmitter" + userId + " @ " + LocalTime.now())
                    .id(String.valueOf(userId))
                    .name("sseEmitter");
            emitterTmp.send(event);
        } catch (Exception ex) {
            emitterTmp.completeWithError(ex);
        }
    });
    return emitterTmp;
}

@GetMapping("/sendSseMsg/{userId}")
public void sseEmitter(@PathVariable String userId, String msg) throws IOException {
    SseEmitter sseEmitter = EMITTER_MAP.get(userId);
    if (sseEmitter == null) {
        return;
    }
    sseEmitter.send(msg);
}

接下来向 userId=7777 的用户发送消息,127.0.0.1:9033/sendSseMsg/7777?msg=欢迎关注–>程序员小富,该消息可以在页面上实时展示。

而且SSE有一点比较好,客户端与服务端一旦建立连接,即便服务端发生重启,也可以做到自动重连

StreamingResponseBody

StreamingResponseBody 与其他响应处理方式略有不同,主要用于处理大数据量或持续数据流的传输,支持将数据直接写入OutputStream

例如,当我们需要下载一个超大文件时,使用 StreamingResponseBody 可以避免将文件数据一次性加载到内存中,而是持续不断的把文件流发送给客户端,从而解决下载大文件时常见的内存溢出问题。

接口实现直接返回 StreamingResponseBody 对象,将数据写入输出流并刷新,调用一次flush就会向客户端写入一次数据。

@GetMapping("/streamingResponse")
public ResponseEntity<StreamingResponseBody> handleRbe() {

    StreamingResponseBody stream = out -> {
        String message = "streamingResponse";
        for (int i = 0; i < 1000; i++) {
            try {
                out.write(((message + i) + "\r\n").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);
}

demo这里输出的是简单的文本流,如果是下载文件那么转换成文件流效果是一样的。

总结

这篇介绍三种实现异步流式接口的工具,算是 Spring 知识点的扫盲。使用起来比较简单,没有什么难点,但它们在实际业务中的应用场景还是很多的,通过这些工具,可以有效提高系统的性能和响应能力。

文中 Demo Github 地址:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot101/%E9%80%9A%E7%94%A8%E5%8A%9F%E8%83%BD/springboot-streaming

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2213898.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

每日OJ题_牛客_小乐乐改数字_模拟_C++_Java

目录 牛客_小乐乐改数字_模拟 题目解析 C代码 Java代码 牛客_小乐乐改数字_模拟 小乐乐改数字_牛客题霸_牛客网 (nowcoder.com) 描述&#xff1a; 小乐乐喜欢数字&#xff0c;尤其喜欢0和1。他现在得到了一个数&#xff0c;想把每位的数变成0或1。如果某一位是奇数&#…

Python 工具库每日推荐【PyAutoGUI】

文章目录 引言Python桌面自动化库的重要性今日推荐:PyAutoGUI工具库主要功能:使用场景:安装与配置快速上手示例代码代码解释实际应用案例案例:自动化图像识别和点击案例分析高级特性失败安全机制相对坐标操作扩展阅读与资源优缺点分析优点:缺点:总结【 已更新完 Python工…

如何远程查看孩子的电脑使用?

越来越多的家庭拥有了多台电脑和智能设备。家长们往往没有办法时刻陪伴在孩子身边&#xff0c;监督他们的上网行为。而远程控制电脑可以帮助家长在任何时间、任何地点对孩子的电脑进行监控和管理&#xff0c;确保他们能够安全、健康地使用互联网。 通过远程控制软件&#xff0c…

锦锐科技CA51F7系列笔记

1、PWM &#xff08;1&#xff09;芯片一共有6个PWM通道&#xff08;PWM0~PWM5&#xff09; &#xff08;2&#xff09;IRCH时钟&#xff08;IRCH 频率为 16MHz&#xff09; &#xff08;3&#xff09;IRCL时钟&#xff08;IRCL频率为100KHz&#xff09; &#xff08;4&…

JavaScript 命令模式实战:打造可撤销的操作命令

一. 前言 在前端开发中&#xff0c;命令模式&#xff08;Command Pattern&#xff09;作为一种行为型设计模式&#xff0c;可以帮助我们将请求封装成一个对象&#xff0c;从而实现调用对象和执行对象之间的解耦&#xff0c;方便扩展和修改。 本文将和大家分享 JavaScript 中的…

实战子网掩码划分问题

拓扑要求总部有2个业务段&#xff0c;分部也有3个业务 还有互联地址段&#xff0c;还有管理地址段&#xff01;&#xff01;&#xff01;&#xff01; 23117 个段 192.168.1.0/24 怎么划分呢&#xff1f; 1 根据要求划分地址段 192.168.1.0/24 要划分7个网段那就…

linux------缓冲区与C库的原理

前言 一、缓冲区 缓冲区的作用是提高效率&#xff0c;因为将数据写入到设备&#xff0c;是需要调用系统接口的&#xff0c;如果每次写入缓冲区的数据就调用一次系统调用&#xff0c;涉及到系统调用这时操作系统就会介入&#xff0c;用户态转为内核态&#xff0c;这个过程需要时…

自然语言到 SQL 的曙光:我们准备好了吗?

发布于&#xff1a;2024 年 10 月 08 日 各位读者&#xff0c;国庆假期已过&#xff0c;我们打工人要开启奋斗新征程了&#xff0c;今天小编也是刚上班假期综合征还没过去&#xff0c;就被抓过来读论文&#xff0c;还好我在假期没闲着&#xff0c;整理了几篇关于 NL2SQL 的最新…

周易解读:两仪01

两 仪01 上一节里面&#xff0c;我们讲解了太极的知识。关于太极呢&#xff0c;它是可以有着多方面的含义。在这里呢&#xff0c;我们主要地&#xff0c;选取它的宇宙生成中的一种含义&#xff0c;认为太极是有所萌动&#xff0c;有所积累&#xff0c;但是呢&#xff0c;就是…

【YOLOv11】实战一:在LabVIEW 中使用OpenVINO实现YOLOv11

‍‍&#x1f3e1;博客主页&#xff1a; virobotics(仪酷智能)&#xff1a;LabVIEW深度学习、人工智能博主 &#x1f384;所属专栏&#xff1a;『LabVIEW深度学习实战』 &#x1f4d1;推荐文章&#xff1a;『LabVIEW人工智能深度学习指南』 &#x1f37b;本文由virobotics(仪酷…

单体应用、SOA 和微服务架构的区别

在软件架构的发展历程中&#xff0c;单体应用、面向服务架构&#xff08;SOA&#xff09;和微服务架构都有着各自的特点和适用场景。了解它们之间的区别&#xff0c;有助于我们在不同的项目需求下做出更合适的架构选择。 一、单体应用 &#xff08;一&#xff09;定义与特点 …

DAY8 Final等

Final关键字 final修饰静态变量&#xff0c;这个变量今后被称为常量&#xff0c; 可以记住一个固定值&#xff0c;并且程序中不能修改了&#xff0c;通常这个值作为系统的配置信息。常量的名称&#xff0c;建议全部大写&#xff0c;多个单词用下划线连接。 public static final…

PaddleOCR的包装生产日期识别_随记1

一、前言 项目难点 没有训练数据图像质量层次不齐: 角度倾斜、图片模糊、光照不足、过曝等问题严重 二.、环境搭建 本任务基于Aistudio完成, 具体环境如下&#xff1a; 操作系统: LinuxPaddleOCR: PaddleOCR_2.7text_renderer: masterpaddlepaddle-gpu2.6.0cuda版本是11.8,…

量化投资学习

1:投资定义就是付出一定的代价&#xff0c;期望能够得到一定汇报&#xff0c;可能会出现没有回报 2&#xff1a;投资分析流派 2.1:宏观策略分析法&#xff1a;从宏观经济大方向入手&#xff0c;再应用到具体股票也叫自上而下的研究方法&#xff0c;需要理解这个趋势的核心驱动…

TS中的接口、泛型、自定义类型 与vue3的使用

泛型的使用场景&#xff1a; 当我们定义了一个对象时&#xff0c;在多次传值的过程中&#xff0c;其对象中的属性值被更改了&#xff0c;以至于后期拿不到我们需要的数据。 举个例子&#xff0c;当我们使用person.name时&#xff0c;是可以使用的。但是多次传递之后&#xff…

讯方·智汇云校华为ICT大赛赛前辅导直播安排

华为ICT大赛赛前辅导直播安排 网络赛道在“智汇云校”视频号上观看。 直播时间&#xff1a; 网络&#xff1a;2024.10.14-10.15-10.17-10.18-10.21-10.23-10.25-10.28-10.29-10.30-11.1-11.4-11.5-11.6&#xff0c;每晚19&#xff1a;30-22&#xff1a;00 安全&#xff1a;2024…

【Next.js 入门教程系列】05-数据库

原文链接 CSDN 的排版/样式可能有问题&#xff0c;去我的博客查看原文系列吧&#xff0c;觉得有用的话&#xff0c; 给我的库点个star&#xff0c;关注一下吧 上一篇【Next.js 入门教程系列】04-构造 API 数据库 本篇包括以下内容: Setting up PrismaDefining data modelsCreat…

pinia学习笔记(1.0)

首先贴出官网地址&#xff1a;开始 | Pinia pinia作为Vue3项目中常用的状态管理工具&#xff0c;正逐渐取代vuex&#xff0c;现从0到1自己搭建pinia仓库。 首先&#xff0c;安装pinia&#xff0c;使用包管理器工具&#xff08;npm,pnpm,yarn,Bun等都可以&#xff09; 安装成…

QGIS如何在正射影像中获得绘制线段的长度信息||QGIS如何获取绘制点的经纬度(如何生成WKT属性)并且导出为CSV文件

QGIS如何在正射影像中获得绘制线段的长度信息 前置条件&#xff1a; 导入正射影像和绘制完需要计算长度的绘制线段后 1.检查菜单栏是否有数据处理&#xff08;processing&#xff09;按键&#xff0c;如果没有的话请根据以下链接打开processing工具箱&#xff1a;https://bl…

微服务架构 --- Nacos的项目实战操作

目录 一.什么是Nacos&#xff1f; 二.什么是注册中心&#xff1f; 1.注册中心的定义&#xff1a; 2.为什么需要使用注册中心&#xff1f; 3.注册中心原理&#xff1a; 三.Nacos的使用&#xff1a; 1.安装与启动Nacos&#xff1a; 2.集成 Nacos 服务注册与发现&#xff…