当前位置: 首页 > news >正文

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

大家好,我是小富~

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

这题我熟,直接上异步接口,使用 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 = 7777function 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

相关文章:

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

大家好&#xff0c;我是小富&#xff5e; 如何处理比较耗时的接口&#xff1f; 这题我熟&#xff0c;直接上异步接口&#xff0c;使用 Callable、WebAsyncTask 和 DeferredResult、CompletableFuture等均可实现。 但这些方法有局限性&#xff0c;处理结果仅返回单个值。在某…...

字节 HLLM 论文阅读

github连接&#xff1a;https://github.com/bytedance/HLLM 探讨问题&#xff1a; 推荐LLM的三个关键问题&#xff1a; LLM预训练权重通常被认为是对世界知识的概括&#xff0c;其对于推荐系统的价值&#xff1f;对推荐任务进行微调的必要性&#xff1f;LLM是否可以在推荐系统…...

Chromium html<iframe>对应c++接口定义

HTML <iframe> 标签 使用 <iframe> 标签 在当前 HTML 文档中嵌入另一个文档&#xff1a; <!DOCTYPE html> <html> <body><h1>iframe 元素</h1><iframe src"https://www.w3school.com.cn" title"W3School 在线教…...

Vue详细入门(语法【三】)

今天滴的学习目标&#xff01;&#xff01;&#xff01; Vue组件是什么&#xff1f;组件的特性和优势Vue3计算属性Vue3监听属性 在前面Vue详细入门&#xff08;语法【一】——【二】&#xff09;当中我们学习了Vue有哪些指令&#xff0c;它的核心语法有哪些&#xff1f;今天我们…...

快速构建SpringBoot项目

快速构建SpringBoot项目 下文将简述如何快速构建一个SpringBoot项目&#xff0c;使用SpringData JPA实现持久层访问&#xff0c;集成lombok、swagger2及集成thymeleaf进行页面展示。 准备环境&#xff1a; JDK版本&#xff1a;jdk17 IntelliJ IDEA版本&#xff1a; 2023.2.7…...

架构设计笔记-14-云原生架构设计理论与实践

知识要点 云原生&#xff08;Cloud Native&#xff09;架构原则&#xff1a; 服务化原则&#xff1a;通过微服务架构&#xff0c;小服务&#xff08;MiniService&#xff09;架构把不同生命周期的模块分离出来&#xff0c;分别进行业务迭代&#xff0c;避免迭代频繁模块被慢速…...

leetcode hot100 之【LeetCode 206. 反转链表】 java实现

LeetCode 206. 反转链表 题目描述 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1]示例 2&#xff1a; 输入&#xff1a;head [1,2] 输出&#x…...

基于Spring Cloud的电商系统设计与实现——用户与商品模块的研究(上)

操作系统&#xff1a;Windows Java开发包&#xff1a;JDK1.8 项目管理工具&#xff1a;Maven3.6.0 项目开发工具&#xff1a;IntelliJIDEA 数据库&#xff1a;MySQL Spring Cloud版本&#xff1a;Finchley.SR2 Spring Boot版本&#xff1a;2.0.6.RELEASE 目录 用户模块—user-…...

Spring Boot + Vue 前后端分离项目总结:解决 CORS 和 404 问题

Spring Boot Vue 前后端分离项目总结&#xff1a;解决 CORS 和 404 问题 在进行前后端分离的项目开发中&#xff0c;我们遇到了几个关键问题&#xff1a;跨域问题 (CORS) 和 404 路由匹配错误。以下是这些问题的详细分析和最终的解决方案。 问题描述 跨域请求被阻止 (CORS) 当…...

JVM篇(学习预热 - JVM正式展开 - (实战课程学习总结))(持续更新迭代)

