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

优雅地实现ChatGPT式的打字机效果:Spring Boot 流式响应

01 引言

之前专门介绍过流式响应的数据的接收、发送以及使用SSE由服务端推送数据的文章,但是要求前端必须使用EventSource订阅实现。

有没有通过直接通过浏览器访问或者Fetch API直接调用的方式呢?效果还能和ChatGPT一样,实现打字机的效果呢?

当然有。Spring框架在4.2及以后引入了强大额异步响应特性,其中ResponseBodyEmitterStreamingResponseBody是处理 HTTP 响应流式输出的两大核心利器,既不阻塞线程又不影响系统的相应能力。

我们一起了解一下吧!

02 StreamingResponseBody

全限定类名:

org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody

2.1 简介

StreamingResponseBody是一个函数式接口,适用于需要直接向 HttpServletResponse 的输出流写入原始字节数据的场景。它提供了一种低层次、高效的方式来流式传输数据。

StreamingResponseBody没有默认的实现,只有一个方法writeTo()。将文件或者数据直接写入输出流中,可以通过输出流的flush()方法,将数据刷出磁盘,减少内存的占用。

当我们需要将大量的数据或者文件响应给客户端时,由于处理耗时合作和内存的限制,我们就可以通过这种方式,逐步将数据响应给客户端,既可以提高用户体验又可以节省内存。

2.2 最佳实践

为了演示的方便,我们选择将文本内容逐步输出到浏览器端。

代码案例

间隔1s将数据包传给客户端:

@GetMapping(value = "/srb")
public StreamingResponseBody srb(HttpServletResponse response) {response.setContentType(MediaType.TEXT_EVENT_STREAM_VALUE);StreamingResponseBody stream = os -> {for (int i = 0; i < 5; i++) {String data = "SRB 数据包【"+(i+1)+"】\n";os.write(data.getBytes());System.out.print(data);// 刷出磁盘os.flush();try {// 间隔1sThread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}};return stream;
}

因为没有默认的实现,这里使用了匿名内部类实现。

响应结果

03 ResponseBodyEmitter

全限定类名:

org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter

3.1 简介

ResponseBodyEmitter是一个类,官方还给出了具体的使用案例。该类使用异步线程发送数据,最终需要使用complete()结束数据的传输,否则就会一直等待的状态。

有没有发现我们之前介绍的SseEmitterResponseBodyEmitter有点类似,SseEmitter其实是ResponseBodyEmitter的子类。

SseEmitter作为特殊的ResponseBodyEmitter,专门支持Server-Sent Events的。

ResponseBodyEmitter 的抽象层次更高。它不仅可以发送字节,更重要的是可以发送对象,这些对象会被配置的 HttpMessageConverter 转换后发送(如转换为 JSON)。它允许你多次发送数据,非常适合服务器端事件(Server-Sent Events, SSE)或分块 JSON

3.2 最佳实践

代码案例

@GetMapping(value = "/rbe")
public ResponseBodyEmitter rbe(HttpServletResponse response) {response.setContentType(MediaType.TEXT_EVENT_STREAM_VALUE);ResponseBodyEmitter emitter = new ResponseBodyEmitter(0L);CompletableFuture.runAsync(() -> {for (int i = 0; i < 5; i++) {String data = "RBE 数据包【"+(i+1)+"】\n";try {emitter.send(data);System.out.print(data);// 间隔1sThread.sleep(1000);} catch (Exception e) {throw new RuntimeException(e);}}emitter.complete();});return emitter;
}

响应结果

04 注意事项

两个类的案例中均有一行设置ContentType类型的代码:

response.setContentType(MediaType.TEXT_EVENT_STREAM_VALUE);

设置响应类型为text/event-stream,这一步很关键。否则直到全部的结果返回才会展示在浏览器端。

@GetMappingproduces属性并不能解决该问题,produces指定返回的内容类型,仅当request 请求头中的(Accept)类型中包含该指定类型才返回。

05 番外

聊到异步线程的响应,又想到了另外一个Spring MVC 专门处理异步请求的类DeferredResult,用法类似ResponseBodyEmitter,不同的是DeferredResult只能设置一次结果。

@GetMapping("/async")
public DeferredResult<String> async() {DeferredResult<String> deferredResult = new DeferredResult<>();CompletableFuture.runAsync(() -> {System.out.println("DeferredResult触发,等待结果");String data = "DeferredResult 数据包";try {// 间隔1sThread.sleep(2000);deferredResult.setResult(data);System.out.println("DeferredResult触发,完结");} catch (Exception e) {throw new RuntimeException(e);}});return deferredResult;
}

执行结果发现:他会等待deferredResult.setResult(data)发送数据之后才会结束响应。

http://www.dtcms.com/a/355686.html

相关文章:

  • Jtekt深沟球轴承外圈防跑圈开发
  • Python Imaging Library (PIL) 全面指南:PIL基础入门-图像颜色模式转换与应用
  • [网鼎杯 2018]Fakebook
  • 基础IO详解
  • 【前端教程】JavaScript 基础总结
  • 教育类《河北教育》杂志简介
  • Day03_苍穹外卖——公共字段自动填充菜品相关功能
  • 河南萌新联赛2025第(七)场:郑州轻工业大学
  • 【数据结构与算法】(LeetCode)141.环形链表 142.环形链表Ⅱ
  • 数据分析学习笔记4:加州房价预测
  • 国产的服务器
  • 如何监控PCIe 5.0 SSD的运行温度?多软件推荐
  • 中国剩余定理(以及扩展..)
  • 用 Docker 玩转 Kafka 4.0镜像选型、快速起步、配置持久化与常见坑
  • 影楼精修-锁骨增强算法
  • 深入理解 PHP 中的 `pcntl_fork()`:父进程与子进程的执行路径
  • SRE网易一面面经
  • Linux笔记12——shell编程基础-6
  • 少样本图异常检测系列【A Survey of Few-Shot Graph Anomaly Detection】
  • Python实战:银行ATM系统开发全解析
  • RuoYi-VuePlus:前端指定接口不显示错误提示
  • 面试tips--JVM(2)--对象创建的过程
  • ERNIE-4.5-VL:技术解密+应用实战,解锁多模态新场景!
  • 8.29 贪心|摩尔投票
  • 【不说废话】pytorch中.to(device)函数详解
  • 基于K8s部署服务:dev、uat、prod环境的核心差异解析
  • 工业级TF卡NAND+北京君正+Rk瑞芯微的应用
  • openEuler Embedded 的 Yocto入门 : 5.基本变量与基本任务详解
  • Linux 系统 poll 与 epoll 机制1:实现原理与应用实践
  • DINOv2 vs DINOv3 vs CLIP:自监督视觉模型的演进与可视化对比