【问题标题】:How to return a zip file stream using Java Springboot如何使用 Java Spring Boot 返回一个 zip 文件流
【发布时间】:2021-09-18 18:14:19
【问题描述】:

我用的是Springboot,我想生成zip文件然后返回前端。

@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 文件(在本地目录中),但是当将流返回到前端时出现 以下错误

spring.HttpMediaTypeNotAcceptableException: Could not find acceptable representation

然后我在谷歌检查并将返回类型更改为ResponseEntity&lt;StreamResponseBody&gt;,但是我应该如何在方法body(...) 中将ZipOutputStream 更改为StreamResponseBody,谷歌中的解决方案是在body() 方法中创建zip 输出流,例如那:

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

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

【问题讨论】:

  • 你最后一个伪代码块看起来不错,有什么问题?
  • @Robert,我已经解决了这个问题,将使用这个伪代码分享我的代码

标签: java spring spring-boot spring-mvc


【解决方案1】:

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

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());

请考虑您需要将返回类型更改为ResponseEntity&lt;byte[]&gt;

【讨论】:

  • byte[] 会留在内存中吗?如果是这样,当大量请求进来时,内存会非常大,甚至OOM。
  • 这可能是个问题
【解决方案2】:

【讨论】:

  • 与@Lorelorelore 的回答相同,导出过程中可能会消耗大量内存。
【解决方案3】:

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

我的解决方案:

将方法重新定义为customService.generateZipStream(ZipOutputStream zipOut),这样我就可以在controller层中使用StreamResponseBody创建一个zip流,然后将其发送到service层,在服务中层,我会做出口。

预设代码如下:

@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

}

希望如果您有类似的问题,它可以帮助您。

【讨论】:

    【解决方案4】:

    如果您使用的是 Spring 3,您可以使用很多 swagger 注释接口来帮助干净地构建您的ResponseEntity,同时正确使用StreamingResponseBody对 spring 预期的格式进行原型制作。

    这里的body 代码是将ZipOutputStream 流映射到控制器期望返回的StreamingResponseBody 类型的一种缩短方法(.body(out -&gt; {...}) 在下面的代码中执行此操作)。

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

        @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 对象中。关键是,您构建了使用控制器提供的 responseOutputStream 播种的 ZIP 流。完全构建完 zip 后,请确保将其关闭(在我的情况下为 zos.close())。然后将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 流中即可。在我的例子中,我正在提取列表/数组数据,将其放入 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
        }
    

    【讨论】:

    • 在流/压缩过程中是否有任何错误通知客户端?似乎进入 catch 块不会影响响应以指示除了关闭资源之外的任何内容,因为响应已经提交!!!!!!在您的示例中,说roles 以某种方式压缩失败?
    猜你喜欢
    • 2018-02-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-01
    • 2017-06-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多