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

Spring Boot中为ResponseEntity<StreamingResponseBody>设置Content-Length遇阻

解决Spring Boot Zip下载中Content-Length无法正确设置的问题

你的核心矛盾在于:StreamingResponseBody是边生成内容边发送响应,而Zip压缩后的总大小必须等压缩完全结束(关闭ZipOutputStream)才能确定,但此时响应头已经发送完毕,导致Content-Length只能设为0。下面给出两种可行的解决方案,适配不同场景:


方案1:预计算压缩后的总大小(无临时文件)

先通过空输出流预压缩一次,计算出最终Zip文件的总大小,再正式执行压缩并设置正确的Content-Length。

实现步骤

  1. 编写工具方法,用NullOutputStream(仅计算压缩大小,不实际写入数据)预计算所有文件压缩后的总大小;
  2. 在Controller中先调用工具方法获取总大小,再用StreamingResponseBody正式压缩输出;
  3. 确保预压缩和正式压缩的压缩级别、算法完全一致,避免大小计算偏差。

代码示例

import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

import java.io.*;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.List;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@RestController
public class ZipDownloadController {

    // 预计算Zip压缩后的总大小
    private long calculateZipTotalSize(List<File> targetFiles) throws IOException {
        long totalCompressedSize = 0;
        try (ZipOutputStream zipOut = new ZipOutputStream(new NullOutputStream())) {
            zipOut.setLevel(Deflater.DEFAULT_COMPRESSION); // 和正式压缩保持一致
            for (File file : targetFiles) {
                ZipEntry entry = new ZipEntry(file.getName());
                zipOut.putNextEntry(entry);
                // 写入文件内容触发压缩计算,不实际存储数据
                Files.copy(file.toPath(), zipOut);
                zipOut.closeEntry();
                totalCompressedSize += entry.getCompressedSize();
            }
        }
        return totalCompressedSize;
    }

    @GetMapping("/download-zip")
    public ResponseEntity<StreamingResponseBody> downloadZip() throws IOException {
        // 替换为你要压缩的文件列表
        List<File> filesToZip = Arrays.asList(new File("file1.txt"), new File("file2.jpg"));

        // 预计算压缩总大小
        long zipTotalSize = calculateZipTotalSize(filesToZip);

        StreamingResponseBody responseBody = outputStream -> {
            try (ZipOutputStream zipOut = new ZipOutputStream(outputStream)) {
                zipOut.setLevel(Deflater.DEFAULT_COMPRESSION);
                for (File file : filesToZip) {
                    ZipEntry entry = new ZipEntry(file.getName());
                    zipOut.putNextEntry(entry);
                    Files.copy(file.toPath(), zipOut);
                    zipOut.closeEntry();
                }
            }
        };

        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"archive.zip\"")
                .header(HttpHeaders.CONTENT_TYPE, "application/zip")
                .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(zipTotalSize))
                .body(responseBody);
    }

    // 自定义空输出流,仅处理压缩计算
    private static class NullOutputStream extends OutputStream {
        @Override
        public void write(int b) {}
        @Override
        public void write(byte[] b) {}
        @Override
        public void write(byte[] b, int off, int len) {}
    }
}

方案2:先压缩到临时文件,再输出文件(单遍IO)

先将Zip文件写入临时磁盘文件,获取文件大小后再输出到响应流,这种方式仅需遍历一次文件,但会占用临时磁盘空间。

实现步骤

  1. 创建临时文件,将所有目标文件压缩到该临时文件;
  2. 获取临时文件的大小,设置Content-Length响应头;
  3. 将临时文件作为Resource返回,同时注册自动删除钩子避免磁盘泄漏。

代码示例

import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.List;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@RestController
public class ZipDownloadController {

    @GetMapping("/download-zip")
    public ResponseEntity<Resource> downloadZip() throws IOException {
        // 替换为你要压缩的文件列表
        List<File> filesToZip = Arrays.asList(new File("file1.txt"), new File("file2.jpg"));

        // 创建临时Zip文件
        File tempZipFile = File.createTempFile("temp-archive", ".zip");
        try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(tempZipFile))) {
            zipOut.setLevel(Deflater.DEFAULT_COMPRESSION);
            for (File file : filesToZip) {
                ZipEntry entry = new ZipEntry(file.getName());
                zipOut.putNextEntry(entry);
                Files.copy(file.toPath(), zipOut);
                zipOut.closeEntry();
            }
        }

        // 构建响应资源
        FileSystemResource zipResource = new FileSystemResource(tempZipFile);
        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"archive.zip\"");
        headers.add(HttpHeaders.CONTENT_TYPE, "application/zip");
        headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(zipResource.contentLength()));

        // 注册JVM退出时删除临时文件
        tempZipFile.deleteOnExit();

        return ResponseEntity.ok()
                .headers(headers)
                .body(zipResource);
    }
}

两种方案对比

方案优点缺点适用场景
预计算大小无磁盘占用,无需临时文件需遍历文件两次,IO开销大小文件、磁盘空间紧张时
临时文件输出仅遍历一次文件,IO效率高占用磁盘临时空间大文件、磁盘充足时

注意事项

  1. 预计算时必须保证压缩级别、算法和正式压缩完全一致,否则计算出的大小会和实际Zip文件大小不符;
  2. 如果目标文件在预计算和正式压缩之间被修改,会导致大小偏差,需确保文件在下载过程中不会被修改;
  3. 临时文件方案中,deleteOnExit()仅在JVM正常退出时生效,若服务异常崩溃可能残留文件,可配合定时任务清理临时目录。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 12:45:31