目录 感觉也看了这么多&#xff0c;说一些乱七八糟的内容&#xff0c;完全没有实质的收获&#xff0c;那么现在让我们正式来预热下JVM 吧&#xff1f; 一、程序的执行方式 二、为什么使用 JVM 三、字节码和机器码的区别 四、JDK、JRE与JVM的关系 五、OracleJDK和OpenJDK …...

WebGL编程指南 - 入门续

相关内容&#xff1a;在attribute变量传递参数的基础上&#xff0c;通过JavaScript获取鼠标事件的坐标&#xff0c;再经过坐标转换传递给attribute变量&#xff1b;Web颜色缓冲区每次绘制之后都会重置相关函数&#xff1a;JavaScript鼠标事件onmousedown/onmouseup/onclick htm…...

EPS导出DWG存在地物缺失或者没有编码属性的情况

问题描述 使用eps导出dwg时&#xff0c;打开dwg会发现部分地物缺失或者没有编码属性。 这里就是一片空白&#xff1a; 解决办法 1 查看eps的图层信息&#xff0c;发现图层没有对应上&#xff0c;故此地物编码也是没有的。 2 可以右键全选本编码对象&#xff0c;实现批量快…...

跨境业务收款难?Zoho Books来帮忙

外贸跨境企业应收账款管理繁琐&#xff0c;ZohoBooks财务管理软件提供自动化解决方案&#xff0c;简化开票、跟进、收款和账户更新流程&#xff0c;提升效率和准确性&#xff0c;助力企业优化现金流和财务健康。 什么是应收账款&#xff1f; 应收账款指的是企业在提供商品或服…...

深入解析 Harris 角点检测算法:从孔径问题到响应函数的完整推导

在图像处理中&#xff0c;角点是非常重要的特征。为了快速、准确地检测角点&#xff0c;Harris 提出了 Harris 角点检测算法&#xff0c;它基于局部窗口内图像梯度的变化来判断角点。本文将从最基础的孔径问题&#xff08;Aperture Problem&#xff09;入手&#xff0c;通过泰勒…...

抖音视频制作怎么暂停画面,抖音视频怎么让它有暂停的效果

千万别滥用视频特效&#xff0c;不然它能毁掉你的抖音作品。在创作过程中&#xff0c;应尽量使用类似暂停画面、隐形字幕这样的视觉特效&#xff0c;可以显著提高作品的视觉体验。增强视频表现力的同时&#xff0c;也不会让画面看起来过于夸张。有关抖音视频制作怎么暂停画面的…...

Android GPIO方式解码红外数据

1 红外遥控协议 1.1 基本概念 1&#xff09;NEC协议&#xff0c;采用PWM方式调制。38KHz载波&#xff0c;一般是由引导码地址码地址反码数据数据反码构成。 遥控接收头端收到的信号为&#xff1a;逻辑1是560us低1680us高&#xff0c;逻辑0是560us低560us高。 地址&#xff1a;u…...

基于SpringBoot+Vue的益农智慧服务平台【提供源码+答辩PPT+参考文档+项目部署】

一、项目技术架构&#xff1a; 本项目是一款SpringBoot益农平台的设计与实现。 该SpringBootVue的益农平台的设计与实现&#xff0c;后端采用SpringBoot架构&#xff0c;前端采用VueElementUI实现页面的快速开发&#xff0c;并使用关系型数据库MySQL存储系统运行数据。本系统分…...

基于springboot的在线考试与学习交流网页

作者&#xff1a;计算机学长阿伟 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、ElementUI等&#xff0c;“文末源码”。 系统展示 【2024最新】基于JavaSpringBootVueMySQL的&#xff0c;前后端分离。 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;…...

JS异步编程进阶(二):rxjs与Vue、React、Angular框架集成及跨框架状态管理实现原理

在现代前端开发中&#xff0c;异步操作已经成为不可或缺的一部分。无论是处理网络请求、响应用户输入&#xff0c;还是监听外部事件&#xff0c;异步编程模式始终占据重要位置。而RxJS作为功能强大的异步编程库&#xff0c;广泛应用于多个框架之中&#xff0c;如Vue、React、An…...

