如何使用 Java Springboot return 压缩文件流

How to return a zip file stream using Java Springboot

我用的是Springboot,我想生成zip文件然后return到前端

@PostMapping(value="/export", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<ZipOutputStream> export() {
    // customService.generateZipStream() is a service method that can 
    //generate zip file using ZipOutputStream and then return this stream
    ZipOutputStream zipOut = customService.generateZipStream();
    return ResponseEntity
                  .ok()
                  .header("Content-Disposition", "attachment;filename=export.zip")
                  .header("Content-Type","application/octet-stream")
                  .body(zipOut)
}

可以正确生成 zip 文件(在本地目录中)但是当 return 流到前端时我得到 以下错误:

spring.HttpMediaTypeNotAcceptableException: Could not find acceptable representation

然后我检查了 google 并将 return 类型更改为 ResponseEntity<StreamResponseBody>,但是我应该如何在方法 [=18] 中将 ZipOutputStream 更改为 StreamResponseBody =],google 中的解决方案是在 body() 方法中创建 zip 输出流,如下所示:

   // pseudocode
   .body(out -> { 
                   ZipOutputStream zipOut = new ZipOutputStream(out));
                   zipOut.putEntry(...);
                   zipOut.write(...);
                   zipOut.closeEntry();
                   ... balabala
                }

我的问题 是如何在这种情况下使用 StreamResponseBody return 的任何替代解决方案zip 流可能有点大。

您可以尝试将其作为字节数组发回:

ByteArrayOutputStream bos = new ByteArrayOutputStream();

ZipOutputStream zipOut = customService.generateZipStream();

int count;
byte data[] = new byte[2048];
BufferedInputStream entryStream = new BufferedInputStream(is, 2048);
while ((count = entryStream.read(data, 0, 2048)) != -1) {
    zos.write( data, 0, count );
}
entryStream.close();

return ResponseEntity
              .ok()
              .header("Content-Disposition", "attachment;filename=export.zip")
              .header("Content-Type","application/octet-stream")
              .body(bos.toByteArray());

考虑到您需要将 return 类型更改为 ResponseEntity<byte[]>

也许你可以returnResponseEntity<Byte[]>

https://github.com/wangwei-ying/initializr/blob/main/initializr-web/src/main/java/io/spring/initializr/web/controller/ProjectGenerationController.java

行:126

感谢大家的帮助,我检查了你的答案并通过更新导出逻辑解决了这个问题。

我的解决方案:

将方法重新定义为customService.generateZipStream(ZipOutputStream zipOut)这样我就可以在controller层用StreamResponseBody创建一个压缩流然后发送到service层,在service层,我会做export.

预设代码如下:

@PostMapping(value="/export", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity< StreamResponseBody > export() {
    // customService.generateZipStream() is a service method that can 
    //generate zip file using ZipOutputStream and then return this stream
    
    return ResponseEntity
                  .ok()
                  .header("Content-Disposition", "attachment;filename=export.zip")
                  .body(outputStream -> {
                     // Use inner implement and set StreamResponseBody to ZipOutputStream
                     try(ZipOutputStream zipOut = new ZipOutputStream(outputStream)) {
                         customService.generateZipStream(zipOut);
                     }
                  });
}

customService 前导码:

public void generateZipStream(ZipOutputStream zipOut) {
    // ... do export here
    zipOut.putEntry(...);
    zipOut.write(...);
    zipOut.closeEntry();
    // ... balabala

}

希望有类似问题的可以帮到你

如果你正在使用Spring 3,你可以使用很多swagger注释接口来帮助干净地构建您的 ResponseEntity 在使用 StreamingResponseBody 正确原型化 spring 所期望的格式时。

此处的 body 代码是将 ZipOutputStream 流映射到控制器期望 return 的 StreamingResponseBody 类型的简化方法(.body(out -> {...})这在我下面的代码中)。

[控制器] 代码如下所示:

    @GetMapping(value = "/myZip")
    @Operation(
        summary = "Retrieves a ZIP file from the system given a proper request ID.",
        responses = {
            @ApiResponse(
                description = "Get ZIP file containing data for the ID.",
                responseCode = "200",
                content = @Content(schema = @Schema(implementation = StreamingResponseBody.class))),
            @ApiResponse(
                description = "Unauthenticated",
                responseCode = "401",
                content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))),
            @ApiResponse(
                description = "Forbidden. Access Denied.",
                responseCode = "403",
                content = @Content(schema = @Schema(implementation = ApiErrorResponse.class)))
        })
    public ResponseEntity<StreamingResponseBody> myZipBuilder(@RequestParam String id, HttpServletResponse response)
        throws IOException {
        final String fileName = "MyRequest_" + id + "_" + new SimpleDateFormat("MMddyyyy").format(new Date());

        return ResponseEntity.ok()
            .header(CONTENT_DISPOSITION,"attachment;filename=\"" + fileName + ".zip\"")
            .contentType(MediaType.valueOf("application/zip"))
            .body(out -> myZipService.build(id, response.getOutputStream()));
    }

