大数跨境
0
0

Spring Boot 二进制数据传输:从内存优化到流式处理与断点续传

Spring Boot 二进制数据传输:从内存优化到流式处理与断点续传 Spring全家桶实战案例
2025-11-22
0
导读:Spring Boot 二进制数据传输:从内存优化到流式处理与断点续传
Spring Boot 3实战案例锦集PDF电子书已更新至130篇!
图片

🎉🎉《Spring Boot实战案例合集》目前已更新192个案例,我们将持续不断的更新。文末有电子书目录。

💪💪永久更新承诺

我们郑重承诺,所有订阅合集的粉丝都将享受永久免费的后续更新服务

💌💌如何获取
订阅我们的合集点我订阅,并通过私信联系我们,我们将第一时间将电子书发送给您。

→ 现在就订阅合集

环境:SpringBoot3.4.2



1. 简介

在 Spring Boot 中高效传输二进制数据(如文件、图片、动态生成内容)需关注 Content-Type 设置、流式处理、缓存控制及分块下载。直接读取文件到内存(如 byte[])适用于小文件,但大文件应使用 InputStreamResource 或 StreamingResponseBody 避免内存溢出。动态内容可通过 HttpServletResponse 直接写入响应流,压缩文件则可用 ZipOutputStream 实时生成。为避免缓存问题,可通过 ETag 或 Last-Modified 实现条件请求。此外,支持 HTTP 范围请求(ResourceRegion)可实现断点续传,显著提升大文件传输体验。本篇文章将结合代码示例,系统化讲解二进制数据传输的核心技术与优化策略。

2.实战案例
2.1 正确设置Content-Type
在响应二进制内容时,Content-Type头部告知客户端即将接收的数据类型。若缺少此信息,浏览器可能会尝试将二进制数据作为文本显示,或在不知如何处理的情况下直接下载。如下示例:
@GetMapping("/download1")public ResponseEntity<byte[]> download1() throws IOException {  Path path = Paths.get("E:/技术架构.pdf");  byte[] pdfData = Files.readAllBytes(path);  String fileName = "技术架构.pdf";  fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8)          .replaceAll("\\+""%20");  return ResponseEntity.ok()      .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")      .contentType(MediaType.APPLICATION_PDF)      .body(pdfData);}
首先将整个文件读取到内存中,通过Content-Disposition头指示浏览器提示下载而非尝试内联渲染。对于不适合直接显示的格式(如归档文件或加密数据),这将非常有用。
示例2:
@GetMapping("/images/logo")public ResponseEntity<byte[]> fetchLogo() throws IOException {  ClassPathResource resource = new ClassPathResource("static/7.png") ;  byte[] bytes;  try (InputStream in = resource.getInputStream()) {    bytes = in.readAllBytes();  }  return ResponseEntity.ok()      .contentType(MediaType.IMAGE_PNG)      .contentLength(resource.contentLength())      .body(bytes);}
该示例将直接在浏览器中展示图片。
2.2 流式处理大文件

当文件超过几兆时,在发送时将内容全部加载到内存中这将带来很大的风险。首先,这种做法会消耗不必要的内存,并在等待文件读取完成时导致响应卡顿。流式传输允许数据以较小块的形式传输,无需一次性存储全部内容。Spring Boot可通过InputStreamResource、FileSystemResource控制流式传输。如下示例:

@GetMapping("/download2")public ResponseEntity<InputStreamResource> download2() throws IOException {  File file = new File("E:/技术架构.pdf");  String fileName = "技术架构.pdf";  fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8)          .replaceAll("\\+""%20");  InputStreamResource resource = new InputStreamResource(new FileInputStream(file));  return ResponseEntity.ok()      .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")      .contentType(MediaType.APPLICATION_OCTET_STREAM)      .contentLength(file.length())      .body(resource);}

此方法实现了直接流式传输至客户端,不会被复制到内存中。同时客户端可以统计设置的Content-Length判断文件是否接收完成。

使用StreamingResponseBody实时生成响应内容
@GetMapping("/export/csv")public ResponseEntity<StreamingResponseBody> exportCsv() {  StreamingResponseBody stream = outputStream -> {    String header = "姓名,年龄,邮箱\n";    outputStream.write(header.getBytes(java.nio.charset.StandardCharsets.UTF_8));    outputStream.write("pack,33,pack@gmail.com\n".getBytes(java.nio.charset.StandardCharsets.UTF_8));    outputStream.write("xg,32,xg@qq.com\n".getBytes(java.nio.charset.StandardCharsets.UTF_8));    outputStream.write("pack_xg,40,pack_xg@163.com\n".getBytes(java.nio.charset.StandardCharsets.UTF_8));  };  return ResponseEntity.ok()      .header(HttpHeaders.CONTENT_DISPOSITION"attachment; filename=\"users.csv\"")      .contentType(MediaType.parseMediaType("text/csv; charset=UTF-8"))      .body(stream) ;}

