Spring REST - 创建ZIP文件并将其发送给客户端

55

我想创建一个包含后端传输的归档文件的ZIP文件,然后将此文件发送给用户。我已经寻找了2天答案,但未找到合适的解决方案,也许你可以帮助我:)

目前的代码如下(我知道我不应该全部放在Spring控制器中,但是这只是为了测试目的,不用担心它,只是为了找到解决方法):

    @RequestMapping(value = "/zip")
    public byte[] zipFiles(HttpServletResponse response) throws IOException {
        // Setting HTTP headers
        response.setContentType("application/zip");
        response.setStatus(HttpServletResponse.SC_OK);
        response.addHeader("Content-Disposition", "attachment; filename=\"test.zip\"");

        // Creating byteArray stream, make it bufferable and passing this buffer to ZipOutputStream
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(byteArrayOutputStream);
        ZipOutputStream zipOutputStream = new ZipOutputStream(bufferedOutputStream);

        // Simple file list, just for tests
        ArrayList<File> files = new ArrayList<>(2);
        files.add(new File("README.md"));

        // Packing files
        for (File file : files) {
            // New zip entry and copying InputStream with file to ZipOutputStream, after all closing streams
            zipOutputStream.putNextEntry(new ZipEntry(file.getName()));
            FileInputStream fileInputStream = new FileInputStream(file);

            IOUtils.copy(fileInputStream, zipOutputStream);

            fileInputStream.close();
            zipOutputStream.closeEntry();
        }

        if (zipOutputStream != null) {
            zipOutputStream.finish();
            zipOutputStream.flush();
            IOUtils.closeQuietly(zipOutputStream);
        }
        IOUtils.closeQuietly(bufferedOutputStream);
        IOUtils.closeQuietly(byteArrayOutputStream);

        return byteArrayOutputStream.toByteArray();
    }

问题是,当我输入URL localhost:8080/zip 时,使用这段代码,我得到一个文件 test.zip.html 而不是 .zip 文件。

当我删除 .html 扩展名,只留下 test.zip 时,它可以正确地打开。所以我的问题是:

  • 如何避免返回这个 .html 扩展名?
  • 为什么会添加它?

我不知道还能做什么。我也尝试用类似以下的东西替换 ByteArrayOuputStream

OutputStream outputStream = response.getOutputStream();

并将方法设置为void,以便它不返回任何内容,但它创建了一个已损坏的.zip文件?

在我的MacBook上解压缩test.zip后,我得到了test.zip.cpgz,然后又得到了test.zip文件,如此往复。

在Windows上,.zip文件已经损坏,甚至无法打开。

我还认为自动删除.html扩展名可能是最好的选择,但如何实现呢?

希望这不像看起来那么难 :)
谢谢


你可能会发现这里的答案有帮助... https://dev59.com/Y2ULtIcB2Jgan1zniGI5#68492465 - Kim Gentes
5个回答

44

问题已经解决。

我进行了替换:

response.setContentType("application/zip");

随着:

@RequestMapping(value = "/zip", produces="application/zip")

现在我得到了一个清晰、美丽的.zip文件。


如果你们中有人有更好、更快的建议,或者只是想给一些意见,那就去吧,我很好奇。


3
对于不平凡的问题并回答自己的问题点个赞!我对这个实现的唯一问题/关注点是最后的返回语句。因为 toByteArray(),我认为这会将整个文件加载到内存中。对吗?如果目录太大,是否有一种流式传输的方法以避免一些 OutOfMemoryException 错误? - taylorcressy
如果您需要任何情况的通用解决方案,请查看此页面:https://dev59.com/cF4c5IYBdhLWcg3wapvz#75595252 - Mavic More

43
@RequestMapping(value="/zip", produces="application/zip")
public void zipFiles(HttpServletResponse response) throws IOException {

    //setting headers  
    response.setStatus(HttpServletResponse.SC_OK);
    response.addHeader("Content-Disposition", "attachment; filename=\"test.zip\"");

    ZipOutputStream zipOutputStream = new ZipOutputStream(response.getOutputStream());

    // create a list to add files to be zipped
    ArrayList<File> files = new ArrayList<>(2);
    files.add(new File("README.md"));

    // package files
    for (File file : files) {
        //new zip entry and copying inputstream with file to zipOutputStream, after all closing streams
        zipOutputStream.putNextEntry(new ZipEntry(file.getName()));
        FileInputStream fileInputStream = new FileInputStream(file);

        IOUtils.copy(fileInputStream, zipOutputStream);

        fileInputStream.close();
        zipOutputStream.closeEntry();
    }    

    zipOutputStream.close();
}

2
因为这个解决方案直接将数据流式传输到响应输出流。 - Jakub
这是Java服务器端代码,如何通过JavaScript在客户端下载它? - Harvey Dent
1
@HarveyDent - 你应该将那作为一个新问题来询问。 - denov
请帮帮我,我被这个问题困扰了整整两天。提前致谢。 - Harvey Dent
你只需要调用URL。在上面的代码中,它将是http://<主机>/zip。 - denov
如果您需要任何情况的通用解决方案,请查看此页面:https://dev59.com/cF4c5IYBdhLWcg3wapvz#75595252 - Mavic More

