1. 概述

本教程,我们将学习如何使用Java实现文件压缩和解压功能。

用到了Java 中的 java.util.zip 包,这里包含了所有与压缩和解压缩相关的工具。

2. 压缩单个文件

首先,我们来看一个简单的操作:把名为test1.txt的文件压缩到名为compressed.zip的存档中。

public class ZipFile {

    public static void main(String[] args) throws IOException {
        String sourceFile = "test1.txt";
        FileOutputStream fos = new FileOutputStream("compressed.zip");
        ZipOutputStream zipOut = new ZipOutputStream(fos);

        File fileToZip = new File(sourceFile);
        FileInputStream fis = new FileInputStream(fileToZip);
        ZipEntry zipEntry = new ZipEntry(fileToZip.getName());
        zipOut.putNextEntry(zipEntry);

        byte[] bytes = new byte[1024];
        int length;
        while((length = fis.read(bytes)) >= 0) {
            zipOut.write(bytes, 0, length);
        }

        zipOut.close();
        fis.close();
        fos.close();
    }
}

3. 压缩多个文件

接下来,我们将看到如何将多个文件压缩到一个zip文件。我们将把 test1.txttest2.txt 压缩到 multiCompressed.zip

public class ZipMultipleFiles {

    public static void main(String[] args) throws IOException {
        String file1 = "src/main/resources/zipTest/test1.txt";
        String file2 = "src/main/resources/zipTest/test2.txt";
        final List<String> srcFiles = Arrays.asList(file1, file2);

        final FileOutputStream fos = new FileOutputStream(Paths.get(file1).getParent().toAbsolutePath() + "/compressed.zip");
        ZipOutputStream zipOut = new ZipOutputStream(fos);

        for (String srcFile : srcFiles) {
            File fileToZip = new File(srcFile);
            FileInputStream fis = new FileInputStream(fileToZip);
            ZipEntry zipEntry = new ZipEntry(fileToZip.getName());
            zipOut.putNextEntry(zipEntry);

            byte[] bytes = new byte[1024];
            int length;
            while((length = fis.read(bytes)) >= 0) {
                zipOut.write(bytes, 0, length);
            }
            fis.close();
        }

        zipOut.close();
        fos.close();
    }
}

4. 压缩文件夹

现在,让学习如何将整个目录压缩到zip中。我们将把 zipTest 文件夹压缩到 dirCompressed.zip 文件中:

public class ZipDirectory {
    public static void main(String[] args) throws IOException {
        String sourceFile = "zipTest";
        FileOutputStream fos = new FileOutputStream("dirCompressed.zip");
        ZipOutputStream zipOut = new ZipOutputStream(fos);

        File fileToZip = new File(sourceFile);
        zipFile(fileToZip, fileToZip.getName(), zipOut);
        zipOut.close();
        fos.close();
    }

    private static void zipFile(File fileToZip, String fileName, ZipOutputStream zipOut) throws IOException {
        if (fileToZip.isHidden()) {
            return;
        }
        if (fileToZip.isDirectory()) {
            if (fileName.endsWith("/")) {
                zipOut.putNextEntry(new ZipEntry(fileName));
                zipOut.closeEntry();
            } else {
                zipOut.putNextEntry(new ZipEntry(fileName + "/"));
                zipOut.closeEntry();
            }
            File[] children = fileToZip.listFiles();
            for (File childFile : children) {
                zipFile(childFile, fileName + "/" + childFile.getName(), zipOut);
            }
            return;
        }
        FileInputStream fis = new FileInputStream(fileToZip);
        ZipEntry zipEntry = new ZipEntry(fileName);
        zipOut.putNextEntry(zipEntry);
        byte[] bytes = new byte[1024];
        int length;
        while ((length = fis.read(bytes)) >= 0) {
            zipOut.write(bytes, 0, length);
        }
        fis.close();
    }
}

注意:

  • 对于子目录,我们递归地遍历它们。
  • 每次找到一个目录时,我们都会将其名称附加到子ZipEntry名称后,以保存层次结构。
  • 我们也为每个空目录创建一个目录。

5. 追加新文件到Zip

接下来,我们将向现有的Zip压缩包中添加新文件。例如,我们将把 file3.txt 添加到 compressed.zip 中:

String file3 = "src/main/resources/zipTest/file3.txt";
Map<String, String> env = new HashMap<>();
env.put("create", "true");

Path path = Paths.get(Paths.get(file3).getParent() + "/compressed.zip");
URI uri = URI.create("jar:" + path.toUri());

try (FileSystem fs = FileSystems.newFileSystem(uri, env)) {
    Path nf = fs.getPath("newFile3.txt");
    Files.write(nf, Files.readAllBytes(Paths.get(file3)), StandardOpenOption.CREATE);
}

简而言之,我们使用 FileSystems 类提供的 .newFileSystem() 方法挂载Zip文件,自JDK 1.7以来就可用。然后,我们在压缩文件夹中创建了一个新的 newFile3.txt,并将 file3.txt 的所有内容添加进来。

6. 解压文件

现在,我们将实现解压并提取其内容。

在这个例子中,我们将把 compressed.zip 解压到名为 unzipTest 的新文件夹中:

public class UnzipFile {

    public static void main(String[] args) throws IOException {
        String fileZip = "src/main/resources/unzipTest/compressed.zip";
        File destDir = new File("src/main/resources/unzipTest");

        byte[] buffer = new byte[1024];
        ZipInputStream zis = new ZipInputStream(new FileInputStream(fileZip));
        ZipEntry zipEntry = zis.getNextEntry();
        while (zipEntry != null) {
           // ...
        }

        zis.closeEntry();
        zis.close();
    }
}

while 循环中,我们逐个遍历每个ZipEntry,首先检查它是否是目录。如果是目录,我们就使用mkdirs()方法创建目录;否则,我们将继续创建文件:

while (zipEntry != null) {
    File newFile = newFile(destDir, zipEntry);
    if (zipEntry.isDirectory()) {
        if (!newFile.isDirectory() && !newFile.mkdirs()) {
            throw new IOException("Failed to create directory " + newFile);
        }
    } else {
        // fix for Windows-created archives
        File parent = newFile.getParentFile();
        if (!parent.isDirectory() && !parent.mkdirs()) {
            throw new IOException("Failed to create directory " + parent);
        }

        // write file content
        FileOutputStream fos = new FileOutputStream(newFile);
        int len;
        while ((len = zis.read(buffer)) > 0) {
            fos.write(buffer, 0, len);
        }
        fos.close();
    }
    zipEntry = zis.getNextEntry();
}

这里需要注意的是,在 else 分支上,我们还检查文件的父目录是否存在。对于在Windows上创建的压缩包,这是必要的,因为根目录在zip文件中没有相应的条目。

另一个关键点在newFile()方法中:

public static File newFile(File destinationDir, ZipEntry zipEntry) throws IOException {
    File destFile = new File(destinationDir, zipEntry.getName());

    String destDirPath = destinationDir.getCanonicalPath();
    String destFilePath = destFile.getCanonicalPath();

    if (!destFilePath.startsWith(destDirPath + File.separator)) {
        throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
    }

    return destFile;
}

newFile() 方法防止将文件写入目标文件夹之外的文件系统。这种漏洞被称为Zip Slip漏洞

7. 总结

在这篇文章中,我们展示了如何使用Java进行文件的压缩和解压缩。

这些示例的实现可以在GitHub上找到。


» 下一篇: Spring Data Redis简介