Java java.util.zip 包提供標準 ZIP 格式壓縮解壓能力,無需第三方庫即可滿足絕多數業務場景。本文提供生產級工具類,涵蓋多文件壓縮、解壓到目錄、列表壓縮包內容、Adler32 校驗和完整性驗證,代碼健壯、異常處理完善,可直接集成。
package com.example.util;
import java.io.*;
import java.util.*;
import java.util.zip.*;
import java.nio.file.*;
public class ZipUtils {
/**
* 壓縮多個文件/目錄到 ZIP
* @param sourceFiles 源文件/目錄路徑列表
* @param zipFilePath 目標 ZIP 文件路徑
* @param comment 可選壓縮包註釋
* @return 校驗和值
*/
public static long zipFiles(List sourceFiles, String zipFilePath, String comment) throws IOException {
Path zipPath = Paths.get(zipFilePath);
Files.createDirectories(zipPath.getParent());
try (FileOutputStream fos = new FileOutputStream(zipFilePath);
CheckedOutputStream cos = new CheckedOutputStream(fos, new Adler32());
ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(cos))) {
if (comment != null) zos.setComment(comment);
byte[] buffer = new byte[8192];
for (String src : sourceFiles) {
Path sourcePath = Paths.get(src);
if (!Files.exists(sourcePath)) {
System.err.println("Skipped non-existent: " + src);
continue;
}
addToZip(zos, sourcePath, sourcePath.getFileName().toString(), buffer);
}
zos.finish(); // 重要:寫入中央目錄
return cos.getChecksum().getValue();
}
}
private static void addToZip(ZipOutputStream zos, Path source, String entryName, byte[] buffer) throws IOException {
if (Files.isDirectory(source)) {
// 目錄條目以 / 結尾
if (!entryName.endsWith("/")) entryName += "/";
zos.putNextEntry(new ZipEntry(entryName));
zos.closeEntry();
// 遞歸處理子項
try (DirectoryStream stream = Files.newDirectoryStream(source)) {
for (Path child : stream) {
addToZip(zos, child, entryName + child.getFileName(), buffer);
}
}
} else {
zos.putNextEntry(new ZipEntry(entryName));
try (InputStream is = Files.newInputStream(source)) {
int len;
while ((len = is.read(buffer)) > 0) {
zos.write(buffer, 0, len);
}
}
zos.closeEntry();
}
}
/**
* 解壓 ZIP 到指定目錄,保留目錄結構
* @param zipFilePath ZIP 文件路徑
* @param destDir 解壓目標目錄
* @return 解壓文件數量
*/
public static int unzipFile(String zipFilePath, String destDir) throws IOException {
Path dest = Paths.get(destDir);
Files.createDirectories(dest);
int count = 0;
try (FileInputStream fis = new FileInputStream(zipFilePath);
CheckedInputStream cis = new CheckedInputStream(fis, new Adler32());
ZipInputStream zis = new ZipInputStream(new BufferedInputStream(cis))) {
ZipEntry entry;
byte[] buffer = new byte[8192];
while ((entry = zis.getNextEntry()) != null) {
Path target = dest.resolve(entry.getName()).normalize();
// 安全檢查:防止 Zip Slip 攻擊(路徑遍歷)
if (!target.startsWith(dest)) {
throw new SecurityException("Zip Slip detected: " + entry.getName());
}
if (entry.isDirectory()) {
Files.createDirectories(target);
} else {
Files.createDirectories(target.getParent());
try (OutputStream os = Files.newOutputStream(target)) {
int len;
while ((len = zis.read(buffer)) > 0) {
os.write(buffer, 0, len);
}
}
}
zis.closeEntry();
count++;
}
System.out.println("Checksum: " + cis.getChecksum().getValue());
}
return count;
}
/**
* 列出 ZIP 內容
* @return ZipEntry 列表(含名稱、大小、壓縮大小、時間、CRC32)
*/
public static List listEntries(String zipFilePath) throws IOException {
List entries = new ArrayList<>();
try (ZipFile zf = new ZipFile(zipFilePath)) {
Enumeration extends ZipEntry> en = zf.entries();
while (en.hasMoreElements()) {
entries.add(en.nextElement());
}
}
return entries;
}
}
FileOutputStream → CheckedOutputStream(Adler32) → BufferedOutputStream → ZipOutputStream
CheckedOutputStream:流式計算校驗和,無需二次讀取BufferedOutputStream:減少底層系統調用,提升寫入吞吐ZipOutputStream:核心壓縮邏輯,putNextEntry() 開啟新條目,write() 寫入數據,closeEntry() 結束條目zos.finish() 必須調用:寫入中央目錄結構,否則壓縮包損壞不可讀/ 結尾,標準 ZIP 規範entryName + child.getFileName() 保證跨平台路徑分隔符統一為 /惡意 ZIP 可能包含 ../../etc/passwd 類路徑遍歷條目,解壓時若不校驗會寫入任意路徑。核心防禦:
Path target = dest.resolve(entry.getName()).normalize();
if (!target.startsWith(dest)) {
throw new SecurityException("Zip Slip detected: " + entry.getName());
}
normalize() 消除 . ..,startsWith(dest) 確保目標在合法目錄樹內。
CheckedOutputStream + Adler32 計算壓縮數據校驗和CheckedInputStream + Adler32 計算解壓數據校驗和byte[8192],避免大文件載入內存ZipInputStream 流式解壓,不載入整個壓縮包Zip64 模式(JDK 7+ ZipOutputStream 自動支援)
public class ZipDemo {
public static void main(String[] args) throws IOException {
List sources = Arrays.asList(
"data/report.pdf",
"data/images",
"config/app.properties"
);
// 壓縮
long checksum = ZipUtils.zipFiles(sources, "backup/project.zip", "Project backup 2024-01-15");
System.out.println("Compression checksum: " + checksum);
// 列表
for (ZipEntry e : ZipUtils.listEntries("backup/project.zip")) {
System.out.printf("%-40s %10d %10d %s%n",
e.getName(), e.getSize(), e.getCompressedSize(),
new java.util.Date(e.getTime()));
}
// 解壓
int count = ZipUtils.unzipFile("backup/project.zip", "restore/");
System.out.println("Extracted " + count + " entries");
}
}
Apache Commons Compress 或 zip4j 實現 AES-256 加密。ForkJoinPool 並行 addToZip,但需注意 ZipOutputStream 非線程安全,需串行寫入或分段壓縮後合併。Consumer 回調已處理字節數,支援 UI 進度條。MappedByteBuffer 減少用戶態/內核態拷貝。