20
@RequestMapping(value="/zip", produces="application/zip")
public ResponseEntity<StreamingResponseBody> zipFiles() {
    return ResponseEntity
            .ok()
            .header("Content-Disposition", "attachment; filename=\"test.zip\"")
            .body(out -> {
                var zipOutputStream = new ZipOutputStream(out);

                // create a list to add files to be zipped
                ArrayList<File> files = new ArrayList<>(2);
                files.add(new File("README.md"));

                // package files
                for (File file : files) {
                    //new zip entry and copying inputstream with file to zipOutputStream, after all closing streams
                    zipOutputStream.putNextEntry(new ZipEntry(file.getName()));
                    FileInputStream fileInputStream = new FileInputStream(file);

                    IOUtils.copy(fileInputStream, zipOutputStream);

                    fileInputStream.close();
                    zipOutputStream.closeEntry();
                }

                zipOutputStream.close();
            });
}

+1,因为这似乎无法在404上设置状态代码,所以帮助我找出如何在不创建HttpServletResponse的情况下完成此操作。 - Adrian Elder
如果在代码块中出现运行时异常,仍将返回200。 - BabyishTank
附上一篇有关使用StreamingResponseBody处理异常问题的相关帖子 https://stackoverflow.com/q/65557527/5777189 - BabyishTank
如果您需要任何情况的通用解决方案,请查看此页面:https://dev59.com/cF4c5IYBdhLWcg3wapvz#75595252 - Mavic More

4
我正在使用Spring Boot的REST Web服务,并且已经设计了端点,无论是JSON、PDF还是ZIP,始终返回ResponseEntity。我参考了denov在这个问题中的答案以及另一个question,学会了如何将ZipOutputStream转换为byte[]以便将其作为端点输出的输入提供给ResponseEntity。
无论如何,我创建了一个简单的实用程序类,其中包含两种方法,用于下载pdf和zip文件。
@Component
public class FileUtil {
    public BinaryOutputWrapper prepDownloadAsPDF(String filename) throws IOException {
        Path fileLocation = Paths.get(filename);
        byte[] data = Files.readAllBytes(fileLocation);

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.parseMediaType("application/pdf"));
        String outputFilename = "output.pdf";
        headers.setContentDispositionFormData(outputFilename, outputFilename);
        headers.setCacheControl("must-revalidate, post-check=0, pre-check=0");

        return new BinaryOutputWrapper(data, headers); 
    }

    public BinaryOutputWrapper prepDownloadAsZIP(List<String> filenames) throws IOException {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.parseMediaType("application/zip"));
        String outputFilename = "output.zip";
        headers.setContentDispositionFormData(outputFilename, outputFilename);
        headers.setCacheControl("must-revalidate, post-check=0, pre-check=0");

        ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
        ZipOutputStream zipOutputStream = new ZipOutputStream(byteOutputStream);

        for(String filename: filenames) {
            File file = new File(filename); 
            zipOutputStream.putNextEntry(new ZipEntry(filename));           
            FileInputStream fileInputStream = new FileInputStream(file);
            IOUtils.copy(fileInputStream, zipOutputStream);
            fileInputStream.close();
            zipOutputStream.closeEntry();
        }           
        zipOutputStream.close();
        return new BinaryOutputWrapper(byteOutputStream.toByteArray(), headers); 
    }
}

现在,端点可以轻松返回ResponseEntity<?>,如下所示,使用特定于pdfzipbyte[]数据和自定义标头。请注意保留HTML标记。
@GetMapping("/somepath/pdf")
public ResponseEntity<?> generatePDF() {
    BinaryOutputWrapper output = new BinaryOutputWrapper(); 
    try {
        String inputFile = "sample.pdf"; 
        output = fileUtil.prepDownloadAsPDF(inputFile);
        //or invoke prepDownloadAsZIP(...) with a list of filenames
    } catch (IOException e) {
        e.printStackTrace();
        //Do something when exception is thrown
    } 
    return new ResponseEntity<>(output.getData(), output.getHeaders(), HttpStatus.OK); 
}
BinaryOutputWrapper是一个简单的不可变的POJO类,我创建了它,并用private byte[] data;org.springframework.http.HttpHeaders headers;作为字段,以便从实用方法返回dataheaders

4
ByteArrayOutputStream将字节存储在内存中,因此对于大文件,您很快就可能耗尽堆空间。 - Rahul

-1

唯一适用于Spring Boot应用程序的方法(不包含任何硬编码文件路径!)

@GetMapping(value = "/zip-download", produces="application/zip")
public void zipDownload(@RequestParam List<String> name, HttpServletResponse response) throws IOException {

    response.setStatus(HttpServletResponse.SC_OK);
    response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + zipFileName + "\"");

    ZipOutputStream zipOut = new ZipOutputStream(response.getOutputStream());

    for (String fileName : name) {

        // resource content length
        int contentLength = 123;
        // resource input stream
        InputStream stream = InputStream.nullInputStream();

        ZipEntry zipEntry = new ZipEntry(fileName);
        zipEntry.setSize(contentLength);

        zipOut.putNextEntry(zipEntry);
        StreamUtils.copy(stream, zipOut);

        zipOut.closeEntry();
    }

    zipOut.finish();
    zipOut.close();
}


很遗憾,对我来说没有起作用。客户报告了无效的压缩文件错误。 - Saurabh
@Saurabh 可能是因为它是空的,这是很常见的事情。 - undefined
是的,那就是问题所在。对于我的负评,我很抱歉。由于Stackoverflow的限制,我现在无法更改它。谢谢。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接