1. 概述
有時我們需要允許我們的 REST API 下載 ZIP 壓縮包。這對於減少網絡負載非常有用。然而,我們可能會在端點上的默認配置下遇到下載文件的困難。
在本文中,我們將學習如何使用 @RequestMapping 註解從我們的端點生成 ZIP 文件,並探索如何從這些端點提供 ZIP 壓縮包。
2. 以字節數組形式創建 ZIP 歸檔文件
通過創建 ZIP 歸檔文件並將其作為 HTTP 響應返回,是提供 ZIP 文件的一種方式。 讓我們創建一個 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 註解的快捷方式。在 produces 屬性中,我們選擇 application/zip,這是一個 ZIP 文件的 MIME 類型。然後,我們用 ByteArrayOutputStream 包裝 ZipOutputStream,並將所有需要的文件添加到其中。最後,我們設置 Content-Disposition 頭部值為 attachment,這樣在調用後我們就能下載我們的歸檔文件。
現在,讓我們實現 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 文件保存在內存中。
3. 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");
}我們已成功檢索到歸檔文件,並且所有文件都已在那裏找到。
4. 控制歸檔壓縮
當使用 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 之間的值。 較低的壓縮級別可以加快處理速度,而較高的級別則會產生更小的輸出,但會降低歸檔速度。
5. 添加 ZIP 歸檔密碼保護
我們還可以為我們的 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。
6. 結論
在本教程中,我們探討了兩種在 Spring Boot 應用程序中提供 ZIP 文件的方法。對於小型到中等大小的歸檔文件,我們可以使用字節數組。對於較大的文件,我們應該直接將 ZIP 歸檔文件流式傳輸到 HTTP 響應中,以降低內存使用率。通過調整壓縮級別,我們可以控制網絡負載和端點的延遲。