知識庫 / Spring / Spring Boot RSS 訂閱

使用 Spring Boot @RequestMapping 部署 ZIP 文件指南

Spring Boot
HongKong
4
10:56 AM · Dec 06 ,2025

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, 從而獲得最大壓縮級別。我們可以選擇一個介於 09 之間的值。 較低的壓縮級別可以加快處理速度,而較高的級別則會產生更小的輸出,但會降低歸檔速度。

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 條目的 encryptionMethodencryptFiles 參數對文件進行加密。

最後,我們調用新的端點並檢查響應:

@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 響應中,以降低內存使用率。通過調整壓縮級別,我們可以控制網絡負載和端點的延遲。

發佈 評論

Some HTML is okay.