这避免了临时文件的使用,即使在数据生成过程中也能保持响应流畅。对于需要保持低内存占用的大型报告或日志导出而言,此方法非常实用。

2.3 响应动态生成的文件

并非所有文件都来自磁盘。许多服务会在内存中生成内容,例如PDF、图像或压缩数据集。当内容在运行时创建时,可直接写入响应流而无需预先保存。如下示例:

@GetMapping("/generate/report")public void generateReport(HttpServletResponse response) throws IOException {  response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE) ;  String filename = "报告.txt";  filename = URLEncoder.encode(filename, StandardCharsets.UTF_8).replaceAll("\\+""%20");  response.setHeader("Content-Disposition""attachment; filename=\"" + filename + "\"");  try (OutputStream out = response.getOutputStream()) {    byte[] reportBytes = createText(); // 假设这个方法生成文本内容    out.write(reportBytes);    out.flush();  } }private byte[] createText() {  return "这是报告内容\n第二行内容".getBytes(StandardCharsets.UTF_8);}

如上实现没有文件IO操作,这在处理动态生成的文档或分析导出时,这种做法很常见。输出流直接连接到HTTP响应,既降低了延迟,又减少了磁盘访问。

如果你需要同时下载多个文件并且是希望生成一个压缩文件进行下载,那么你可以采用如下的方式:
@GetMapping("/generate/zip")public ResponseEntity<StreamingResponseBody> generateZip() {  StreamingResponseBody stream = output -> {    try (ZipOutputStream zipOut = new ZipOutputStream(output)) {      ZipEntry entry = new ZipEntry("summary.txt");      zipOut.putNextEntry(entry);      zipOut.write("这里是摘要内容".getBytes(java.nio.charset.StandardCharsets.UTF_8));      entry = new ZipEntry("content.txt");      zipOut.putNextEntry(entry);      zipOut.write("主体内容".getBytes(java.nio.charset.StandardCharsets.UTF_8));      zipOut.closeEntry();      zipOut.finish() ;    }  };  return ResponseEntity.ok()      .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"export.zip\"")      .contentType(MediaType.APPLICATION_OCTET_STREAM)      .body(stream);}
最终生成的压缩文件如下:
2.4 避免浏览器缓存

浏览器倾向于保留已下载的文件。这对图像或静态资源很有帮助,但对于经常更新的数据(如每日报告或导出文件)则不然。如果响应未告知浏览器更新,用户可能在不知情的情况下下载过时文件。为确保始终提供最新数据,可在响应中直接添加缓存标头。如下示例:

@GetMapping("/download3")public ResponseEntity<byte[]> download3() throws IOException {  Path path = Paths.get("E:/技术架构.pdf");  byte[] data = Files.readAllBytes(path);  String fileName = "技术架构.pdf";  fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8)          .replaceAll("\\+""%20");  ZonedDateTime expiresTime = ZonedDateTime.now(ZoneId.systemDefault()).plusSeconds(30);  String expiresHeader = expiresTime.format(DateTimeFormatter.RFC_1123_DATE_TIME);  return ResponseEntity.ok()          .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")          .header(HttpHeaders.EXPIRES, expiresHeader)          .contentType(MediaType.APPLICATION_PDF)          .body(data);}
通过expires设置有效期为30s,运行该接口浏览器查看执行情况:
过期后又会从服务器进行下载文件。在有效期时间内并不会请求我们的实际接口。

当我们需要更加精确的控制缓存时,可通过更具选择性的方法。条件缓存标头(如ETag或Last-Modified)会指示浏览器在复用文件前重新验证其有效性。如下示例:

