Apache Commons Compress 快速入门与集成示例
Commons Compress 1.28.0 快速入门:Maven 集成、tar 打包解包、zip 读写、gzip/bzip2/xz 压缩流、格式自动探测、256MB 堆流式处理 200MB 文件、并行写 zip。
Apache Commons Compress 快速入门与集成示例
目标:十分钟内跑通 Apache Commons Compress 1.28.0 的核心场景——Maven 集成、tar 打包解包、zip 读写、gzip/bzip2/xz 压缩流、格式自动探测、大文件流式处理与并行写 zip。全部示例已在 Ubuntu 22.04 / CentOS 8(JDK 17 + Maven)实测通过。
一、环境信息
本章示例基于以下环境,两个系统的运行结果完全一致。Commons Compress 是纯 Java 库(无常驻服务),1.28.0 字节码目标为 Java 8,示例统一用 JDK 17 编译运行。
java -version 2>&1 | head -1
mvn -v | head -1
实测回显(Ubuntu 22.04):
openjdk version "17.0.20.1" 2026-08-18
Apache Maven 3.6.3
实测回显(CentOS 8,需先 export JAVA_HOME=/usr/lib/jvm/java-17-openjdk):
openjdk version "17.0.1" 2021-10-19 LTS
Apache Maven 3.5.4 (Red Hat 3.5.4-5)
💡 技术拓展:动手前先分清两类概念,示例代码会反复用到。**归档(archiver)**把多个文件打包成一个文件(tar/zip/jar),**压缩(compressor)**只处理单条数据流(gzip/bzip2/xz);
tar.gz是「先归档再压缩」的两层结构。对比下表:
| 类别 | 格式 | 是否压缩 | 随机访问 | Commons Compress 入口 |
|---|---|---|---|---|
| 归档 | tar | 否(仅打包) | 否(TarFile 例外) | TarArchiveOutputStream / TarArchiveInputStream |
| 归档 | zip | 是(每条目 DEFLATE) | 是(ZipFile) | ZipArchiveOutputStream / ZipFile |
| 归档 | jar | 是(zip 超集) | 是 | 同 zip |
| 归档 | 7z | 是 | 部分 | SevenZOutputFile / SevenZFile |
| 压缩 | gzip | 是(DEFLATE) | 否 | GzipCompressorOutputStream / GzipCompressorInputStream |
| 压缩 | bzip2 | 是(BWT) | 否 | BZip2CompressorOutputStream / BZip2CompressorInputStream |
| 压缩 | xz | 是(LZMA2) | 否 | XZCompressorOutputStream / XZCompressorInputStream |
| 压缩 | zstd | 是(Zstandard) | 否 | 需 zstd-jni 可选依赖 |
💡 技术拓展:如果只需要基础压缩能力,也可以选其他 Java 库。下表对比常见选择,本文使用功能最全的 Commons Compress:
| 库 | 坐标 | 支持格式 | 特点 |
|---|---|---|---|
| JDK 内置 | 无(java.util.zip) | zip/gzip/deflate | 零依赖,但不支持 tar、7z、bzip2、xz 等 |
| Commons Compress | org.apache.commons:commons-compress | 14 种压缩流、8 种归档流 | 覆盖面最广,API 统一 |
| snappy-java | org.xerial.snappy:snappy-java | snappy | 极致速度,JNI 实现,无归档能力 |
| XZ for Java | org.tukaani:xz | xz/lzma | 高压缩比,Commons Compress 以它为可选依赖 |
二、Maven 集成
创建项目并声明依赖:
mkdir -p /opt/compress-demo/src/main/java/net/quzuo/demo
cd /opt/compress-demo
该命令无输出。pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>net.quzuo.demo</groupId>
<artifactId>compress-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.28.0</version>
</dependency>
<!-- 以下为可选依赖,按用到的格式引入 -->
<dependency>
<groupId>org.tukaani</groupId>
<artifactId>xz</artifactId>
<version>1.10</version>
</dependency>
</dependencies>
</project>
逐项说明:commons-compress 本体会自动带入三个必需传递依赖;org.tukaani:xz 是 xz/lzma 格式的运行时依赖,不用这两种格式可删去。验证依赖树:
mvn dependency:tree
实测回显(节选,实测首尾):
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ compress-demo ---
[INFO] net.quzuo.demo:compress-demo:jar:1.0.0
[INFO] \- org.apache.commons:commons-compress:jar:1.28.0:compile
[INFO] +- commons-codec:commons-codec:jar:1.19.0:compile
[INFO] +- commons-io:commons-io:jar:2.20.0:compile
[INFO] \- org.apache.commons:commons-lang3:jar:3.18.0:compile
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
依赖树确认了 1.28.0 的三个必需传递依赖:commons-codec 1.19.0(lz4/snappy 用)、commons-io 2.20.0、commons-lang3 3.18.0。
三、打包与解包 tar
tar 是纯归档:逐条目写入「头 + 数据」。完整可运行示例:
package net.quzuo.demo;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class TarPackUnpack {
public static void main(String[] args) throws Exception {
Path work = Paths.get("tar-demo");
Files.createDirectories(work.resolve("src"));
Files.write(work.resolve("src/notes.txt"), "Commons Compress tar demo\n第二行:中文内容测试\n".getBytes("UTF-8"));
Files.write(work.resolve("src/data.bin"), new byte[64 * 1024]);
// 1) 打包:目录 -> tar(POSIX 长文件名模式 + UTF-8 条目名编码)
File tarFile = work.resolve("bundle.tar").toFile();
try (OutputStream fos = new BufferedOutputStream(new FileOutputStream(tarFile), 65536);
TarArchiveOutputStream tos = new TarArchiveOutputStream(fos, "UTF-8")) {
tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
File dir = work.resolve("src").toFile();
for (File f : dir.listFiles()) {
TarArchiveEntry entry = new TarArchiveEntry(f, "docs/" + f.getName());
tos.putArchiveEntry(entry);
try (InputStream in = new FileInputStream(f)) {
byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) > 0) {
tos.write(buf, 0, n);
}
}
tos.closeArchiveEntry();
System.out.println("打包: docs/" + f.getName() + " (" + f.length() + " 字节)");
}
tos.finish();
}
System.out.println("tar 大小: " + tarFile.length() + " 字节");
// 2) 解包:tar -> 目录
Path out = work.resolve("out");
Files.createDirectories(out);
try (TarArchiveInputStream tis = new TarArchiveInputStream(
new BufferedInputStream(new FileInputStream(tarFile), 65536))) {
TarArchiveEntry entry;
while ((entry = tis.getNextEntry()) != null) {
Path target = out.resolve(entry.getName());
if (entry.isDirectory()) {
Files.createDirectories(target);
continue;
}
Files.createDirectories(target.getParent());
Files.copy(tis, target);
System.out.println("解出: " + entry.getName()
+ " size=" + entry.getSize()
+ " user=" + entry.getUserName()
+ " mode=" + entry.getMode());
}
}
System.out.println("还原校验: notes.txt 内容 = "
+ new String(Files.readAllBytes(out.resolve("docs/notes.txt")), "UTF-8").split("\n")[0]);
}
}
要点逐条说明:new TarArchiveEntry(File, String) 由源文件自动填充大小、时间、权限;setLongFileMode(LONGFILE_POSIX) 允许超过 100 字节的文件名(默认 ERROR 会直接抛异常);每条目以 putArchiveEntry 开始、closeArchiveEntry 结束;解包时 Files.copy(tis, target) 直接从归档流复制,getNextEntry() 在 1.28.0 返回 TarArchiveEntry(旧的 getNextTarEntry() 已弃用)。运行:
mvn -q compile exec:java -Dexec.mainClass=net.quzuo.demo.TarPackUnpack
实测回显:
打包: docs/data.bin (65536 字节)
打包: docs/notes.txt (57 字节)
tar 大小: 68096 字节
解出: docs/data.bin size=65536 user=root mode=33188
解出: docs/notes.txt size=57 user=root mode=33188
还原校验: notes.txt 内容 = Commons Compress tar demo
打包后的 bundle.tar 可直接用系统命令验证互通性:tar -tf tar-demo/bundle.tar 实测输出 docs/data.bin 与 docs/notes.txt,Commons Compress 生成的 tar 与 GNU tar 完全兼容。
四、zip 读写与压缩级别
zip 每条目独立压缩,支持随机访问。示例演示级别对比与 ZipFile 读回:
package net.quzuo.demo;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.ZipFile;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.zip.Deflater;
public class ZipDemo {
public static void main(String[] args) throws Exception {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 5000; i++) {
sb.append("Commons Compress zip demo line ").append(i).append(" - repeated pattern abcdefgh\n");
}
byte[] payload = sb.toString().getBytes(StandardCharsets.UTF_8);
Files.write(Paths.get("payload.txt"), payload);
// 用压缩级别 1 写一份
long size1 = writeZip("level1.zip", payload, Deflater.BEST_SPEED);
// 用压缩级别 9 写一份
long size9 = writeZip("level9.zip", payload, Deflater.BEST_COMPRESSION);
System.out.println("原始大小: " + payload.length + " 字节");
System.out.println("级别1(最快) zip 大小: " + size1 + " 字节");
System.out.println("级别9(最小) zip 大小: " + size9 + " 字节");
// ZipFile 随机访问读取(builder 设置条目名编码)
try (ZipFile zf = ZipFile.builder().setFile(new File("level9.zip")).setCharset("GBK").get()) {
Enumeration<ZipArchiveEntry> entries = zf.getEntries();
while (entries.hasMoreElements()) {
ZipArchiveEntry e = entries.nextElement();
System.out.println("条目: " + e.getName()
+ " size=" + e.getSize()
+ " compressed=" + e.getCompressedSize()
+ " method=" + e.getMethod()
+ " time=" + e.getTime());
}
String firstLine;
try (BufferedReader r = new BufferedReader(
new InputStreamReader(zf.getInputStream(zf.getEntry("payload.txt")), StandardCharsets.UTF_8))) {
firstLine = r.readLine();
}
System.out.println("首行内容: " + firstLine);
}
}
private static long writeZip(String target, byte[] payload, int level) throws Exception {
try (ZipArchiveOutputStream zos = new ZipArchiveOutputStream(
new BufferedOutputStream(new FileOutputStream(target), 65536))) {
zos.setEncoding("GBK"); // 条目名编码
zos.setCreateUnicodeExtraFields(
ZipArchiveOutputStream.UnicodeExtraFieldPolicy.ALWAYS); // 写入 UTF-8 附加字段
zos.setLevel(level); // DEFLATE 压缩级别 0-9
zos.setMethod(ZipArchiveOutputStream.DEFLATED);
ZipArchiveEntry entry = new ZipArchiveEntry("payload.txt");
entry.setSize(payload.length);
entry.setTime(System.currentTimeMillis());
zos.putArchiveEntry(entry);
zos.write(payload);
zos.closeArchiveEntry();
}
return new File(target).length();
}
}
要点逐条说明:setLevel 接受 0-9(Deflater.BEST_SPEED=1、Deflater.BEST_COMPRESSION=9);ZipFile 是随机访问入口,按条目名直接取流;写条目前设置 setSize 可让 zip 头记录准确大小(DEFLATED 流式写也可不设,库会写数据描述符)。运行:
mvn -q compile exec:java -Dexec.mainClass=net.quzuo.demo.ZipDemo
实测回显:
原始大小: 318890 字节
级别1(最快) zip 大小: 13522 字节
级别9(最小) zip 大小: 12879 字节
条目: payload.txt size=318890 compressed=12703 method=8 time=1791879020000
首行内容: Commons Compress zip demo line 0 - repeated pattern abcdefgh
method=8 即 DEFLATED。中文条目名的编码处理(GBK 写入 + Unicode 附加字段兜底,实测 UTF-8 读取 仍能正确还原 报表-2026年10月.txt,而关闭附加字段后乱码为 ????-2026??10??.txt)的完整示例见《Apache Commons Compress 配置参数详解》编码一章。
五、压缩流:gzip/bzip2/xz
压缩流面向单条数据流,两层套用即得 tar.gz 类格式。示例对同一份数据用不同算法与参数压缩,并演示格式探测:
package net.quzuo.demo;
import org.apache.commons.compress.compressors.CompressorStreamFactory;
import org.apache.commons.compress.compressors.deflate.DeflateCompressorOutputStream;
import org.apache.commons.compress.compressors.deflate.DeflateParameters;
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipParameters;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.zip.Deflater;
public class CompStreamDemo {
public static void main(String[] args) throws Exception {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 20000; i++) {
sb.append("compress stream benchmark line ").append(i).append(" apache commons compress 1.28.0\n");
}
byte[] raw = sb.toString().getBytes(StandardCharsets.UTF_8);
System.out.println("输入大小: " + raw.length + " 字节");
System.out.println("gzip 级别1: " + gzip(raw, Deflater.BEST_SPEED) + " 字节");
System.out.println("gzip 级别9: " + gzip(raw, Deflater.BEST_COMPRESSION) + " 字节");
System.out.println("bzip2 : " + byFactory(CompressorStreamFactory.BZIP2, raw) + " 字节");
System.out.println("xz : " + byFactory(CompressorStreamFactory.XZ, raw) + " 字节");
System.out.println("lzma : " + byFactory(CompressorStreamFactory.LZMA, raw) + " 字节");
System.out.println("deflate(9): " + deflate(raw) + " 字节");
// 自动探测:把 gzip 输出喂给工厂,不解码先探测
ByteArrayOutputStream gzOut = new ByteArrayOutputStream();
try (OutputStream os = new GzipCompressorOutputStream(gzOut)) {
os.write(raw);
}
byte[] gzBytes = gzOut.toByteArray();
try (InputStream in = new ByteArrayInputStream(gzBytes)) {
String detected = CompressorStreamFactory.detect(in);
System.out.println("探测到压缩格式: " + detected);
}
try (InputStream in = new ByteArrayInputStream(gzBytes);
InputStream cis = new CompressorStreamFactory()
.createCompressorInputStream(in);
ByteArrayOutputStream restored = new ByteArrayOutputStream()) {
byte[] buf = new byte[8192];
int n;
while ((n = cis.read(buf)) > 0) {
restored.write(buf, 0, n);
}
System.out.println("自动探测解压还原字节数: " + restored.size());
}
}
private static long gzip(byte[] data, int level) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
GzipParameters params = new GzipParameters();
params.setCompressionLevel(level);
params.setFileName("source.txt");
params.setComment("produced by commons-compress demo");
try (GzipCompressorOutputStream gz = new GzipCompressorOutputStream(bos, params)) {
gz.write(data);
}
return bos.size();
}
private static long deflate(byte[] data) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DeflateParameters dp = new DeflateParameters();
dp.setCompressionLevel(Deflater.BEST_COMPRESSION);
dp.setWithZlibHeader(true);
try (DeflateCompressorOutputStream os = new DeflateCompressorOutputStream(bos, dp)) {
os.write(data);
}
return bos.size();
}
private static long byFactory(String algo, byte[] data) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (org.apache.commons.compress.compressors.CompressorOutputStream<?> os =
new CompressorStreamFactory()
.createCompressorOutputStream(algo, bos)) {
os.write(data);
}
return bos.size();
}
}
要点逐条说明:GzipParameters 可设级别、原始文件名、注释等 gzip 头字段;bzip2 没有参数对象,工厂直接建流;createCompressorInputStream(InputStream) 单参版按流签名自动识别格式;detect(InputStream) 静态方法只探测不解压。运行:
mvn -q compile exec:java -Dexec.mainClass=net.quzuo.demo.CompStreamDemo
实测回显:
输入大小: 1348890 字节
gzip 级别1: 55677 字节
gzip 级别9: 52677 字节
bzip2 : 22000 字节
xz : 26692 字节
lzma : 26615 字节
deflate(9): 52620 字节
探测到压缩格式: gz
自动探测解压还原字节数: 1348890
注意 xz/lzma 需要 pom 里的 org.tukaani:xz 依赖;实测缺依赖时运行报 org/tukaani/xz/FilterOptions 的 NoClassDefFoundError,zstd/brotli 同理需要各自的可选依赖。
六、格式自动探测
ArchiveStreamFactory 能识别 tar/zip/jar/ar/cpio/7z 等归档签名,但不会自动解压外层压缩;正确姿势是「压缩层探测 + 归档层探测」两级链。示例对 tar、zip、tar.gz、tar.bz2 四个样本统一处理:
package net.quzuo.demo;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.ArchiveStreamFactory;
import org.apache.commons.compress.compressors.CompressorInputStream;
import org.apache.commons.compress.compressors.CompressorStreamFactory;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.InputStream;
public class AutoDetectDemo {
public static void main(String[] args) throws Exception {
String[] files = {"detect-sample/sample.tar", "detect-sample/sample.zip",
"detect-sample/sample.tar.gz", "detect-sample/sample.tar.bz2"};
for (String f : files) {
try (InputStream fin = new BufferedInputStream(new FileInputStream(f), 65536)) {
String comp;
try {
comp = CompressorStreamFactory.detect(fin);
} catch (Exception ex) {
comp = "无压缩层";
}
// 第二层:有压缩层就先解压,再探测归档
InputStream base = fin;
CompressorInputStream cis = null;
if (!"无压缩层".equals(comp)) {
cis = new CompressorStreamFactory().createCompressorInputStream(fin);
base = new BufferedInputStream(cis, 65536);
}
try (ArchiveInputStream<? extends ArchiveEntry> ais =
new ArchiveStreamFactory().createArchiveInputStream(base)) {
System.out.println(f + " 压缩层=" + comp
+ " 归档层=" + ais.getClass().getSimpleName());
ArchiveEntry e = ais.getNextEntry();
System.out.println(" 首条目: " + e.getName() + " (size=" + e.getSize() + ")");
} finally {
if (cis != null) {
cis.close();
}
}
}
}
// 反例:tar.gz 直接喂给归档工厂
System.out.println("== 反例:tar.gz 直接喂给 ArchiveStreamFactory ==");
try (InputStream fin = new BufferedInputStream(new FileInputStream("detect-sample/sample.tar.gz"));
ArchiveInputStream<? extends ArchiveEntry> ais =
new ArchiveStreamFactory().createArchiveInputStream(fin)) {
System.out.println(ais.getClass().getSimpleName());
} catch (Exception ex) {
System.out.println("抛出: " + ex.getClass().getSimpleName() + ": " + ex.getMessage());
}
}
}
准备四个样本(用系统命令生成):
cd /opt/compress-demo
mkdir -p detect-sample && echo "hello commons compress" > detect-sample/a.txt
tar -cf detect-sample/sample.tar -C detect-sample a.txt
zip -q detect-sample/sample.zip detect-sample/a.txt
tar -czf detect-sample/sample.tar.gz -C detect-sample a.txt
tar -cjf detect-sample/sample.tar.bz2 -C detect-sample a.txt
该命令无输出。运行:
mvn -q compile exec:java -Dexec.mainClass=net.quzuo.demo.AutoDetectDemo
实测回显:
detect-sample/sample.tar 压缩层=无压缩层 归档层=TarArchiveInputStream
首条目: a.txt (size=32)
detect-sample/sample.zip 压缩层=无压缩层 归档层=ZipArchiveInputStream
首条目: a.txt (size=32)
detect-sample/sample.tar.gz 压缩层=gz 归档层=TarArchiveInputStream
首条目: a.txt (size=32)
detect-sample/sample.tar.bz2 压缩层=bzip2 归档层=TarArchiveInputStream
首条目: a.txt (size=32)
== 反例:tar.gz 直接喂给 ArchiveStreamFactory ==
抛出: ArchiveException: No Archiver found for the stream signature
两级探测的关键点:CompressorStreamFactory.detect 内部 mark/reset 会把流复位到起点,因此探测完还能从头建解压流;zip 文件不需要压缩层(条目内自压缩),detect 对它会报「无压缩层」,直接走归档层。
七、流式处理大文件
Commons Compress 是流式 API,内存占用只取决于缓冲区,不取决于文件大小。200MB 文件在 256MB 堆下打包解包 tar.gz:
package net.quzuo.demo;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class BigStreamDemo {
public static void main(String[] args) throws Exception {
Path bigFile = Paths.get("bigfile.bin");
// 生成 200MB 可压缩数据(同一段 16KB 随机内容重复 12800 次,重复距离在 gzip 32KB 窗口内)
if (!Files.exists(bigFile) || Files.size(bigFile) != 200L * 1024 * 1024) {
byte[] chunk = new byte[1 << 14];
java.util.random.RandomGenerator.getDefault().nextBytes(chunk);
try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(bigFile), 1 << 16)) {
for (int i = 0; i < 200 * 64; i++) { // 200MB / 16KB = 12800 块
out.write(chunk);
}
}
}
System.out.println("输入文件: " + Files.size(bigFile) + " 字节");
Runtime rt = Runtime.getRuntime();
System.out.println("最大堆: " + rt.maxMemory() / 1024 / 1024 + " MB");
// 流式打包 tar.gz
try (OutputStream fos = new BufferedOutputStream(new FileOutputStream("bigfile.tar.gz"), 1 << 16);
GzipCompressorOutputStream gzos = new GzipCompressorOutputStream(fos);
TarArchiveOutputStream tos = new TarArchiveOutputStream(gzos, "UTF-8");
InputStream in = new BufferedInputStream(new FileInputStream(bigFile.toFile()), 1 << 16)) {
tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
TarArchiveEntry entry = new TarArchiveEntry(bigFile.toFile(), "bigfile.bin");
tos.putArchiveEntry(entry);
byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) > 0) {
tos.write(buf, 0, n);
}
tos.closeArchiveEntry();
tos.finish();
}
System.out.println("tar.gz 大小: " + Files.size(Paths.get("bigfile.tar.gz")) + " 字节");
// 流式读回并统计字节数(gzip 外层必须先解压)
long total = 0;
try (TarArchiveInputStream tis = new TarArchiveInputStream(
new GzipCompressorInputStream(
new BufferedInputStream(new FileInputStream("bigfile.tar.gz"), 1 << 16)))) {
TarArchiveEntry e;
while ((e = tis.getNextEntry()) != null) {
byte[] buf = new byte[8192];
int n;
while ((n = tis.read(buf)) > 0) {
total += n;
}
System.out.println("读回条目: " + e.getName());
}
}
System.out.println("读回总字节数: " + total);
}
}
要点逐条说明:写出方向是「tar 套在 gzip 外」,即 TarArchiveOutputStream(GzipCompressorOutputStream(FileOutputStream));读入方向相反,TarArchiveInputStream 包住 GzipCompressorInputStream;全程 8KB/64KB 缓冲,没有任何整文件读入内存的操作。用 256MB 堆运行(示例已先经 mvn compile 与 dependency:build-classpath 生成 cp.txt 类路径文件):
CP="target/classes:$(cat cp.txt)"
java -Xmx256m -Dfile.encoding=UTF-8 -cp "$CP" net.quzuo.demo.BigStreamDemo
实测回显:
输入文件: 209715200 字节
最大堆: 256 MB
tar.gz 大小: 1439630 字节
读回条目: bigfile.bin
读回总字节数: 209715200
200MB 输入、256MB 堆、全程零 OOM,读回字节数与原文件完全一致,验证了流式链路的正确性。
八、并行写 zip
条目多、单条目大的 zip 可以用 ParallelScatterZipCreator 多线程压缩:
package net.quzuo.demo;
import org.apache.commons.compress.archivers.zip.ParallelScatterZipCreator;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.parallel.InputStreamSupplier;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
public class ScatterZipDemo {
public static void main(String[] args) throws Exception {
ParallelScatterZipCreator creator = new ParallelScatterZipCreator();
try (ZipArchiveOutputStream zos = new ZipArchiveOutputStream(
Files.newOutputStream(Paths.get("scatter.zip")))) {
for (int i = 0; i < 16; i++) {
final int idx = i;
ZipArchiveEntry entry = new ZipArchiveEntry("part-" + String.format("%02d", idx) + ".txt");
byte[] data = ("scatter demo payload " + idx + "\n").repeat(500)
.getBytes(StandardCharsets.UTF_8);
entry.setSize(data.length);
entry.setMethod(ZipArchiveEntry.DEFLATED); // 并行写入必须显式指定压缩方法
InputStreamSupplier supplier = () -> new ByteArrayInputStream(data);
creator.addArchiveEntry(entry, supplier);
}
creator.writeTo(zos);
}
int count = 0;
long total = 0;
try (ZipArchiveInputStream zis = new ZipArchiveInputStream(
Files.newInputStream(Paths.get("scatter.zip")))) {
ZipArchiveEntry e;
while ((e = zis.getNextEntry()) != null) {
count++;
total += e.getSize();
zis.readAllBytes();
}
}
System.out.println("并行写入完成,读回条目数: " + count + ",总大小: " + total + " 字节");
System.out.println("文件大小: " + Files.size(Paths.get("scatter.zip")) + " 字节");
}
}
要点逐条说明:addArchiveEntry 提交条目与数据源(InputStreamSupplier 每次调用返回新流,并行线程各自消费);所有条目提交完后 writeTo 汇总写出;条目必须显式 setMethod,实测缺省会报 Method must be set on zipArchiveEntry: part-00.txt。控制并发度可构造时传 Executors.newFixedThreadPool(4)。运行:
mvn -q compile exec:java -Dexec.mainClass=net.quzuo.demo.ScatterZipDemo
实测回显:
并行写入完成,读回条目数: 16,总大小: 187000 字节
文件大小: 2745 字节
16 个条目全部读回且字节数一致,产物可用系统命令 unzip -l scatter.zip 复核。
九、参考链接
- Apache Commons Compress 官方主页:https://commons.apache.org/proper/commons-compress/
- 官方用户指南(Streaming/Zip/Tar 章节):https://commons.apache.org/proper/commons-compress/userguide.html
- 1.28.0 javadoc:https://commons.apache.org/proper/commons-compress/javadocs/api-release/index.html
- Maven 中央仓库 1.28.0:https://repo1.maven.org/maven2/org/apache/commons/commons-compress/1.28.0/
- 官方示例代码库(examples 包源码):https://github.com/apache/commons-compress
- XZ for Java(xz/lzma 可选依赖):https://tukaani.org/xz/java.html
- zstd-jni(Zstandard 可选依赖):https://github.com/luben/zstd-jni
- ParallelScatterZipCreator 设计说明(javadoc):https://commons.apache.org/proper/commons-compress/javadocs/api-release/org/apache/commons/compress/archivers/zip/ParallelScatterZipCreator.html