| 庫 | 特點 | 推薦場景 |
| java.util.zip | JDK 原生、無依賴 | 簡單場景、無中文路徑 |
| Apache Commons Compress | 功能豐富、支援多格式 | 企業級應用、多格式需求 |
| Apache Ant Zip (org.apache.tools.zip) | 支援 GBK 編碼、解決中文亂碼 | **中文文件名場景首選** |
| Zip4j | 支援加密、分卷、流式處理 | 加密 ZIP、大文件處理 |
推薦方案:Ant Zip 處理中文路徑
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant</artifactId>
<version>1.10.14</version>
</dependency>
完整解壓工具類
import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipFile;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Enumeration;
public class ZipUtils {
/**
* 解壓 ZIP 文件到指定目錄
* @param zipPath ZIP 文件路徑
* @param destDir 目標目錄
* @param charset 編碼 (建議 GBK/UTF-8)
* @param deleteAfter 是否解壓後刪除原文件
* @throws IOException
*/
public static void unzip(String zipPath, String destDir, String charset, boolean deleteAfter)
throws IOException {
File zipFile = new File(zipPath);
if (!zipFile.exists()) {
throw new FileNotFoundException("ZIP 文件不存在: " + zipPath);
}
File destDirectory = new File(destDir);
if (!destDirectory.exists()) {
destDirectory.mkdirs();
}
try (ZipFile zip = new ZipFile(zipFile, charset)) {
Enumeration entries = zip.getEntries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
File targetFile = new File(destDirectory, entry.getName());
// 安全檢查:防止 Zip Slip 攻擊 (目錄遍歷)
if (!targetFile.getCanonicalPath().startsWith(destDirectory.getCanonicalPath())) {
throw new SecurityException("檢測到惡意路徑遍歷: " + entry.getName());
}
if (entry.isDirectory()) {
targetFile.mkdirs();
continue;
}
// 確保父目錄存在
File parent = targetFile.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
// 流式複製,避免大文件內存溢出
try (InputStream is = zip.getInputStream(entry);
FileOutputStream fos = new FileOutputStream(targetFile);
BufferedOutputStream bos = new BufferedOutputStream(fos)) {
byte[] buffer = new byte[8192];
int len;
while ((len = is.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
bos.flush();
}
}
}
if (deleteAfter) {
Files.deleteIfExists(zipFile.toPath());
}
}
/**
* 解壓單個文件到輸出流 (適合大文件、無需落地)
*/
public static void unzipToStream(String zipPath, String entryName, OutputStream out, String charset)
throws IOException {
try (ZipFile zip = new ZipFile(new File(zipPath), charset)) {
ZipEntry entry = zip.getEntry(entryName);
if (entry == null) throw new FileNotFoundException("條目不存在: " + entryName);
try (InputStream is = zip.getInputStream(entry);
BufferedInputStream bis = new BufferedInputStream(is)) {
byte[] buffer = new byte[8192];
int len;
while ((len = bis.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
}
}
}
}
關鍵問題與解決方案
1. 中文文件名亂碼
原因:JDK 原生 ZipFile 僅支援 UTF-8,Windows 建立的 ZIP 通常用 GBK。
解決:使用 org.apache.tools.zip.ZipFile 並指定 charset="GBK"。
2. Zip Slip 安全漏洞
原理:惡意 ZIP 包含 `../../etc/passwd` 等路徑,解壓時跳出目標目錄。
防禦:必須驗證解壓後路徑在目標目錄內(見代碼 canonicalPath 檢查)。
3. 大文件內存溢出 (OOM)
錯誤做法:`byte[] data = IOUtils.toByteArray(is)` 一次性讀入內存。
正確做法:分塊流式讀寫(見代碼 8KB buffer)。
4. 空目錄處理
ZIP 條目以 `/` 結尾表示目錄,需顯式 `mkdirs()`,否則空目錄會丟失。
Spring Boot 整合示例
@Service
public class FileStorageService {
@Value("${storage.temp-dir}")
private String tempDir;
public void extractUploadedZip(MultipartFile zipFile, String targetDir) throws IOException {
File tempZip = File.createTempFile("upload-", ".zip");
try {
zipFile.transferTo(tempZip);
ZipUtils.unzip(tempZip.getAbsolutePath(), targetDir, "GBK", false);
} finally {
tempZip.delete(); // 確保臨時文件清理
}
}
}
總結
Java 解壓 ZIP 看似簡單,實則暗藏編碼、安全、內存、空目錄等坑點。掌握 Ant Zip + 流式處理 + 路徑校驗 + 異常安全關閉,才是生產級的正確姿勢。
- 上一篇文章: MyBatis Insert 回寫主鍵完整指南:自增主鍵、Sequence、UUID 多種策略
- 下一篇文章: Java 配置加載機制設計:多源融合、擴展性與實戰模式
Copyright ©
程式碼之家 All Rights Reserved