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

关注我的公众号:【编程朝花夕拾】,可获取首发内容。

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将数据包传给客户端:

java 复制代码
@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 {
                // 间隔1s
                Thread.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 最佳实践

代码案例

java 复制代码
@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);

                // 间隔1s
                Thread.sleep(1000);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        emitter.complete();
    });

    return emitter;
}

响应结果

04 注意事项

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

java 复制代码
response.setContentType(MediaType.TEXT_EVENT_STREAM_VALUE);

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

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

05 番外

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

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

    return deferredResult;
}

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

相关推荐
邵伯3 分钟前
Java源码中的排序算法(一)--Arrays.sort()
java·排序算法
小杨同学494 分钟前
C 语言实战:动态规划求解最长公共子串(连续),附完整实现与优化
后端
Cache技术分享6 分钟前
290. Java Stream API - 从文本文件的行创建 Stream
前端·后端
用户948357016517 分钟前
拒绝 try-catch:如何设计全局通用的异常拦截体系?
后端
golang学习记9 分钟前
Go 1.22 隐藏彩蛋:cmp.Or —— 让“默认值”写起来像呼吸一样自然!
后端
阿里巴巴P8高级架构师10 分钟前
从0到1:用 Spring Boot 4 + Java 21 打造一个智能AI面试官平台
java·后端
stevenzqzq12 分钟前
trace和Get thread dump的区别
java·android studio·断点
桦说编程13 分钟前
并发编程踩坑实录:这些原则,帮你少走80%的弯路
java·后端·性能优化
程序猿零零漆13 分钟前
Spring之旅 - 记录学习 Spring 框架的过程和经验(十三)SpringMVC快速入门、请求处理
java·学习·spring
BHXDML13 分钟前
JVM 深度理解 —— 程序的底层运行逻辑
java·开发语言·jvm