1. 项目概述

本文将介绍如何使用 Apache Commons Compress 库进行文件压缩、归档和解压操作。我们将探讨其支持的格式、核心功能以及使用时的注意事项。

2. Apache Commons Compress 是什么

Apache Commons Compress 是一个为常用压缩和归档格式提供统一接口的库。它不仅支持 TAR、ZIP、GZIP 等主流格式,还涵盖 BZIP2、XZ、LZMA、Snappy 等专业格式。

2.1 压缩器与归档器的区别

归档器(如 TAR)将目录结构打包成单个文件,而压缩器则通过算法减少数据体积。某些格式(如 ZIP)兼具归档和压缩功能,但在该库中被归类为归档器。

可通过以下方式查看支持的格式:

  • 归档格式:查看 ArchiveStreamFactory 类的静态字段
  • 压缩格式:查看 CompressorStreamFactory 类的静态字段

2.2 依赖配置

首先添加核心依赖:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-compress</artifactId>
    <version>1.26.1</version>
</dependency>

默认支持 TAR、ZIP、BZIP2、CPIO 和 GZIP。其他格式需额外依赖:

<!-- 支持 XZ、7z、LZMA -->
<dependency>
    <groupId>org.tukaani</groupId>
    <artifactId>xz</artifactId>
    <version>1.9</version>
</dependency>

<!-- 支持 LZ4 和 ZSTD -->
<dependency>
    <groupId>com.github.luben</groupId>
    <artifactId>zstd-jni</artifactId>
    <version>1.5.5-11</version>
</dependency>

⚠️ 缺少这些依赖会导致处理特定格式时抛出异常。

3. 流的压缩与解压

虽然库为不同格式提供了通用抽象,但各格式仍有独特功能。我们重点使用 CompressorStreamFactory 实现格式无关的代码,而非直接操作具体实现类(如 GzipCompressorInputStream)。

3.1 压缩文件

压缩时需指定格式。通过 FileNameUtils 获取文件扩展名作为格式参数:

public class CompressUtils {
    public static void compressFile(Path file, Path destination) {
        String format = FileNameUtils.getExtension(destination);

        try (OutputStream out = Files.newOutputStream(destination);
          BufferedOutputStream buffer = new BufferedOutputStream(out);
          CompressorOutputStream compressor = new CompressorStreamFactory()
            .createCompressorOutputStream(format, buffer)) {
            IOUtils.copy(Files.newInputStream(file), compressor);
        }
    }

    // ...
}

测试用例:

@Test
void givenFile_whenCompressing_thenCompressed() {
    Path destination = Paths.get("/tmp/simple.txt.gz");

    CompressUtils.compressFile(Paths.get("/tmp/simple.txt"), destination);

    assertTrue(Files.isRegularFile(destination));
}

通过修改目标文件扩展名即可切换压缩格式(如 .bz2.xz),且支持任意文件类型作为输入。

3.2 解压文件

解压时库会自动检测格式:

public static void decompress(Path file, Path destination) {
    try (InputStream in = Files.newInputStream(file);
      BufferedInputStream inputBuffer = new BufferedInputStream(in);
      OutputStream out = Files.newOutputStream(destination);
      CompressorInputStream decompressor = new CompressorStreamFactory()
        .createCompressorInputStream(inputBuffer)) {
        IOUtils.copy(decompressor, out);
    }
}

测试用例:

@Test
void givenCompressedArchive_whenDecompressing_thenArchiveAvailable() {
    Path destination = Paths.get("/tmp/decompressed-archive.tar");

    CompressUtils.decompress("/tmp/archive.tar.gz", destination);

    assertTrue(Files.isRegularFile(destination));
}

✅ 支持任意压缩格式组合(如 .tar.xz.zip.gz),且不限于归档文件。

4. 创建与操作归档

4.1 创建归档

使用 Archiver 类的便捷方法归档整个目录:

public static void archive(Path directory, Path destination) {
    String format = FileNameUtils.getExtension(destination);
    new Archiver().create(format, destination, directory);
}

4.2 归档与压缩结合

单步操作创建压缩归档:将扩展名拆分为压缩格式和归档格式:

public static void archiveAndCompress(Path directory, Path destination) {
    String compressionFormat = FileNameUtils.getExtension(destination);
    String archiveFormat = FilenameUtils.getExtension(
      destination.getFileName().toString().replace("." + compressionFormat, ""));

    try (OutputStream archive = Files.newOutputStream(destination);
      BufferedOutputStream archiveBuffer = new BufferedOutputStream(archive);
      CompressorOutputStream compressor = new CompressorStreamFactory()
        .createCompressorOutputStream(compressionFormat, archiveBuffer);
      ArchiveOutputStream<?> archiver = new ArchiveStreamFactory()
        .createArchiveOutputStream(archiveFormat, compressor)) {
        new Archiver().create(archiver, directory);
    }
}

4.3 解压归档

使用 Expander 类单行解压:

public static void extract(Path archive, Path destination) {
    new Expander().expand(archive, destination);
}

自动处理流操作、格式检测和文件提取

4.4 提取单个条目

遍历归档条目并匹配文件名:

public static void extractOne(Path archivePath, String fileName, Path destinationDirectory) {
    try (InputStream input = Files.newInputStream(archivePath); 
      BufferedInputStream buffer = new BufferedInputStream(input); 
      ArchiveInputStream<?> archive = new ArchiveStreamFactory()
        .createArchiveInputStream(buffer)) {

        ArchiveEntry entry;
        while ((entry = archive.getNextEntry()) != null) {
            if (entry.getName().equals(fileName)) {
                Path outFile = destinationDirectory.resolve(fileName);
                Files.createDirectories(outFile.getParent());
                try (OutputStream os = Files.newOutputStream(outFile)) {
                    IOUtils.copy(archive, os);
                }
                break;
            }
        }
    }
}

测试用例:

@Test
void givenExistingArchive_whenExtractingSingleEntry_thenFileExtracted() {
    Path archive = Paths.get("/tmp/archive.tar.gz");
    String targetFile = "sub-directory/some.txt";

    CompressUtils.extractOne(archive, targetFile, Paths.get("/tmp/"));

    assertTrue(Files.isRegularFile("/tmp/sub-directory/some.txt"));
}

⚠️ 文件名可包含归档内的子目录路径。

4.5 添加条目到现有归档

库不直接支持追加条目。踩坑点:直接调用 putArchiveEntry() 会覆盖内容。替代方案:

@Test
void givenExistingArchive_whenAddingSingleEntry_thenArchiveModified() {
    Path archive = Paths.get("/tmp/archive.tar");
    Path newArchive = Paths.get("/tmp/modified-archive.tar");
    Path tmpDir = Paths.get("/tmp/extracted-archive");

    Path newEntry = Paths.get("/tmp/new-entry.txt");

    // 1. 解压归档
    CompressUtils.extract(archive, tmpDir);
    assertTrue(Files.isDirectory(tmpDir));

    // 2. 添加新文件
    Files.copy(newEntry, tmpDir.resolve(newEntry.getFileName()));
    
    // 3. 重新归档
    CompressUtils.archive(tmpDir, newArchive);
    assertTrue(Files.isRegularFile(newArchive));

    // 4. 清理并替换
    FileUtils.deleteDirectory(tmpDir.toFile());
    Files.delete(archive);
    Files.move(newArchive, archive);
    assertTrue(Files.isRegularFile(archive));
}

⚠️ 此操作会破坏原归档,建议先备份。

4.6 直接使用具体实现

需要格式独有功能时(如 ZIP 压缩级别),直接操作具体类:

public static void zip(Path file, Path destination) {
    try (InputStream input = Files.newInputStream(file);
      OutputStream output = Files.newOutputStream(destination);
      ZipArchiveOutputStream archive = new ZipArchiveOutputStream(output)) {
        archive.setMethod(ZipEntry.DEFLATED);
        archive.setLevel(Deflater.BEST_COMPRESSION);

        archive.putArchiveEntry(new ZipArchiveEntry(file.getFileName().toString()));
        IOUtils.copy(input, archive);
        archive.closeArchiveEntry();
    }
}

5. 局限性

使用时需注意以下限制:

  1. 多卷归档支持有限:处理分卷归档时需谨慎
  2. 编码问题:跨文件系统或非标准化数据时可能出现编码异常
  3. ZIP 处理建议Apache 官方建议在复杂场景使用 ZipFile 获取更多控制
  4. TAR 格式注意事项:参考 TAR 专用文档

6. 总结

Apache Commons Compress 提供了强大的文件压缩与归档能力。通过理解其特性与限制,我们能以格式无关的方式高效处理文件操作。

完整示例代码见 GitHub 仓库


原始标题:Intro to the Apache Commons Compress Project