@GetMapping("/getConfig")public ResponseEntity<Resource> getConfigFile(HttpServletRequest request) throws IOException {  FileSystemResource resource = new FileSystemResource("E:/config.json");  long lastModified = resource.lastModified();  String eTag = "\"" + lastModified + "\"";  String ifNoneMatch = request.getHeader(HttpHeaders.IF_NONE_MATCH);  boolean tagMatches = ifNoneMatch != null && ifNoneMatch.contains(eTag);  if (tagMatches) {    return ResponseEntity.status(HttpStatus.NOT_MODIFIED)        .eTag(eTag)        .lastModified(lastModified)        .build();  }  return ResponseEntity.ok()      .eTag(eTag)      .lastModified(lastModified)      .contentType(MediaType.APPLICATION_JSON)      .body(resource);}
如上实现,在不影响文件更新性的前提下减少了不必要的带宽消耗。当文件未变更时,客户端将收到304未修改响应;而更新后的内容则正常发送。上面接口运行结果:
当文件config.json发生变化后才会再次读取文件内容发送。
对于静态资源,我们可以通过如下配置进行全局设置:
@Configurationpublic class CacheConfig implements WebMvcConfigurer {  @Override  public void addResourceHandlers(ResourceHandlerRegistry registry) {    registry.addResourceHandler("/resources/**")      .addResourceLocations("classpath:/public/")      .setCacheControl(CacheControl.maxAge(Duration.ofDays(1)));  }}
2.5 范围分块下载

当需要高效传输大文件(如视频、音频、大型软件包)或支持断点续传时,HTTP范围请求可按需下载文件片段。例如视频拖动进度条、下载工具暂停后恢复,或仅传输客户端未缓存的部分,显著节省带宽和时间。如下示例:

@GetMapping("/download4")public ResponseEntity<ResourceRegion> download4(@RequestHeader HttpHeaders headers) throws IOException {  // 1.获取系统资源文件  FileSystemResource resource = new FileSystemResource("e:/doubao.exe");  if (!resource.exists()) {    return ResponseEntity.notFound().build();  }  long contentLength = resource.contentLength();  MediaType mediaType = MediaTypeFactory.getMediaType(resource)          .orElse(MediaType.APPLICATION_OCTET_STREAM);  // 2.处理范围请求(支持多范围请求,但仅返回第一个范围)  List<HttpRange> ranges = headers.getRange();  if (ranges.isEmpty()) {    // 完整文件下载    return ResponseEntity.ok()        .header("Accept-Ranges""bytes")        .contentType(mediaType)        .contentLength(contentLength)        .body(new ResourceRegion(resource, 0, contentLength));  }  // 3.严格校验范围有效性(避免越界)  HttpRange range = ranges.get(0);  long start = range.getRangeStart(contentLength);  long end = range.getRangeEnd(contentLength);  if (start > end || start < 0 || end >= contentLength) {    return ResponseEntity.status(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE)        .header("Content-Range""bytes */" + contentLength)        .build();  }  // 4.优化分块大小  long rangeLength = end - start + 1;  long chunkSize = Math.min(10 * 1024 * 1024, rangeLength); // 默认10MB,适应大文件  ResourceRegion region = new ResourceRegion(resource, start, chunkSize);  return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)          .header("Content-Range""bytes " + start + "-" + (start + chunkSize - 1) + "/" + contentLength)          .header("Content-Disposition""attachment; filename=\"" + resource.getFilename() + "\"")          .contentType(mediaType)          .body(region);}
注意,这里的返回值类型不能使用ResponseEntity<?> 通配符,否则将会报错。

以上是本篇文章的全部内容,如对你有帮助帮忙点赞+转发+收藏
Spring 7.0 正式发布啦!强大新特性强势来袭

性能优化!Spring Boot使用强大的@EntityGraph优化查询

性能优化!Spring Boot 优化JPA插入操作,性能提升N倍,尤其批量操作

Spring Boot全局异常处理:3种方案,第三种性能40%的提升

太实用了!自定义@PackParam,请求参数万能处理器

Spring Boot 神奇的2个类!动态方法查找调用

知道太晚了!实时通信新选择fetch-event-source,真香

Spring Boot 记录Controller接口请求日志7种方式,第六种性能极高

绝了!Spring Boot 事务回滚竟藏 5 种神操作

TargetSource炸场!Spring AOP动态换“靶”超神!

强大!Spring Boot敏感数据动态配置,这样做更安全

图片
图片
图片
图片
图片
图片
图片
图片

【声明】内容源于网络
0
0
Spring全家桶实战案例
Java全栈开发,前端Vue2/3全家桶;Spring, SpringBoot 2/3, Spring Cloud各种实战案例及源码解读
内容 832
粉丝 0
Spring全家桶实战案例 Java全栈开发,前端Vue2/3全家桶;Spring, SpringBoot 2/3, Spring Cloud各种实战案例及源码解读
总阅读195
粉丝0
内容832