【问题标题】:Java zip files from streams instantly without using byte[]无需使用 byte[] 即可立即从流中提取 Java zip 文件
【发布时间】:2019-05-21 23:11:30
【问题描述】:

我想将多个文件压缩成一个zip文件,我正在处理大文件,然后将它们下载到客户端,目前我正在使用这个:

@RequestMapping(value = "/download", method = RequestMethod.GET, produces = "application/zip")
public ResponseEntity <StreamingResponseBody> getFile() throws Exception {
    File zippedFile = new File("test.zip");
    FileOutputStream fos = new FileOutputStream(zippedFile);
    ZipOutputStream zos = new ZipOutputStream(fos);
    InputStream[] streams = getStreamsFromAzure();
    for (InputStream stream: streams) {
        addToZipFile(zos, stream);
    }
    final InputStream fecFile = new FileInputStream(zippedFile);
    Long fileLength = zippedFile.length();
    StreamingResponseBody stream = outputStream - >
        readAndWrite(fecFile, outputStream);

    return ResponseEntity.ok()
        .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_DISPOSITION)
        .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + "download.zip")
        .contentLength(fileLength)
        .contentType(MediaType.parseMediaType("application/zip"))
        .body(stream);
}

private void addToZipFile(ZipOutputStream zos, InputStream fis) throws IOException {
    ZipEntry zipEntry = new ZipEntry(generateFileName());
    zos.putNextEntry(zipEntry);
    byte[] bytes = new byte[1024];
    int length;
    while ((length = fis.read(bytes)) >= 0) {
        zos.write(bytes, 0, length);
    }
    zos.closeEntry();
    fis.close();
}

在所有文件被压缩然后开始下载之前这需要很长时间,对于大文件,这个 kan 需要很多时间,这是造成延迟的行:

while ((length = fis.read(bytes)) >= 0) {
    zos.write(bytes, 0, length);
}

那么有没有办法在压缩文件时立即下载文件?

【问题讨论】:

    标签: java spring download stream zip


    【解决方案1】:

    试试这个。与其使用ZipOutputStream 包装FileOutputStream,不如将​​您的zip 写入文件,然后将其复制到客户端输出流,而只需使用ZipOutputStream 包装客户端输出流,这样当您添加zip 条目时并将数据直接发送给客户端。如果您想将其存储到服务器上的文件中,那么您可以让 ZipOutputStream 写入拆分输出流,同时写入两个位置。

    @RequestMapping(value = "/download", method = RequestMethod.GET, produces = "application/zip")
    public ResponseEntity<StreamingResponseBody> getFile() throws Exception {
    
        InputStream[] streamsToZip = getStreamsFromAzure();
    
        // You could cache already created zip files, maybe something like this:
        //   String[] pathsOfResourcesToZip = getPathsFromAzure();
        //   String zipId = getZipId(pathsOfResourcesToZip);
        //   if(isZipExist(zipId))
        //     // return that zip file
        //   else do the following
    
        StreamingResponseBody streamResponse = clientOut -> {
            FileOutputStream zipFileOut = new FileOutputStream("test.zip");
    
            ZipOutputStream zos = new ZipOutputStream(new SplitOutputStream(clientOut, zipFileOut));
            for (InputStream in : streamsToZip) {
                addToZipFile(zos, in);
            }
        };
    
        return ResponseEntity.ok()
                .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_DISPOSITION)
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + "download.zip")
                .contentType(MediaType.parseMediaType("application/zip")).body(streamResponse);
    }
    
    
    private void addToZipFile(ZipOutputStream zos, InputStream fis) throws IOException {
        ZipEntry zipEntry = new ZipEntry(generateFileName());
        zos.putNextEntry(zipEntry);
        byte[] bytes = new byte[1024];
        int length;
        while ((length = fis.read(bytes)) >= 0) {
            zos.write(bytes, 0, length);
        }
        zos.closeEntry();
        fis.close();
    }
    

    public static class SplitOutputStream extends OutputStream {
        private final OutputStream out1;
        private final OutputStream out2;
    
        public SplitOutputStream(OutputStream out1, OutputStream out2) {
            this.out1 = out1;
            this.out2 = out2;
        }
    
        @Override public void write(int b) throws IOException {
            out1.write(b);
            out2.write(b);
        }
    
        @Override public void write(byte b[]) throws IOException {
            out1.write(b);
            out2.write(b);
        }
    
        @Override public void write(byte b[], int off, int len) throws IOException {
            out1.write(b, off, len);
            out2.write(b, off, len);
        }
    
        @Override public void flush() throws IOException {
            out1.flush();
            out2.flush();
        }
    
        /** Closes all the streams. If there was an IOException this throws the first one. */
        @Override public void close() throws IOException {
            IOException ioException = null;
            for (OutputStream o : new OutputStream[] {
                    out1,
                    out2 }) {
                try {
                    o.close();
                } catch (IOException e) {
                    if (ioException == null) {
                        ioException = e;
                    }
                }
            }
            if (ioException != null) {
                throw ioException;
            }
        }
    }
    

    对于要压缩的一组资源的第一次请求,您不会知道生成的 zip 文件的大小,因此您无法将长度与响应一起发送,因为您是在压缩文件时对其进行流式传输。

    但是,如果您希望重复请求压缩同一组资源,那么您可以缓存您的 zip 文件并在任何后续请求中简单地返回它们;您还将知道缓存的 zip 文件的长度,以便您也可以在响应中发送它。

    如果您想这样做,那么您必须能够为要压缩的资源的每个组合始终创建相同的标识符,以便您可以检查这些资源是否已经压缩并返回缓存文件,如果它们是。您也许可以对将被压缩的资源的 ID(可能是完整路径)进行排序,然后将它们连接起来为 zip 文件创建一个 ID。

    【讨论】:

    • 谢谢这是工作我可以在chrome的网络选项卡中看到正在下载的文件,并在文件完全下载后出现,所以我需要在客户端显示下载进度所以我需要要知道要设置的文件大小(长度) .contentLength(fileLength) 那么有没有办法获取 zip 文件的大小?
    • 不幸的是,您在流式传输之前无法知道 zip 的大小,因为您在流式传输的同时对其进行压缩。这是您必须为这种额外效率做出的牺牲。但是,一旦您处理了一个请求并创建了 zip 文件,您就可以将该缓存文件用于任何重复的请求,并且您将知道它的大小。
    • @AbdennacerLachiheb 您可以尝试根据您预期 zip 的大小来确定您的进度,并且可以随着进度的继续更新您的估计。取决于您使用的压缩算法,它可能在原始大小的 70% 左右,但实际上也取决于您要压缩的内容。
    猜你喜欢
    • 2022-11-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-10
    • 2019-06-21
    • 2018-08-23
    • 2018-06-24
    • 2021-08-17
    相关资源
    最近更新 更多