Java 內建 `java.util.zip` 包提供了 ZIP 操作功能,但早期版本 (JDK 7 前) **預設使用 UTF-8 編碼讀取條目名稱**,導致在 Windows (GBK/CP936) 或 Linux (可能為其他編碼) 建立的包含中文檔名的 ZIP 檔解壓時出現亂碼甚至異常。本文提供基於 **JDK 7+ 標準 API** 的現代解決方案,涵蓋編碼自動檢測、大文件流式處理、安全防禦 (Zip Slip) 等生產級最佳實踐。
**推薦策略**:優先嘗試 UTF-8 (符合標準、支援多語言);若解碼失敗或檢測到亂碼,回退嘗試系統預設編碼 (GBK/CP936) 或指定編碼。
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.zip.*;
import java.util.Enumeration;
public class ModernUnzipper {
/**
* 解壓 ZIP 到目標目錄
* @param zipPath ZIP 檔路徑
* @param destDir 目標目錄 (會自動創建)
* @param charset 指定編碼,null 表示自動嘗試 UTF-8 -> 系統預設
*/
public static void unzip(String zipPath, String destDir, Charset charset) throws IOException {
Path dest = Paths.get(destDir).toAbsolutePath().normalize();
Files.createDirectories(dest);
// 嘗試編碼順序
Charset[] charsets = (charset != null)
? new Charset[]{charset}
: new Charset[]{StandardCharsets.UTF_8, Charset.forName("GBK")}; // 中文環境常用回退
ZipFile zipFile = null;
Exception lastException = null;
for (Charset cs : charsets) {
try {
zipFile = new ZipFile(zipPath, cs);
break; // 成功打開,跳出迴圈
} catch (Exception e) {
lastException = e;
if (zipFile != null) { try { zipFile.close(); } catch (IOException ignored) {} }
}
}
if (zipFile == null) {
throw new IOException("無法以任何支援編碼打開 ZIP: " + zipPath, lastException);
}
try (zipFile) {
Enumeration extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
Path targetPath = dest.resolve(entry.getName()).normalize();
// === 關鍵安全防禦:防止 Zip Slip (目錄遍歷攻擊) ===
if (!targetPath.startsWith(dest)) {
throw new SecurityException("檢測到 Zip Slip 攻擊嘗試: " + entry.getName() + " -> " + targetPath);
}
if (entry.isDirectory()) {
Files.createDirectories(targetPath);
continue;
}
// 確保父目錄存在
Files.createDirectories(targetPath.getParent());
// 流式複製,避免大文件 OOM
try (InputStream is = zipFile.getInputStream(entry);
OutputStream os = Files.newOutputStream(targetPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
byte[] buffer = new byte[8192];
int len;
while ((len = is.read(buffer)) > 0) {
os.write(buffer, 0, len);
}
}
}
}
}
// 便捷重載
public static void unzip(String zipPath, String destDir) throws IOException {
unzip(zipPath, destDir, null);
}
public static void main(String[] args) {
try {
unzip("test.zip", "output_dir");
System.out.println("解壓完成");
} catch (IOException e) {
e.printStackTrace();
}
}
}
代碼嘗試 `UTF-8` -> `GBK` 順序。若 ZIP 內含日文/韓文檔名,可能需加入 `Shift_JIS`, `EUC-KR`。生產環境建議:**要求上游壓縮方開啟「UTF-8 編碼」選項 (如 7-Zip 的 `cu=on`),從源頭消除編碼地獄。**
惡意 ZIP 可包含 `../../etc/passwd` 或 `..\..\windows\system32\malware.dll` 等路徑。targetPath.startsWith(dest) 配合 normalize() 是**唯一可靠防禦**。絕不可直接用 `new File(dest, entry.getName())` 拼接路徑。
| 特性 | ZipFile (推薦) | ZipInputStream |
|---|---|---|
| 隨機訪問 | 支援 (中央目錄) | 不支援 (順序讀取) |
| 編碼指定 | 構造函數支援 Charset (JDK 7+) | 構造函數支援 Charset (JDK 7+) |
| 大文件效能 | 優 (可直接定位) | 需順序讀取 |
| 記憶體佔用 | 較高 (保留中央目錄) | 極低 (流式) |
| 適用場景 | 一般解壓、需列表條目 | 超大 ZIP、記憶體受限、管道流 |
若需支援更多格式 (TAR, GZIP, 7z, RAR, Zstandard 等) 或更強大的編碼自動檢測 (基於 `org.apache.commons.compress.archivers.zip.ZipArchiveEntry` 的 `setName`/`getName` 機制),建議引入:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.26.0</version>
</dependency>
它能自動處理 UTF-8 標誌位、提供 `ZipEncoding` 接口實現更智能的編碼回退。
Java 解壓 ZIP 的生產級代碼必須處理三大核心問題:**編碼兼容 (指定 Charset + 回退)**、**安全防禦 (normalize + startsWith 防 Zip Slip)**、**記憶體安全 (流式讀寫 + 緩衝區)**。JDK 7+ 的 `ZipFile(Charset)` 讓編碼問題迎刃而解,無需修改 JDK 內部類。請堅持使用標準 API,拒絕複製修改 `java.util.zip` 源碼的反模式。