有時我們可能需要允許 REST API 下載 ZIP 檔案。這對於減少網路負載很有用。但是,使用終端上的預設配置下載檔案時,我們可能會遇到困難。
在本教程中,我們探討了在 Spring Boot 應用程式中提供 ZIP 檔案的兩種方法。
- 對於中小型檔案,我們可以使用位元組陣列。
- 對於較大的檔案,我們應該考慮在 HTTP 響應中直接流式傳輸 ZIP 檔案,以保持較低的記憶體使用率。
透過調整壓縮級別,我們可以控制網路負載和端點的延遲。在本文中,我們將看到如何使用@RequestMapping註釋從我們的端點生成 ZIP 檔案,並且我們將探索一些從它們提供 ZIP 檔案的方法。
將 Zip 存檔壓縮為位元組陣列
提供 ZIP 檔案的第一種方法是將其建立為位元組陣列並在 HTTP 響應中返回。讓我們使用返回存檔位元組的端點建立 REST 控制器:
@RestController public class ZipArchiveController { @GetMapping(value = <font>"/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(<font>"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(<font>"/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 = <font>"/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(<font>"/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 = <font>"/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 = <font>"/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(<font>"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(<font>"/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, <font>"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。