1. 概述
有时我们需要让 REST API 支持下载 ZIP 压缩包,这能有效减少网络传输负担。但直接使用默认配置开发这类接口时,可能会踩一些坑。
本文将演示如何通过 @RequestMapping
注解让接口输出 ZIP 文件,并探讨几种实现方案。
2. 字节数组方式生成 ZIP
最直接的方式是将 ZIP 文件转为字节数组直接返回。先创建一个返回字节流的 REST 接口:
@GetMapping(value = "/zip-as-byte", produces = "application/zip")
public ResponseEntity<byte[]> getZipFileAsByteArray() throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream);
addFilesToArchive(zipOutputStream);
zipOutputStream.close();
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=files.zip")
.body(byteArrayOutputStream.toByteArray());
}
关键点解析:
- 使用
@GetMapping
简化@RequestMapping
配置 produces
指定application/zip
MIME 类型- 通过
ByteArrayOutputStream
+ZipOutputStream
构建压缩包 - 设置
Content-Disposition
头实现下载功能
实现 addFilesToArchive()
方法添加文件:
private void addFilesToArchive(ZipOutputStream zipOutputStream) throws IOException {
Resource resource1 = new ClassPathResource("test-file1.txt");
Resource resource2 = new ClassPathResource("test-file2.txt");
addToZipFile(resource1, zipOutputStream);
addToZipFile(resource2, zipOutputStream);
}
private void addToZipFile(Resource resource, ZipOutputStream zipOutputStream) throws IOException {
ZipEntry zipEntry = new ZipEntry(resource.getFilename());
zipOutputStream.putNextEntry(zipEntry);
StreamUtils.copy(resource.getInputStream(), zipOutputStream);
zipOutputStream.closeEntry();
}
测试接口:
@Test
public void whenDownloadZipAsByteArray_thenSuccess() throws IOException {
ResponseEntity<byte[]> response = restTemplate.getForEntity("/zip-as-byte", byte[].class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals("attachment; filename=files.zip",
response.getHeaders().getFirst("Content-Disposition"));
File zipFile = new File("downloaded-byte-array.zip");
Files.write(zipFile.toPath(), response.getBody());
List<String> fileNames = fetchFileNamesFromArchive(zipFile);
assertTrue(fileNames.contains("test-file1.txt"));
assertTrue(fileNames.contains("test-file2.txt"));
}
⚠️ 注意:这种方式只适合小文件,大文件会吃光堆内存!因为 ByteArrayInputStream
会把整个 ZIP 文件加载到内存中。
3. 流式传输 ZIP
对于大文件,应该避免全量加载到内存。更优雅的方案是边生成边传输:
@GetMapping(value = "/zip-as-stream", produces = "application/zip")
public void getZipFileAsStream(HttpServletResponse response) throws IOException {
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename=files.zip");
ZipOutputStream zipOutputStream = new ZipOutputStream(response.getOutputStream());
addFilesToArchive(zipOutputStream);
zipOutputStream.close();
}
核心改进:
- 直接使用
HttpServletResponse
的输出流 - 数据直接流向客户端,不占用堆内存
- 适合 GB 级大文件传输
测试验证:
@Test
public void whenDownloadZipAsStream_thenSuccess() throws IOException {
ResponseEntity<byte[]> response = restTemplate.getForEntity("/zip-as-stream", byte[].class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals("attachment; filename=files.zip",
response.getHeaders().getFirst("Content-Disposition"));
File zipFile = new File("downloaded-stream.zip");
Files.write(zipFile.toPath(), response.getBody());
List<String> fileNames = fetchFileNamesFromArchive(zipFile);
assertTrue(fileNames.contains("test-file1.txt"));
assertTrue(fileNames.contains("test-file2.txt"));
}
4. 压缩级别控制
ZipOutputStream
默认启用压缩,我们可以通过 setLevel()
调整压缩强度:
@GetMapping(value = "/zip-with-compression", produces = "application/zip")
public void getZipWithCompression(HttpServletResponse response) throws IOException {
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename=compressed.zip");
ZipOutputStream zipOutputStream = new ZipOutputStream(response.getOutputStream());
zipOutputStream.setLevel(9); // 最高压缩级别
addFilesToArchive(zipOutputStream);
zipOutputStream.close();
}
压缩级别对照表: | 级别 | 压缩率 | 速度 | 适用场景 | |------|--------|------|----------| | 0 | 无压缩 | 最快 | 临时文件 | | 1-3 | 低 | 快 | 实时传输 | | 4-6 | 中 | 平衡 | 通用场景 | | 7-9 | 高 | 慢 | 长期存储 |
⚡ 简单粗暴原则:网络带宽紧张用高压缩,CPU 紧张用低压缩。
5. 密码保护 ZIP
需要添加密码保护时,引入 zip4j 依赖:
<dependency>
<groupId>net.lingala.zip4j</groupId>
<artifactId>zip4j</artifactId>
<version>2.9.1</version>
</dependency>
创建加密接口:
@GetMapping(value = "/zip-with-password", produces = "application/zip")
public void getZipWithPassword(HttpServletResponse response) throws IOException {
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename=secured.zip");
ZipParameters zipParameters = new ZipParameters();
zipParameters.setEncryptFiles(true);
zipParameters.setEncryptionMethod(EncryptionMethod.AES);
ZipOutputStream zipOutputStream = new ZipOutputStream(response.getOutputStream());
zipOutputStream.setPassword("secret123".toCharArray());
addFilesToArchive(zipOutputStream, zipParameters);
zipOutputStream.close();
}
实现加密文件添加:
private void addFilesToArchive(ZipOutputStream zipOutputStream, ZipParameters zipParameters) throws IOException {
Resource resource1 = new ClassPathResource("test-file1.txt");
Resource resource2 = new ClassPathResource("test-file2.txt");
addToZipFile(resource1, zipOutputStream, zipParameters);
addToZipFile(resource2, zipOutputStream, zipParameters);
}
private void addToZipFile(Resource resource, ZipOutputStream zipOutputStream, ZipParameters zipParameters) throws IOException {
zipParameters.setFileNameInZip(resource.getFilename());
zipOutputStream.putNextEntry(zipParameters);
StreamUtils.copy(resource.getInputStream(), zipOutputStream);
zipOutputStream.closeEntry();
}
测试加密包:
@Test
public void whenDownloadZipWithPassword_thenSuccess() throws IOException {
ResponseEntity<byte[]> response = restTemplate.getForEntity("/zip-with-password", byte[].class);
File zipFile = new File("downloaded-secured.zip");
Files.write(zipFile.toPath(), response.getBody());
// 使用密码解压
List<String> fileNames = fetchFileNamesFromArchive(zipFile, "secret123");
assertTrue(fileNames.contains("test-file1.txt"));
assertTrue(fileNames.contains("test-file2.txt"));
}
private List<String> fetchFileNamesFromArchive(File zipFile, String password) throws IOException {
List<String> fileNames = new ArrayList<>();
try (ZipInputStream zipInputStream = new ZipInputStream(Files.newInputStream(zipFile.toPath()))) {
zipInputStream.setPassword(password.toCharArray());
ZipEntry entry;
while ((entry = zipInputStream.getNextEntry()) != null) {
fileNames.add(entry.getName());
}
}
return fileNames;
}
🔑 关键提醒:解压时必须提供相同密码,否则会抛出 ZipException
。
6. 总结
在 Spring Boot 中实现 ZIP 文件下载有两种核心方案:
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
字节数组方式 | 实现简单 | 内存占用高 | 小文件 (<10MB) |
流式传输 | 内存占用低 | 代码稍复杂 | 大文件/高并发场景 |
其他优化点:
- 通过
setLevel()
平衡压缩率与性能 - 使用 zip4j 实现密码保护
- 始终设置
Content-Disposition
头触发下载
根据实际需求选择合适方案,避免内存溢出这个大坑!