
Spring Boot 中使用 ServletOutputStream 下载 PDF 文件时若出现文件损坏、体积异常缩小、无法打开等问题,往往源于对输出流的不当包装(如误用 BufferedOutputStream),本文详解根本原因、正确实现方式及关键注意事项。
spring boot 中使用 `servletoutputstream` 下载 pdf 文件时若出现文件损坏、体积异常缩小、无法打开等问题,往往源于对输出流的不当包装(如误用 `bufferedoutputstream`),本文详解根本原因、正确实现方式及关键注意事项。
在 Spring Boot 应用中实现 PDF 文件下载是一个高频需求,但开发者常遇到“文件能下载、却打不开”“下载后体积明显变小”“Acrobat 提示‘已损坏或不支持的格式’”等典型问题。从你提供的代码和排查过程来看,核心问题并非 IOUtils.copy() 或 MIME 类型设置错误,而在于 对 HttpServletResponse.getOutputStream() 返回的 ServletOutputStream 进行了不兼容的缓冲包装。
? 根本原因:ServletOutputStream 不应被 BufferedOutputStream 包装
你提到的关键线索是:
new BufferedOutputStream(outputStream); // ❌ 错误做法
ServletOutputStream 是 Servlet 容器(如 Tomcat)专为 HTTP 响应设计的底层输出流,它内部已具备高效的缓冲与写入机制,且与容器的响应生命周期强耦合。当你用 BufferedOutputStream 对其进行二次包装时:
- BufferedOutputStream 会在内存中缓存数据,直到调用 flush() 或 close() 才真正写出;
- 但 Spring MVC 在 Controller 方法返回后会自动调用 response.flushBuffer(),此时若 BufferedOutputStream 的缓冲区尚未清空(或已被提前关闭),部分字节将永久丢失;
- 导致最终下载的 PDF 文件头/尾部截断、交叉引用表(xref)损坏,PDF 阅读器无法解析 —— 这正是“原文件 300KB,下载后仅 12KB 且打不开”的直接原因。
✅ 正确做法是:直接使用 ServletOutputStream,避免任何中间包装。
✅ 推荐实现(安全、简洁、符合规范)
@GetMapping(value = "/download/{pdfId}", produces = "application/pdf")
public void downloadPdf(@PathVariable String pdfId, HttpServletResponse response) throws IOException {
String filename = pdf_location + pdfId + ".pdf";
File file = new File(filename);
if (!file.exists() || !file.isFile()) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "PDF not found");
return;
}
// ✅ 正确设置响应头(注意:filename 需 URL 编码以支持中文)
response.setContentType("application/pdf");
String encodedFilename = URLEncoder.encode(pdfId + ".pdf", StandardCharsets.UTF_8);
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFilename);
response.setContentLength((int) file.length());
// ✅ 直接使用 ServletOutputStream,不包装!
try (FileInputStream fis = new FileInputStream(file);
ServletOutputStream sos = response.getOutputStream()) {
IOUtils.copy(fis, sos); // Apache Commons IO 2.11+
sos.flush(); // 显式刷新(虽通常自动,但显式更稳妥)
}
}
? 说明:
- produces = "application/pdf" 与 setContentType(...) 双重保障 MIME 类型;
- 使用 filename*=UTF-8''... 格式(RFC 5987)确保浏览器正确解析含空格/中文的文件名;
- setContentLength() 提前告知客户端文件大小,提升体验并辅助校验;
- try-with-resources 确保 FileInputStream 和 ServletOutputStream 安全释放(注意:ServletOutputStream 的 close() 由容器管理,但 flush() 必须显式调用)。
⚠️ 其他关键注意事项
-
禁用 Spring Boot 的默认字符编码过滤器干扰:
若项目启用了 CharacterEncodingFilter(默认开启),它可能对二进制响应注入 BOM 或换行符。确保 application.yml 中配置:server: servlet: context-path: "/" spring: http: encoding: force: false # 避免强制编码影响二进制流 -
Swagger/Knife4j 测试限制:
Knife4j 的 UI 默认通过 AJAX 请求下载,而浏览器对 Content-Disposition: attachment 的 AJAX 响应不触发下载行为,且可能因 CORS 或响应体解析失败导致乱码。✅ 建议:- 使用 curl 或 Postman 直接测试:
curl -X GET "http://localhost:8080/download/123" --output test.pdf
- 或在浏览器地址栏直接访问 /download/123(GET 方式)验证原始行为。
- 使用 curl 或 Postman 直接测试:
-
替代方案:使用 Resource + ResponseEntity(更 Spring 风格)
@GetMapping("/download/{pdfId}") public ResponseEntity<resource> downloadPdf(@PathVariable String pdfId) throws IOException { Path path = Paths.get(pdf_location, pdfId + ".pdf"); Resource resource = new UrlResource(path); if (!resource.exists()) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "PDF not found"); } return ResponseEntity.ok() .contentType(MediaType.APPLICATION_PDF) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + URLEncoder.encode(pdfId + ".pdf", "UTF-8")) .body(resource); }</resource>此方式由 Spring 自动处理流拷贝与缓冲,规避手动流操作风险,强烈推荐用于新项目。
✅ 总结
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| PDF 下载后体积变小、无法打开 | 对 ServletOutputStream 错误包装为 BufferedOutputStream,导致缓冲未刷出即丢弃 | 禁止包装,直接使用 response.getOutputStream() |
| 中文文件名乱码 | Content-Disposition 未遵循 RFC 5987 | 使用 filename*=UTF-8''{encoded} 格式 |
| Swagger 测试失败 | AJAX 无法触发附件下载 | 改用 Postman/curl 或浏览器直连测试 |
| 潜在字符干扰 | CharacterEncodingFilter 强制编码 | 设置 spring.http.encoding.force=false |
只要坚持“不包装原始 ServletOutputStream、正确设置响应头、优先选用 ResponseEntity










