Spring Boot中用@RequestMapping 提供 Zip 檔案

banq發表於2024-10-18

有時我們可能需要允許 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=\&#34files.zip\&#34")
          .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=\&#34files.zip\&#34")
      .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=\&#34files.zip\&#34")
      .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=\&#34files.zip\&#34")
      .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。

 

相關文章