一、什么是流式输出?解决什么问题?
✅ 1. 流式输出
SpringMVC中的流式输出(Streaming Response),也叫分块输出/渐进式响应,核心语义:
服务端不是一次性将所有响应数据组装完成后整体返回给前端,而是将数据拆分成多个数据块,分批、持续地写入HTTP响应体,边写边返回;前端则可以实时接收每一个数据块,即时处理展示。
✅ 2. 流式输出的核心底层支撑:HTTP 分块传输编码
所有SpringMVC的流式输出能力,底层都是基于 HTTP/1.1 标准的分块传输编码(Chunked Transfer Encoding)实现。
- 当服务端需要流式输出时,会在HTTP响应头中设置:Transfer-Encoding: chunked
- 该响应头的含义:响应体的数据是分块传输的,每一块都有自己的长度标识,最后以一个「长度为0的块」标识传输结束
- 特性:无需设置响应头 Content-Length(因为总数据量未知),服务端可以边生成数据边传输,前端实时解析。
✅ 3. 流式输出的核心应用场景
✅ 大文件下载(Excel导出、视频/音频文件、压缩包):一次性加载大文件到内存会直接导致 OOM内存溢出,流式输出边读边写,内存占用极低;
✅ 大数据量接口返回(百万级列表、海量报表数据):避免一次性组装大数据JSON导致内存溢出,同时前端可以实时渲染;
✅ 实时数据推送(服务端主动推送):比如实时日志打印、任务进度推送、系统监控指标、AI流式问答(ChatGPT对话效果);
✅ 耗时任务的渐进式反馈:比如文件上传进度、数据同步进度,前端实时看到进度条更新;
✅ 长文本内容输出:比如动态生成的日志文件、数据库导出的SQL脚本。

