You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot使用StreamingResponseBody返回文件时响应体为空问题咨询

问题根因

你的原始实现存在两个核心缺陷,共同导致了偶发空响应的问题:

1. 输入流资源未正确释放,导致文件句柄泄漏

你在控制器方法中创建了FileInputStream,但后续没有手动关闭该流:

  • Spring的StreamUtils.copy、JDK的InputStream.transferTo都不会主动关闭输入/输出流,资源释放完全由调用方负责
  • 每次请求都会泄漏一个文件句柄,当进程打开的文件句柄数触及操作系统上限(Linux默认通常为1024,Windows会对未关闭的文件加独占锁)时,后续请求要么无法打开文件,要么读取到已经处于EOF末尾状态的流,复制操作直接返回0字节,最终表现为空响应
  • 第一次请求能正常返回,是因为此时还没有出现文件句柄泄漏/锁冲突的情况

2. 流创建和流操作的线程分离,存在异常处理盲区

StreamingResponseBody的写入逻辑是在Spring MVC的异步工作线程中执行的,而你在控制器的主线程中就提前创建了FileInputStream:

  • 如果异步任务因为超时、请求中断等原因被取消,已经打开的输入流完全没有机会被关闭,会进一步加剧资源泄漏
  • 没有做异常兜底处理,流读取过程中出现IO异常时,你没有手动触发响应错误标记,Spring可能会认为响应已经正常完成,直接返回已写入的0字节内容

为什么换成InputStreamResource就正常了?

Spring MVC内置的ResourceHttpMessageConverter会统一处理InputStreamResource的全生命周期:

  1. 它会把流的创建、复制、关闭逻辑全部包裹在try-with-resources结构中,执行完成后自动释放输入流资源,不会出现句柄泄漏
  2. 自动处理IO异常、响应头设置(比如自动计算Content-Length)、异步超时等边界情况,不会出现半完成的空响应

如果你坚持用StreamingResponseBody,正确的写法应该把输入流的创建和关闭都放在回调逻辑里,用try-with-resources包裹:

@GetMapping(value = "/{fileUuid}")
public StreamingResponseBody exportFile(@PathVariable String fileUuid, HttpServletResponse response) {    
    response.setContentType("application/zip");
    response.setHeader("Content-Disposition", "attachment; filename=\"export.zip\"");
    // 不要在控制器主线程创建InputStream,要放到回调里
    return outputStream -> {
        // 用try-with-resources自动关闭输入流
        try (InputStream inputStream = new FileInputStream(this.resolveFile(fileUuid))) {
            StreamUtils.copy(inputStream, outputStream);
            outputStream.flush();
        }
    };
}

内容的提问来源于stack exchange,提问作者eol

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.07 15:15:03