立即从流中提取 Java zip 文件,无需使用 byte[]

Abd*_*heb 6 java zip spring download stream

我想将多个文件压缩成一个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();
}
Run Code Online (Sandbox Code Playgroud)

在压缩所有文件然后开始下载之前,这需要花费大量时间,对于大文件,这可能需要花费大量时间,这是造成延迟的行:

while ((length = fis.read(bytes)) >= 0) {
    zos.write(bytes, 0, length);
}
Run Code Online (Sandbox Code Playgroud)

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

xtr*_*tic 5

试试这个吧。不要使用 来ZipOutputStream包装 a 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();
}
Run Code Online (Sandbox Code Playgroud)
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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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