✅ 4. 优势总结
- 极致节省内存:服务端无需将所有数据加载到内存,内存占用始终保持在极低水平,彻底解决大数据量/OOM问题;
- 前端实时响应:用户体验极佳,无需等待所有数据加载完成,前端可以即时展示内容(如AI问答的打字机效果);
- 适配未知数据量:数据量不确定时(如实时日志),流式输出是唯一可行的方案;
- 无性能瓶颈:分块传输的效率极高,不会因为数据量大导致请求超时。
二、SpringMVC流式输出的本质
所有SpringMVC的流式输出实现方案,无论高级还是低级,最终的底层操作都是同一个核心:
✅ 操作 HttpServletResponse 对象提供的原生输出流,将数据分批写入流中,完成分块传输。
SpringMVC为我们封装了多种优雅的上层API(如StreamingResponseBody、SseEmitter),但这些API的底层,都是对 HttpServletResponse.getOutputStream() / HttpServletResponse.getWriter() 的封装。
三、SpringMVC 流式输出 4种实现方案
✅ 方案一:StreamingResponseBody 标准流式输出
说明
StreamingResponseBody 是 SpringMVC 专门为流式输出设计的标准接口(Spring 4.2+ 引入),也是企业开发的首选方案,无任何侵入性,不需要手动操作原生的HttpServletResponse。
特点
- 优雅无侵入:Controller方法直接返回该接口的实现类即可,SpringMVC自动处理流式响应;
- 内存友好:数据按需生成、按需写入,无内存溢出风险;
- 自动适配:SpringMVC会自动为响应头添加 Transfer-Encoding: chunked,无需手动设置;
- 通用性强:支持任意类型的流式输出(文本、文件、JSON等),是最通用的流式方案。
接口核心源码
public interface StreamingResponseBody { // 核心方法:将数据写入输出流,SpringMVC会自动处理分块传输 void writeTo(OutputStream outputStream) throws IOException;}✔ 实战案例1:基础流式文本输出(如AI流式问答、实时文本推送)
常用的场景,比如AI问答的打字机效果,后端每生成一段文本就立即返回,前端实时展示:
import org.springframework.http.MediaType;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.OutputStream;import java.nio.charset.StandardCharsets;@RestControllerpublic class StreamController { /** * 流式文本输出 - AI流式问答/实时文本推送 典型场景 */ @GetMapping(value = "/stream/text", produces = MediaType.TEXT_PLAIN_VALUE) public StreamingResponseBody streamText(HttpServletResponse response) { // 可选:设置响应头,指定编码(防止中文乱码) response.setCharacterEncoding(StandardCharsets.UTF_8.name()); // 返回流式响应体,Lambda实现核心方法 return outputStream -> { // 模拟分批生成的文本数据(比如AI生成的逐段回答) String[] contentList = { "SpringMVC流式输出的核心是分块传输编码\n", "使用StreamingResponseBody是官方推荐的方案\n", "该方案无侵入,内存占用极低\n", "支持任意类型的流式数据输出\n", "流式输出可以完美解决大数据量OOM问题\n" }; // 分批写入输出流,模拟「边生成边返回」 for (String content : contentList) { outputStream.write(content.getBytes(StandardCharsets.UTF_8)); outputStream.flush(); // 核心:刷新缓冲区,立即将数据返回给前端 Thread.sleep(500); // 模拟数据生成耗时,前端会看到「打字机」效果 } }; }}✔ 实战案例2:流式大文件下载
这是流式输出最高频的生产场景,解决大文件下载时的内存溢出问题,原理:服务端从磁盘/网络读取文件的一小块,写入输出流,立即返回给前端,循环往复直到文件读取完毕,内存中永远只存在一小块文件数据,内存占用几乎可以忽略不计。
/** * 流式大文件下载 - 生产环境必用方案,彻底解决大文件OOM问题 */@GetMapping("/stream/download")public StreamingResponseBody streamFileDownload(HttpServletResponse response) { // 1. 设置下载响应头,告诉浏览器是下载文件 response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); response.setHeader("Content-Disposition", "attachment; filename=bigfile.txt"); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); // 2. 流式读取文件并写入响应流 return outputStream -> { // 读取本地大文件(替换为你的文件路径) try (FileInputStream fis = new FileInputStream(new File("D:/test/bigfile.txt")); BufferedInputStream bis = new BufferedInputStream(fis)) { byte[] buffer = new byte[1024 * 8]; // 8KB缓冲区,按需调整大小 int len; // 循环读取文件,分批写入响应流 while ((len = bis.read(buffer)) != -1) { outputStream.write(buffer, 0, len); outputStream.flush(); // 立即返回给前端 } } catch (Exception e) { e.printStackTrace(); } };}该方案的优势
✅ 无需手动处理分块编码,Spring自动完成;
✅ 无需手动关闭流,Spring自动释放资源;
✅ 方案二:原生 Servlet API 流式输出
说明
这是最基础、最底层、万能的流式输出方案,直接在Controller中注入 HttpServletResponse 对象,通过其提供的原生输出流(OutputStream/Writer)写入数据,是所有流式方案的底层根基。
该方案的优势是:无任何版本依赖、无任何封装、万能适配,无论是SpringMVC、原生Servlet项目,还是老旧的Spring版本,都能使用;缺点是:需要手动操作原生对象,代码侵入性稍强,但胜在灵活。
核心原理
- 从HttpServletResponse中获取 字节输出流OutputStream(适用于二进制数据:文件、图片、视频) 或 字符输出流PrintWriter(适用于文本数据:字符串、JSON);
- 手动设置响应头 Transfer-Encoding: chunked(SpringMVC会自动设置,可省略);
- 分批将数据写入输出流,调用flush()刷新缓冲区,立即返回给前端;
- 循环直到数据写入完毕,无需关闭流(容器会自动关闭)。
✔ 实战案例1:原生流 流式文本输出
/** * 原生Servlet API 流式文本输出 - 万能方案 */@GetMapping("/stream/native/text")public void nativeStreamText(HttpServletResponse response) throws IOException { // 1. 设置响应头 response.setContentType(MediaType.TEXT_PLAIN_VALUE); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); // 2. 获取字符输出流(文本数据首选) PrintWriter writer = response.getWriter(); // 3. 分批写入数据 String[] contentList = {"你好,", "这是原生Servlet的", "流式输出示例\n", "每一行都会实时返回!"}; for (String content : contentList) { writer.write(content); writer.flush(); // 核心:刷新缓冲区,立即返回 Thread.sleep(600); } writer.close(); // 可选,容器会自动关闭}✔ 实战案例2:原生流 大文件流式下载
/** * 原生Servlet API 大文件流式下载 - 生产环境常用 */@GetMapping("/stream/native/download")public void nativeStreamDownload(HttpServletResponse response) throws IOException { // 1. 设置下载响应头 response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); response.setHeader("Content-Disposition", "attachment; filename=bigfile.zip"); // 2. 获取字节输出流(二进制数据首选) OutputStream out = response.getOutputStream(); // 3. 流式读取文件并写入 try (FileInputStream fis = new FileInputStream(new File("D:/test/bigfile.zip")); BufferedInputStream bis = new BufferedInputStream(fis)) { byte[] buffer = new byte[1024 * 16]; int len; while ((len = bis.read(buffer)) != -1) { out.write(buffer, 0, len); out.flush(); } } catch (Exception e) { e.printStackTrace(); }}注意事项
- flush()方法是核心:如果不调用flush(),数据会被缓存到服务端的缓冲区,直到缓冲区满了才会返回给前端,就失去了流式输出的意义;
- 字符流 vs 字节流:文本数据用PrintWriter,二进制数据用OutputStream,不要混用;
- 中文乱码:必须设置response.setCharacterEncoding("UTF-8")和response.setContentType("text/plain;charset=UTF-8")。
✅ 方案三:SseEmitter 服务端事件推送
说明
SseEmitter 是 SpringMVC 提供的 服务端推送(Server-Sent Events,简称SSE) 专用流式方案,是流式输出的进阶形态。
✔ 什么是 SSE?
SSE 是 基于HTTP长连接的单向流式推送技术:服务端和前端建立一个持久的HTTP连接,服务端可以在任意时间、持续地向前端推送文本数据块,前端实时接收并处理,直到连接主动关闭。
✔ SSE 与普通流式输出的核心区别
- 普通流式输出(StreamingResponseBody):一次性分块传输,数据传输完成后,连接立即断开;适用于大文件下载、大数据量返回等一次性输出场景;
- SSE 流式推送(SseEmitter):长连接持续传输,连接会一直保持,服务端可以主动、多次推送数据,直到手动关闭连接;适用于实时日志、任务进度推送、系统监控、AI流式对话等实时推送场景。
✔ SSE 的特性
✅ 基于HTTP协议,无需额外协议(如WebSocket),无需引入额外依赖;
✅ 单向推送:服务端 → 前端,无法双向通信(如果需要双向通信,用WebSocket);
✅ 自动重连:前端原生支持自动重连机制,断连后会自动重试;
✅ 文本数据:只支持文本流(JSON、字符串),不支持二进制数据(文件、图片);
✅ SpringMVC完美封装:SseEmitter 是Spring对原生SSE的封装,开箱即用。
✔ 实战案例:实时日志推送
后端持续生成日志,前端实时展示日志内容,典型的运维平台/监控平台场景,代码可直接运行:
后端代码(SpringMVC)
import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;import java.io.IOException;import java.util.Random;@RestControllerpublic class SseStreamController { private final Random random = new Random(); /** * SSE流式推送 - 实时日志推送 典型场景 */ @GetMapping("/stream/sse/log") public SseEmitter sseLogStream() { // 设置超时时间:0表示永不超时,也可以设置毫秒值(如30*60*1000) SseEmitter emitter = new SseEmitter(0L); // 异步推送数据(必须异步,否则会阻塞主线程) new Thread(() -> { try { for (int i = 1; i <= 100; i++) { // 模拟实时生成的日志内容 String log = "【" + System.currentTimeMillis() + "】- 系统日志:" + "用户操作-" + i + ",状态:" + (random.nextBoolean() ? "成功" : "失败") + "\n"; // 核心:推送数据到前端 emitter.send(log); Thread.sleep(1000); // 每秒推送一条日志 } // 推送完成,关闭连接 emitter.complete(); } catch (IOException | InterruptedException e) { // 推送失败,关闭连接并抛出异常 emitter.completeWithError(e); } }).start(); return emitter; }}前端代码(原生JS,无需任何依赖,实时接收)
SSE的前端接收非常简单,原生EventSource对象即可实现,无需引入axios/fetch等库,完美适配流式推送:
SSE实时日志 实时日志输出:
[xss_clean] // 建立SSE连接,监听后端推送 const eventSource = new EventSource('http://localhost:8080/stream/sse/log'); const logContainer = document.getElementById('log-container'); // 接收后端推送的消息 eventSource.onmessage = function (event) { logContainer[xss_clean] += event.data; // 滚动到最新日志 logContainer.scrollTop = logContainer.scrollHeight; }; // 连接关闭时触发 eventSource.onclose = function () { console.log('SSE连接已关闭'); eventSource.close(); }; // 连接异常时触发 eventSource.onerror = function (error) { console.error('SSE连接异常:', error); eventSource.close(); }; [xss_clean]✔ SSE vs WebSocket 选型建议
✅ 用 SseEmitter:需要单向推送(服务端→前端)、文本数据、实时日志/进度推送、无需双向通信,追求简单无依赖;
✅ 用 WebSocket:需要双向通信(前端↔服务端)、二进制数据、高并发实时交互(如聊天室、在线游戏);
✅ 方案四:ResponseBodyEmitter 通用异步流式输出【✅ 灵活度最高、进阶方案】
核心说明
ResponseBodyEmitter 是 SseEmitter 的父类,是SpringMVC提供的通用异步流式输出方案,灵活度最高,也是最底层的异步流式API。
核心特点
- 是SseEmitter的超类,SseEmitter本质是ResponseBodyEmitter的子类,专门适配SSE场景;
- 支持任意类型的流式数据:文本、二进制、JSON等,无数据类型限制;
- 支持异步推送:可以在任意线程中推送数据,完美适配异步业务逻辑;
- 支持自定义响应头、响应状态码,灵活度拉满;
- 适用于复杂的异步流式场景(如:多线程生成数据、异步调用返回结果后分批推送)。
✔ 极简实战案例
@GetMapping("/stream/response-body")public ResponseBodyEmitter responseBodyStream() { ResponseBodyEmitter emitter = new ResponseBodyEmitter(); // 异步推送数据 new Thread(() -> { try { emitter.send("第一块数据", MediaType.TEXT_PLAIN); Thread.sleep(500); emitter.send("第二块数据", MediaType.TEXT_PLAIN); Thread.sleep(500); emitter.send("第三块数据", MediaType.TEXT_PLAIN); emitter.complete(); } catch (Exception e) { emitter.completeWithError(e); } }).start(); return emitter;}四、避坑指南
✅ 坑点1:流式输出没有生效,前端一次性收到所有数据
原因
- 忘记调用 flush() 方法:数据被缓存到服务端缓冲区,直到缓冲区满才会返回;
- 数据量太小:服务端缓冲区默认大小为8KB,数据量小于8KB时,会被缓存;
- 前端使用了axios的默认配置:axios会缓存响应数据,直到连接关闭才会返回给业务代码。
解决方案
- 必须调用 flush():所有流式方案中,写入数据后一定要调用flush()刷新缓冲区,立即返回数据;
- 模拟大数据量:开发测试时,可通过循环生成大量数据,或设置Thread.sleep()模拟耗时;
- 前端适配:axios需要关闭缓存,或使用原生fetch/EventSource接收流式数据。
✅ 坑点2:中文乱码
原因
未设置响应的字符编码,或字符编码不一致(服务端UTF-8,前端GBK)。
解决方案
// 方式1:设置响应头response.setCharacterEncoding("UTF-8");response.setContentType("text/plain;charset=UTF-8");// 方式2:在@RequestMapping中指定produces@GetMapping(value = "/stream/text", produces = "text/plain;charset=UTF-8")✅ 坑点3:大文件下载时内存溢出
原因
没有使用流式读取,而是一次性将文件加载到内存(如FileUtils.readFileToByteArray())。
解决方案
强制使用流式读取:通过BufferedInputStream+缓冲区读取文件,分批写入输出流,如方案一/二中的文件下载案例。
✅ 坑点4:SSE连接频繁断开/超时
原因
- 设置了较短的超时时间,或未设置超时时间;
- 服务端长时间未推送数据,被网关/防火墙断开连接。
解决方案
- 设置超时时间为0(永不超时):new SseEmitter(0L);
- 定时推送心跳包:如果长时间无数据,推送一个空数据块,保持连接活跃;
- 前端开启自动重连:原生EventSource支持自动重连,无需额外配置。
✅ 坑点5:流式输出时接口阻塞
原因
同步执行流式输出逻辑,阻塞了SpringMVC的核心线程池,导致其他接口无法访问。
解决方案
异步执行流式逻辑:使用@Async注解,或手动创建线程执行流式写入,如SSE案例中的异步线程。
五、总结
✅ 流式输出底层原理
SpringMVC流式输出的核心是 HTTP/1.1 分块传输编码(Transfer-Encoding: chunked),所有方案最终都是操作HttpServletResponse的原生输出流,分批写入数据并刷新缓冲区。
✅ 方案选型
- 标准流式选 StreamingResponseBody;
- 实时推送选 SseEmitter;
- 万能兜底选 原生Servlet流;
- 复杂异步选 ResponseBodyEmitter。
✅ 避坑点
- flush()是流式输出的核心,必须调用;
- 中文乱码要设置统一的UTF-8编码;
- 大文件必须流式读取,避免OOM;
- 实时推送要异步执行,避免阻塞线程池。