JDK 內建 java.util.zip.ZipInputStream 僅支援 UTF-8 檔名編碼。Windows 系統建立的 ZIP 檔案檔名通常採用 GBK(CP936)編碼,直接解壓會拋出 IllegalArgumentException: MALFORMED 或產生亂碼目錄。根因在於 ZipEntry 解析檔名時硬編碼使用 UTF-8。
由於 JDK 封裝了 java.util.zip 內部類(ZipConstants、ZipEntry、ZipInputStream、InflaterInputStream、DeflaterOutputStream、ZipOutputStream),且 ZipInputStream 為 final 無法繼承,唯一可行方案是完整複製原始碼到自定義包(如 cn.edu.xidian.crytoll),修改檔名解碼邏輯支援 GBK。
原始碼中 ZipEntry 建構子解析檔名:
// 原始 JDK (簡化)
this.name = new String(buf, 0, len, StandardCharsets.UTF_8);
修改為:
// 自定義 ZipEntry.java
// 嘗試 UTF-8,失敗則回退 GBK
String name;
try {
name = new String(buf, 0, len, "UTF-8");
// 簡易驗證:含不可見控制字元視為解碼錯誤
if (name.matches(".*[\\p{Cntrl}&&[^\\t\\n\\r]].*")) throw new Exception();
} catch (Exception e) {
name = new String(buf, 0, len, "GBK"); // Windows 中文預設
}
this.name = name;
確保 getNextEntry() 返回的 ZipEntry 已正確解碼檔名。
ZipConstants:常數定義(LOCSIG, CENSIG 等)。InflaterInputStream:解壓輸入流基類。DeflaterOutputStream:壓縮輸出流基類(附件包含完整原始碼)。ZipOutputStream:壓縮寫入(同理需修改寫入編碼)。package cn.edu.xidian.crytoll;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.Deflater;
public class DeflaterOutputStream extends FilterOutputStream {
protected Deflater def;
protected byte[] buf;
private boolean closed = false;
boolean usesDefaultDeflater = false;
public DeflaterOutputStream(OutputStream out, Deflater def, int size) {
super(out);
if (out == null || def == null) throw new NullPointerException();
if (size <= 0) throw new IllegalArgumentException("buffer size <= 0");
this.def = def;
buf = new byte[size];
}
public DeflaterOutputStream(OutputStream out, Deflater def) {
this(out, def, 512);
}
public DeflaterOutputStream(OutputStream out) {
this(out, new Deflater());
usesDefaultDeflater = true;
}
public void write(int b) throws IOException {
byte[] buf = new byte[1];
buf[0] = (byte)(b & 0xff);
write(buf, 0, 1);
}
public void write(byte[] b, int off, int len) throws IOException {
if (def.finished()) throw new IOException("write beyond end of stream");
if ((off | len | (off + len) | (b.length - (off + len))) < 0)
throw new IndexOutOfBoundsException();
if (len == 0) return;
if (!def.finished()) {
int stride = buf.length;
for (int i = 0; i < len; i += stride) {
def.setInput(b, off + i, Math.min(stride, len - i));
while (!def.needsInput()) deflate();
}
}
}
public void finish() throws IOException {
if (!def.finished()) {
def.finish();
while (!def.finished()) deflate();
}
}
public void close() throws IOException {
if (!closed) {
finish();
if (usesDefaultDeflater) def.end();
out.close();
closed = true;
}
}
protected void deflate() throws IOException {
int len = def.deflate(buf, 0, buf.length);
if (len > 0) out.write(buf, 0, len);
}
}
import cn.edu.xidian.crytoll.ZipInputStream; // 自定義版
import java.io.*;
public class UnzipGBK {
public static void main(String[] args) throws IOException {
File zipFile = new File("中文檔名.zip");
File destDir = new File("output");
destDir.mkdirs();
try (FileInputStream fis = new FileInputStream(zipFile);
ZipInputStream zis = new ZipInputStream(fis, Charset.forName("GBK"))) { // 假設構造子支援 Charset
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
File outFile = new File(destDir, entry.getName());
if (entry.isDirectory()) {
outFile.mkdirs();
} else {
outFile.getParentFile().mkdirs();
try (FileOutputStream fos = new FileOutputStream(outFile)) {
byte[] buf = new byte[8192];
int len;
while ((len = zis.read(buf)) > 0) fos.write(buf, 0, len);
}
}
zis.closeEntry();
}
}
}
}
| 方案 | 優勢 | 適用場景 |
|---|---|---|
| Apache Commons Compress | 成熟、支援 ZIP/7Z/TAR/GZIP/BZIP2、自動檢測編碼、支援 Zip64、加密、分卷 | 企業級專案、複雜壓縮格式 |
| Apache Ant Zip (org.apache.tools.zip) | 輕量、ZipFile.setEncoding("GBK") 一行解決中文、API 近似 JDK | 僅需 ZIP、依賴極小 |
| JDK 7+ NIO.2 FileSystem | FileSystems.newFileSystem(zipPath, env),支援 zipfs-encoding 環境變數指定編碼 | 純 JDK、現代化 Path API |
| zeroturnaround/zt-zip | Fluent API、ZipUtil.unpack(zip, dest, "GBK") 一行碼 | 快速整合、Spring Boot 專案 |
import org.apache.tools.zip.ZipFile;
import java.io.File;
ZipFile zipFile = new ZipFile("中文.zip");
zipFile.setEncoding("GBK"); // 關鍵!
Enumeration<ZipEntry> entries = zipFile.getEntries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
// entry.getName() 已正確解碼為中文
// zipFile.getInputStream(entry) 讀取內容...
}
FileSystemProvider 設置 zipfs-encoding=GBK 環境變數(非標準 API,慎用)。