nginx web代理

目录 1.nginx的简单介绍 2.正向代理的应用场景 2.1做访问控制 2.2审计 2.3负载分散 2.4隐私保护和匿名性 3.反向代理的应用场景如下 3.1.负载均衡 2.缓存静态内容 3.压缩和优化内容 4.提供故障转移 5.安全性和匿名性 4.正向代理 4.1web端 4.2lb01代理服务器…...

微信小程序之bind和catch

这两个呢&#xff0c;都是绑定事件用的&#xff0c;具体使用有些小区别。 官方文档&#xff1a; 事件冒泡处理不同 bind&#xff1a;绑定的事件会向上冒泡&#xff0c;即触发当前组件的事件后&#xff0c;还会继续触发父组件的相同事件。例如&#xff0c;有一个子视图绑定了b…...

深入理解JavaScript设计模式之单例模式

目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式&#xff08;Singleton Pattern&#…...

Golang dig框架与GraphQL的完美结合

将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用&#xff0c;可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器&#xff0c;能够帮助开发者更好地管理复杂的依赖关系&#xff0c;而 GraphQL 则是一种用于 API 的查询语言&#xff0c;能够提…...

uniapp微信小程序视频实时流+pc端预览方案

方案类型技术实现是否免费优点缺点适用场景延迟范围开发复杂度​WebSocket图片帧​定时拍照Base64传输✅ 完全免费无需服务器 纯前端实现高延迟高流量 帧率极低个人demo测试 超低频监控500ms-2s⭐⭐​RTMP推流​TRTC/即构SDK推流❌ 付费方案 &#xff08;部分有免费额度&#x…...

学校时钟系统,标准考场时钟系统,AI亮相2025高考,赛思时钟系统为教育公平筑起“精准防线”

2025年#高考 将在近日拉开帷幕&#xff0c;#AI 监考一度冲上热搜。当AI深度融入高考&#xff0c;#时间同步 不再是辅助功能&#xff0c;而是决定AI监考系统成败的“生命线”。 AI亮相2025高考&#xff0c;40种异常行为0.5秒精准识别 2025年高考即将拉开帷幕&#xff0c;江西、…...

高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数

高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数 在软件开发中,单例模式(Singleton Pattern)是一种常见的设计模式,确保一个类仅有一个实例,并提供一个全局访问点。在多线程环境下,实现单例模式时需要注意线程安全问题,以防止多个线程同时创建实例,导致…...

LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf

FTP 客服管理系统 实现kefu123登录&#xff0c;不允许匿名访问&#xff0c;kefu只能访问/data/kefu目录&#xff0c;不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...

GruntJS-前端自动化任务运行器从入门到实战

Grunt 完全指南&#xff1a;从入门到实战 一、Grunt 是什么&#xff1f; Grunt是一个基于 Node.js 的前端自动化任务运行器&#xff0c;主要用于自动化执行项目开发中重复性高的任务&#xff0c;例如文件压缩、代码编译、语法检查、单元测试、文件合并等。通过配置简洁的任务…...

LangChain知识库管理后端接口:数据库操作详解—— 构建本地知识库系统的基础《二》

这段 Python 代码是一个完整的 知识库数据库操作模块&#xff0c;用于对本地知识库系统中的知识库进行增删改查&#xff08;CRUD&#xff09;操作。它基于 SQLAlchemy ORM 框架 和一个自定义的装饰器 with_session 实现数据库会话管理。 &#x1f4d8; 一、整体功能概述 该模块…...

push [特殊字符] present

push &#x1f19a; present 前言present和dismiss特点代码演示 push和pop特点代码演示 前言 在 iOS 开发中&#xff0c;push 和 present 是两种不同的视图控制器切换方式&#xff0c;它们有着显著的区别。 present和dismiss 特点 在当前控制器上方新建视图层级需要手动调用…...