您的服务代码 build 方法只需要接收数据所需的任何参数,加上 您的 ServletOutputStream responseOutputStream 参数以允许您构建由该流播种的 ZipOutputStream 对象。

在我下面的小示例中,您可以看到我在 buildDataLists 方法(未显示)中构建了一些 CSV 数据,这只是一个 List.. 然后我将每个的顶级列表项,并使用我的 streamWriteCsvToZip 将它们推送到 ZipOutputStream 对象中。关键是,您构建 ZIP 流时使用控制器提供的 responseOutputStream 作为种子。完全构建 zip 后,请确保将其关闭(在我的例子中 zos.close())。然后return将zos对象传给控制器。

    /**
     * Get ZIP file containing datafiles for a given request id
     *
     * @param id of the request
     * @param responseOutputStream for streaming the zip results
     * @return ZipOutputStream a ZIP file stream for the contents
     * @throws AccessDeniedException    if user does not have access to this function
     * @throws UnauthenticatedException if user is not authenticated
     */
    public ZipOutputStream build(String id, ServletOutputStream responseOutputStream) throws IOException {

        try {
            List<List<String[]>> csvFilesContents = buildDataLists(id);

            final ZipOutputStream zos = new ZipOutputStream(responseOutputStream);
            streamWriteCsvToZip("control", id, zos, csvFilesContents.remove(0));
            streamWriteCsvToZip("roles", id, zos, csvFilesContents.remove(0));
            streamWriteCsvToZip("accounts", id, zos, csvFilesContents.remove(0));

            zos.close(); // finally closing the ZipOutputStream to mark completion of ZIP file
            return zos;
        } catch (IOException | ClientException ex) {
            throw ex;
        }
    }

这里没有魔法。只需获取您的数据并将其放入 zip 流中即可。就我而言,我正在提取 list/array 数据,将其放入 CSV 中,然后将该 CSV 放入 zip 中(作为使用 zos.putNextEntry(entry); 的条目)。 CSV 和 ZIP 都保存为流,因此在此操作期间不会向文件系统写入任何内容,最终结果可以由控制器流出。确保每次向 zip 输出流写入一个条目时关闭该条目 (zos.closeEntry())。


    private void streamWriteCsvToZip(String csvName, String id, ZipOutputStream zos, List<String[]> csvFileContents)
        throws IOException {
        String filename = id + "_" + csvName + ".csv";
        ZipEntry entry = new ZipEntry(filename); // create a zip entry and add it to ZipOutputStream
        zos.putNextEntry(entry);

        CSVWriter csvWriter = new CSVWriter(new OutputStreamWriter(zos));  // Directly write bytes to the output stream
        csvWriter.writeAll(csvFileContents);  // write the contents
        csvWriter.flush(); // flush the writer
        zos.closeEntry(); // close the entry. Note: not closing the zos just yet as we need to add more files to our ZIP
    }