浅谈Java Zip 压缩及其优化
作者:sp42
压缩文件
Java 压缩文件,就是输入多个文件的参数,最终压缩为一个 zip 文件。这个代码比较简单就不张贴了。
压缩目录
压缩目录的话显然较复杂一点,其中一个思路自然是通过递归目录实现的。网上找过几个例子都有点小问题,还是谷歌找出来的靠谱。主要是增加了指定文件的功能,通过 Java8 的 Lambda 判断是否加入 ZIP 压缩,比较方便。函数表达式的签名是Function<File, Boolean>
参数是待加入的File
对象,返回值true
表示允许,反之不行。
import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.function.Function; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import com.ajaxjs.util.logger.LogHelper; /** * ZIP 压缩/解压缩 * * @author sp42 * */ public class ZipHelper { private static final LogHelper LOGGER = LogHelper.getLog(ZipHelper.class); /** * 解压文件 * * @param save 解压文件的路径,必须为目录 * @param zipFile 输入的解压文件路径,例如C:/temp/foo.zip或 c:\\temp\\bar.zip */ public static void unzip(String save, String zipFile) { if (!new File(save).isDirectory()) throw new IllegalArgumentException("保存的路径必须为目录路径"); long start = System.currentTimeMillis(); File folder = new File(save); if (!folder.exists()) folder.mkdirs(); try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile));) { ZipEntry ze; while ((ze = zis.getNextEntry()) != null) { File newFile = new File(save + File.separator + ze.getName()); System.out.println("file unzip : " + newFile.getAbsoluteFile()); // 大部分网络上的源码,这里没有判断子目录 if (ze.isDirectory()) { newFile.mkdirs(); } else { // new File(newFile.getParent()).mkdirs(); FileHelper.initFolder(newFile); FileOutputStream fos = new FileOutputStream(newFile); IoHelper.write(zis, fos, false); fos.close(); } // ze = zis.getNextEntry(); } zis.closeEntry(); } catch (IOException e) { LOGGER.warning(e); } LOGGER.info("解压缩完成,耗时:{0}ms,保存在{1}", System.currentTimeMillis() - start, save); } /** * 压缩文件 * * @param toZip 要压缩的目录或文件 * @param saveZip 压缩后保存的 zip 文件名 */ public static void zip(String toZip, String saveZip) { zip(toZip, saveZip, null); } /** * 压缩文件 * * @param toZip 要压缩的目录或文件 * @param saveZip 压缩后保存的 zip 文件名 * @param everyFile 输入 File,可在这 Lambda 里面判断是否加入 ZIP 压缩,返回 true 表示允许,反之不行 */ public static void zip(String toZip, String saveZip, Function<File, Boolean> everyFile) { long start = System.currentTimeMillis(); File fileToZip = new File(toZip); FileHelper.initFolder(saveZip); try (FileOutputStream fos = new FileOutputStream(saveZip); ZipOutputStream zipOut = new ZipOutputStream(fos);) { zip(fileToZip, fileToZip.getName(), zipOut, everyFile); } catch (IOException e) { LOGGER.warning(e); } LOGGER.info("压缩完成,耗时:{0}ms,保存在{1}", System.currentTimeMillis() - start, saveZip); } /** * 内部的压缩方法 * * @param toZip 要压缩的目录或文件 * @param fileName ZIP 内的文件名 * @param zipOut ZIP 流 * @param everyFile 输入 File,可在这 Lambda 里面判断是否加入 ZIP 压缩,返回 true 表示允许,反之不行 */ private static void zip(File toZip, String fileName, ZipOutputStream zipOut, Function<File, Boolean> everyFile) { if (toZip.isHidden()) return; if (everyFile != null && !everyFile.apply(toZip)) { return; // 跳过不要的 } try { if (toZip.isDirectory()) { zipOut.putNextEntry(new ZipEntry(fileName.endsWith("/") ? fileName : fileName + "/")); zipOut.closeEntry(); File[] children = toZip.listFiles(); for (File childFile : children) { zip(childFile, fileName + "/" + childFile.getName(), zipOut, everyFile); } return; } zipOut.putNextEntry(new ZipEntry(fileName)); try (FileInputStream in = new FileInputStream(toZip);) { IoHelper.write(in, zipOut, false); } } catch (IOException e) { LOGGER.warning(e); } } }
目标大致是实现了,不过性能则比较差。接着我们看看如何去优化。
优化速度
开始拜读了大神文章《Zip 压缩大文件从30秒到近乎1秒的优化过程》,深入分析了 Java 文件压缩的优化过程,从最初的无缓冲压缩到使用缓冲区,再到利用 NIO 的 Channel 和内存映射文件技术,最终实现压缩速度的显著提升。
具体代码如下。
/** * Zip压缩大文件从30秒到近乎1秒的优化过程 * 这是一个调用本地方法与原生操作系统进行交互,从磁盘中读取数据。 * 每读取一个字节的数据就调用一次本地方法与操作系统交互,是非常耗时的。例如我们现在有30000个字节的数据,如果使用 FileInputStream * 那么就需要调用30000次的本地方法来获取这些数据,而如果使用缓冲区的话(这里假设初始的缓冲区大小足够放下30000字节的数据)那么只需要调用一次就行。因为缓冲区在第一次调用 read() 方法的时候会直接从磁盘中将数据直接读取到内存中。 * 随后再一个字节一个字节的慢慢返回。 * * @param toZip * @param saveZip */ public static void zipFileBuffer(String toZip, String saveZip) { File fileToZip = new File(toZip); try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(fileToZip.toPath())); BufferedOutputStream bout = new BufferedOutputStream(zipOut)) { for (int i = 1; i < 11; i++) { try (BufferedInputStream bin = new BufferedInputStream(Files.newInputStream(Paths.get(saveZip + i + ".jpg")))) { zipOut.putNextEntry(new ZipEntry(saveZip + i + ".jpg")); int temp; while ((temp = bin.read()) != -1) { bout.write(temp); bout.flush();// BufferedInputStream 在每次write 后应该加入 flush } } } } catch (IOException e) { log.warn("zipFileBuffer", e); } }
文章有网友评论附议:
- “BufferedInputStream 在每次write 后应该加入 flush(注:实际是 BufferedOutputStream )”
- 除了
flush()
还应马上close()
流 - 如果最快就用
STORED
。注:zip 有几种压缩策略,就是调整其压缩比的,STORED 是其中一种,就是不怎么压缩,所以快
看来还可以继续地优化。于是我翻阅那位评论者的博客,果然还有介绍他怎么优化的文章,可惜目前已经收复了……不过好在我当时已经 copy 了代码:
/** * Java 极快压缩方式 <a href="https://blog.csdn.net/weixin_44044915/article/details/115734457" rel="external nofollow" >fileContent</a> */ public static void zipFile(File[] fileContent, String saveZip) { try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(Paths.get(saveZip))); BufferedOutputStream bout = new BufferedOutputStream(zipOut)) { for (File fc : fileContent) { try (BufferedInputStream bin = new BufferedInputStream(Files.newInputStream(fc.toPath()))) { ZipEntry entry = new ZipEntry(fc.getName()); // 核心,和复制粘贴效果一样,并没有压缩,但速度很快 entry.setMethod(ZipEntry.STORED); entry.setSize(fc.length()); entry.setCrc(getFileCRCCode(fc)); zipOut.putNextEntry(entry); int len; byte[] data = new byte[8192]; while ((len = bin.read(data)) != -1) bout.write(data, 0, len); bin.close(); bout.flush(); } } } catch (IOException e) { log.warn("zipFile", e); } } /** * 获取 CRC32 * CheckedInputStream一种输入流,它还维护正在读取的数据的校验和。然后可以使用校验和来验证输入数据的完整性。 * * @param file * @return */ public static long getFileCRCCode(File file) { CRC32 crc32 = new CRC32(); try (BufferedInputStream bufferedInputStream = new BufferedInputStream(Files.newInputStream(file.toPath())); CheckedInputStream checkedinputstream = new CheckedInputStream(bufferedInputStream, crc32)) { while (checkedinputstream.read() != -1) { } } catch (IOException e) { log.warn("getFileCRCCode", e); } return crc32.getValue(); }
然后该文还有网友评论可以优化(厉害了 不过本人看不是太懂……):
getFileCRCCode 里面的while用buff read速度更快,那里得到的value跟read是一样的。实测2G视频 提升40秒
接着我交给 GPT 去优化,得出下面优化过后的函数。
/** * 支持传入压缩方式的Zip方法 * * @param fileContent 需要压缩的文件数组 * @param saveZip 目标zip文件路径 * @param useStore true:仅存储(STORED,不压缩),false:标准压缩(DEFLATED) */ public static void zipFile(File[] fileContent, String saveZip, boolean useStore) { Path path = Paths.get(saveZip); // 用 BufferedOutputStream 包裹文件输出流,然后交给 ZipOutputStream try (BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(path)); ZipOutputStream zipOut = new ZipOutputStream(bos)) { for (File fc : fileContent) { try (BufferedInputStream bin = new BufferedInputStream(Files.newInputStream(fc.toPath()))) { ZipEntry entry = new ZipEntry(fc.getName()); if (useStore) { entry.setMethod(ZipEntry.STORED); entry.setSize(fc.length()); entry.setCrc(getFileCRCCode(fc)); } else { // DEFLATED 模式不需要设置size和crc,ZipOutputStream会自动处理 entry.setMethod(ZipEntry.DEFLATED); } zipOut.putNextEntry(entry); int len; byte[] data = new byte[8192]; while ((len = bin.read(data)) != -1) zipOut.write(data, 0, len); zipOut.closeEntry(); } } } catch (IOException e) { log.warn("zipFile", e); } }
主要优化点说明
- 只在底层文件流包裹一次
BufferedOutputStream
,ZipOutputStream
直接用它,无需再包一层。 - 每个 entry 的数据直接写入 zipOut,保证 putNextEntry/closeEntry 的正确配对。
bout.flush()
不再需要(zipOut 的close/flush
会自动做)。
最终版本
由于这个优化只是支持多个 File 传入,而不是传入目录的参数。因此我们让 GPT 再提供一般完整的 API。
/** * 一维文件数组压缩为 ZIP * * @param fileContent 文件数组 * @param saveZip 目标 zip 文件路径 * @param useStore true: 仅存储(STORED),false: 标准压缩(DEFLATED) */ public static void zipFile(File[] fileContent, String saveZip, boolean useStore) { try (BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(Paths.get(saveZip))); ZipOutputStream zipOut = new ZipOutputStream(bos)) { for (File fc : fileContent) addFileToZip(fc, fc.getName(), zipOut, useStore); } catch (IOException e) { e.printStackTrace(); } } /** * 递归压缩目录为ZIP * * @param sourceDir 目录路径 * @param saveZip 目标 zip 文件路径 * @param useStore true: 仅存储(STORED),false: 标准压缩(DEFLATED) */ public static void zipDirectory(String sourceDir, String saveZip, boolean useStore) { File dir = new File(sourceDir); if (!dir.exists() || !dir.isDirectory()) throw new IllegalArgumentException("Source directory does not exist or is not a directory: " + sourceDir); try (BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(Paths.get(saveZip))); ZipOutputStream zipOut = new ZipOutputStream(bos)) { String basePath = dir.getCanonicalPath(); zipDirectoryRecursive(dir, basePath, zipOut, useStore); } catch (IOException e) { e.printStackTrace(); } } /** * 目录压缩,用于递归 */ private static void zipDirectoryRecursive(File file, String basePath, ZipOutputStream zipOut, boolean useStore) throws IOException { String relativePath = basePath.equals(file.getCanonicalPath()) ? StrUtil.EMPTY_STRING : file.getCanonicalPath().substring(basePath.length() + 1).replace(File.separatorChar, '/'); if (file.isDirectory()) { File[] files = file.listFiles(); if (files != null && files.length == 0 && !relativePath.isEmpty()) { ZipEntry entry = new ZipEntry(relativePath + "/"); // 空目录也要加入Zip zipOut.putNextEntry(entry); zipOut.closeEntry(); } else if (files != null) { for (File child : files) zipDirectoryRecursive(child, basePath, zipOut, useStore); } } else addFileToZip(file, relativePath, zipOut, useStore); } /** * 单文件添加到 zip */ private static void addFileToZip(File file, String zipEntryName, ZipOutputStream zipOut, boolean useStore) throws IOException { try (BufferedInputStream bin = new BufferedInputStream(Files.newInputStream(file.toPath()))) { ZipEntry entry = new ZipEntry(zipEntryName); if (useStore) { entry.setMethod(ZipEntry.STORED); entry.setSize(file.length()); entry.setCrc(getFileCRCCode(file)); } else entry.setMethod(ZipEntry.DEFLATED);// // DEFLATED 模式不需要设置 size 和 crc,ZipOutputStream 会自动处理 zipOut.putNextEntry(entry); byte[] buffer = new byte[8192]; int len; while ((len = bin.read(buffer)) != -1) zipOut.write(buffer, 0, len); zipOut.closeEntry(); } } /** * 获取 CRC32 * CheckedInputStream 一种输入流,它还维护正在读取的数据的校验和。然后可以使用校验和来验证输入数据的完整性。 */ private static long getFileCRCCode(File file) { CRC32 crc32 = new CRC32(); try (BufferedInputStream bufferedInputStream = new BufferedInputStream(Files.newInputStream(file.toPath())); CheckedInputStream checkedinputstream = new CheckedInputStream(bufferedInputStream, crc32)) { while (checkedinputstream.read() != -1) { // 只需遍历即可统计 } } catch (IOException e) { log.warn("getFileCRCCode", e); } return crc32.getValue(); }
自此我们的 zip 压缩工具函数就完成了。
到此这篇关于浅谈Java Zip 压缩及其优化的文章就介绍到这了,更多相关Java Zip 压缩内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!