有时我们可能需要允许 REST API 下载 ZIP 档案。这对于减少网络负载很有用。但是,使用终端上的默认配置下载文件时,我们可能会遇到困难。
在本教程中,我们探讨了在 Spring Boot 应用程序中提供 ZIP 文件的两种方法。
- 对于中小型档案,我们可以使用字节数组。
- 对于较大的文件,我们应该考虑在 HTTP 响应中直接流式传输 ZIP 档案,以保持较低的内存使用率。
通过调整压缩级别,我们可以控制网络负载和端点的延迟。在本文中,我们将看到如何使用@RequestMapping注释从我们的端点生成 ZIP 文件,并且我们将探索一些从它们提供 ZIP 档案的方法。
将 Zip 存档压缩为字节数组
提供 ZIP 文件的第一种方法是将其创建为字节数组并在 HTTP 响应中返回。让我们使用返回存档字节的端点创建 REST 控制器:
@RestController public class ZipArchiveController { @GetMapping(value = "/zip-archive", produces = "application/zip") public ResponseEntity<byte[]> getZipBytes() throws IOException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(byteArrayOutputStream); ZipOutputStream zipOutputStream = new ZipOutputStream(bufferedOutputStream); addFilesToArchive(zipOutputStream); IOUtils.closeQuietly(bufferedOutputStream); IOUtils.closeQuietly(byteArrayOutputStream); return ResponseEntity .ok() .header("Content-Disposition", "attachment; filename=\"files.zip\"") .body(byteArrayOutputStream.toByteArray()); } }
|
我们使用@GetMapping作为@RequestMapping 注释的快捷方式。在produce属性中,我们选择application/zip,这是 ZIP 档案的 MIME 类型。然后我们用 ZipOutputStream包装ByteArrayOutputStream并在其中添加所有需要的文件。最后,我们用附件值设置Content-Disposition标头,这样我们就可以在调用后下载档案。现在,让我们实现addFilesToArchive()方法:
void addFilesToArchive(ZipOutputStream zipOutputStream) throws IOException { List<String> filesNames = new ArrayList<>(); filesNames.add("first-file.txt"); filesNames.add("second-file.txt"); for (String fileName : filesNames) { File file = new File(ZipArchiveController.class.getClassLoader() .getResource(fileName).getFile()); zipOutputStream.putNextEntry(new ZipEntry(file.getName())); FileInputStream fileInputStream = new FileInputStream(file); IOUtils.copy(fileInputStream, zipOutputStream); fileInputStream.close(); zipOutputStream.closeEntry(); } zipOutputStream.finish(); zipOutputStream.flush(); IOUtils.closeQuietly(zipOutputStream); }
|
在这里,我们只需用资源文件夹中的几个文件填充档案。最后,让我们调用我们的端点并检查是否返回了所有文件:
@WebMvcTest(ZipArchiveController.class) public class ZipArchiveControllerUnitTest { @Autowired MockMvc mockMvc; @Test void givenZipArchiveController_whenGetZipArchiveBytes_thenExpectedArchiveShouldContainExpectedFiles() throws Exception { MvcResult result = mockMvc.perform(get("/zip-archive")) .andReturn(); MockHttpServletResponse response = result.getResponse(); byte[] content = response.getContentAsByteArray(); List<String> fileNames = fetchFileNamesFromArchive(content); assertThat(fileNames) .containsExactly("first-file.txt", "second-file.txt"); } List<String> fetchFileNamesFromArchive(byte[] content) throws IOException { InputStream byteStream = new ByteArrayInputStream(content); ZipInputStream zipStream = new ZipInputStream(byteStream); List<String> fileNames = new ArrayList<>(); ZipEntry entry; while ((entry = zipStream.getNextEntry()) != null) { fileNames.add(entry.getName()); zipStream.closeEntry(); } return fileNames; } }
|
正如响应中所预期的那样,我们从终端获得了 ZIP 存档。我们从那里解压了所有文件,并仔细检查了所有预期文件是否都已到位。对于较小的文件,我们可以使用此方法,但较大的文件可能会导致堆消耗问题。这是因为ByteArrayInputStream将整个 ZIP 文件保存在内存中。
将 Zip 存档作为流
对于较大的档案,我们应避免将所有内容加载到内存中。相反,我们可以在创建 ZIP 文件时将其直接传输到客户端。这可以减少内存消耗,并使我们能够高效地提供大型文件。
让我们在控制器上创建另一个端点:
@GetMapping(value = "/zip-archive-stream", produces = "application/zip") public ResponseEntity<StreamingResponseBody> getZipStream() { return ResponseEntity .ok() .header("Content-Disposition", "attachment; filename=\"files.zip\"") .body(out -> { ZipOutputStream zipOutputStream = new ZipOutputStream(out); addFilesToArchive(zipOutputStream); }); }
|
我们在这里使用了Servlet 输出流而不是ByteArrayInputStream,因此我们所有的文件都将流式传输到客户端,而无需完全存储在内存中。让我们调用这个端点并检查它是否返回我们的文件:
@Test void givenZipArchiveController_whenGetZipArchiveStream_thenExpectedArchiveShouldContainExpectedFiles() throws Exception { MvcResult result = mockMvc.perform(get("/zip-archive-stream")) .andReturn(); MockHttpServletResponse response = result.getResponse(); byte[] content = response.getContentAsByteArray(); List<String> fileNames = fetchFileNamesFromArchive(content); assertThat(fileNames) .containsExactly("first-file.txt", "second-file.txt"); }
|
我们成功检索了档案并且所有文件都已找到。控制档案压缩
当我们使用ZipOutputStream时,它已经提供了压缩功能。我们可以使用zipOutputStream.setLevel()方法调整压缩级别。
让我们修改其中一个端点代码来设置压缩级别:
@GetMapping(value = "/zip-archive-stream", produces = "application/zip") public ResponseEntity<StreamingResponseBody> getZipStream() { return ResponseEntity .ok() .header("Content-Disposition", "attachment; filename=\"files.zip\"") .body(out -> { ZipOutputStream zipOutputStream = new ZipOutputStream(out); zipOutputStream.setLevel(9); addFilesToArchive(zipOutputStream); }); }
|
我们将压缩级别设置为9,这是最大压缩级别。我们可以在0到9之间选择一个值。较低的压缩级别可加快处理速度,而较高的压缩级别会产生较小的输出,但会减慢存档速度。添加存档密码保护
我们还可以为 ZIP 档案设置密码。为此,让我们添加zip4j 依赖项:
<dependency> <groupId>net.lingala.zip4j</groupId> <artifactId>zip4j</artifactId> <version>${zip4j.version}</version> </dependency>
|
现在我们将向控制器添加一个新的端点,在那里返回密码加密的存档流:import net.lingala.zip4j.io.outputstream.ZipOutputStream; @GetMapping(value = "/zip-archive-stream-secured", produces = "application/zip") public ResponseEntity<StreamingResponseBody> getZipSecuredStream() { return ResponseEntity .ok() .header("Content-Disposition", "attachment; filename=\"files.zip\"") .body(out -> { ZipOutputStream zipOutputStream = new ZipOutputStream(out, "password".toCharArray()); addFilesToArchive(zipOutputStream); }); }
|
这里我们使用了zip4j 库中的ZipOutputStream ,它可以处理密码。现在让我们实现addFilesToArchive()方法:
import net.lingala.zip4j.model.ZipParameters; void addFilesToArchive(ZipOutputStream zipOutputStream) throws IOException { List<String> filesNames = new ArrayList<>(); filesNames.add("first-file.txt"); filesNames.add("second-file.txt"); ZipParameters zipParameters = new ZipParameters(); zipParameters.setCompressionMethod(CompressionMethod.DEFLATE); zipParameters.setEncryptionMethod(EncryptionMethod.ZIP_STANDARD); zipParameters.setEncryptFiles(true); for (String fileName : filesNames) { File file = new File(ZipArchiveController.class.getClassLoader() .getResource(fileName).getFile()); zipParameters.setFileNameInZip(file.getName()); zipOutputStream.putNextEntry(zipParameters); FileInputStream fileInputStream = new FileInputStream(file); IOUtils.copy(fileInputStream, zipOutputStream); fileInputStream.close(); zipOutputStream.closeEntry(); } zipOutputStream.flush(); IOUtils.closeQuietly(zipOutputStream); }
|
我们使用ZIP 条目的EncryptionMethod和EncryptFiles参数来加密文件。最后,让我们调用新的端点并检查响应:
@Test void givenZipArchiveController_whenGetZipArchiveSecuredStream_thenExpectedArchiveShouldContainExpectedFilesSecuredByPassword() throws Exception { MvcResult result = mockMvc.perform(get("/zip-archive-stream-secured")) .andReturn(); MockHttpServletResponse response = result.getResponse(); byte[] content = response.getContentAsByteArray(); List<String> fileNames = fetchFileNamesFromArchive(content); assertThat(fileNames) .containsExactly("first-file.txt", "second-file.txt"); }
|
在fetchFileNamesFromArchive()中,我们将实现从 ZIP 存档中检索数据的逻辑:import net.lingala.zip4j.io.inputstream.ZipInputStream; List<String> fetchFileNamesFromArchive(byte[] content) throws IOException { InputStream byteStream = new ByteArrayInputStream(content); ZipInputStream zipStream = new ZipInputStream(byteStream, "password".toCharArray()); List<String> fileNames = new ArrayList<>(); LocalFileHeader entry = zipStream.getNextEntry(); while (entry != null) { fileNames.add(entry.getFileName()); entry = zipStream.getNextEntry(); } zipStream.close(); return fileNames; }
|
这里我们再次使用zip4j 库中的ZipInputStream并设置我们在加密时使用的密码。否则,我们将遇到ZipException。