一、问题背景
业务场景是员工附件 zip 批量导入。
用户可以:
已有 zip → 直接打开 → 往里面拖入新文件 → 保存 → 上传系统
不要求用户把 zip 全部解压后重新压缩。
实际遇到的问题是:
导入1/毕业证书.xls 导入11/个人证件照2.png
这些原有文件正常。
但后来拖进去的文件变成:
????11/screenshot_2026-08-13_094919_415.png ????1/screenshot_2026-08-13_094919_415.png
windows 解压时还可能报:
文件名、目录名或卷标语法不正确。
二、问题根因
zip 中保存的文件名本质上是:
filename bytes
java 解压库需要通过 charset 将它转换成:
string
例如:
gbk bytes → gbk / gb18030 → 导入1
如果错误地使用 utf-8:
gbk bytes → utf-8 → ????1
问题就在这里。
三、为什么同一个 zip 会有两种编码?
这和业务允许:
直接往已有 zip 中拖文件
有关。
例如原 zip 中的文件:
entry 1 → utf-8 + efs entry 2 → utf-8 + efs entry 3 → utf-8 + efs
后来用户通过另外一个压缩软件向 zip 中添加文件。
压缩软件可能不会重新生成原来的 entry,只增加新的 entry:
旧文件: utf-8 + efs 新增文件: gbk / 本地代码页 + 无 efs
最终就形成:
同一个 zip ├── utf-8 entry ├── utf-8 entry ├── utf-8 entry ├── ... ├── gbk entry └── gbk entry
所以不能简单认为:
一个 zip = 一种文件名编码
四、efs / utf-8 flag
zip entry 的 general purpose bit flag 第 11 位表示 utf-8 文件名。
commons compress 可以通过:
entry.getgeneralpurposebit().usesutf8fornames();
判断。
可以简单理解:
efs = true → utf-8 efs = false → zip 没明确声明 utf-8
注意:
utf-8 flag 是 bit 11
不是 bit 3。
五、测试结果
分别尝试:
utf-8 gbk gb18030 big5
结果:
utf-8
前 10 个正常:
导入1/毕业证书.xls 导入11/个人证件照2.png
最后两个乱码:
????11/screenshot... ????1/screenshot...
gbk
最后两个恢复正常:
导入11/screenshot... 导入1/screenshot...
gb18030
同样正常。
因此可以确定:
原有 entry: utf-8 新增 entry: gbk / gb18030
六、为什么 zip4j 不好处理这个场景?
问题不是 zip4j 不能处理中文。
而是当前 zip 属于:
utf-8 entry + gbk entry
如果默认读取:
efs=true → utf-8
前面的文件正常。
但没有 efs 的 gbk entry 可能乱码。
如果直接:
zipfile.setcharset(charset.forname("gb18030"));
又可能影响原本 utf-8 entry 的处理。
当前业务真正需要的是:
每个 entry 单独判断 efs=true → utf-8 efs=false → gb18030
七、commons compress 正好适合
最终使用:
zipfile.builder()
.setfile(tempfile)
.setcharset(charset.forname("gb18030"))
.setuseunicodeextrafields(true)
.get();这里:
setcharset(gb18030)
并不是简单理解成:
整个 zip 强制 gb18030
而是:
efs=true → utf-8 efs=false → gb18030 fallback
正好符合当前 zip:
前 10 个 efs=true → utf-8 最后 2 个 efs=false → gb18030
因此一次遍历即可全部正常解压。
八、完整测试源码
package com.es.oa.employee.service.impl;
import org.apache.commons.compress.archivers.zip.ziparchiveentry;
import org.apache.commons.compress.archivers.zip.zipfile;
import java.io.*;
import java.nio.charset.charset;
import java.nio.file.*;
import java.util.*;
/**
* zip 文件名编码诊断 + 生产级解压工具
*
* <p>apache commons compress zipfile 内置编码协商逻辑:
* <ul>
* <li>efs flag (general purpose bit 第 11 位) = true → 自动 utf-8 解码</li>
* <li>efs flag = false → 回退使用 builder.setcharset() 指定的编码</li>
* </ul>
*
* <p>本工具通过 setcharset(gb18030) 覆盖非 utf-8 条目,实现单一 zipfile 实例解码所有条目,
* 无需多次解压或逐条目猜测编码。
*/
public class zip4jcharsettest {
/** 非 utf-8 条目的回退编码,中文 windows zip 优先选用 gb18030 */
private static final charset fallback_charset = charset.forname("gb18030");
public static void main(string[] args) {
file zipfile = new file("d:\\user\\local\\esfiles\\local1\\employee\\doc\\导入1111.zip");
path outdir = paths.get(system.getproperty("java.io.tmpdir"))
.resolve("zip_production_" + uuid.randomuuid().tostring().substring(0, 8));
system.out.println("zip: " + zipfile);
system.out.println("是否存在: " + zipfile.exists());
system.out.println("输出目录: " + outdir);
system.out.println();
if (!zipfile.exists()) {
system.out.println("文件不存在,请确认路径");
return;
}
try {
diagnose(zipfile);
extractzip(zipfile, outdir);
} catch (exception e) {
e.printstacktrace();
} finally {
system.out.println("\n解压目录已就绪: " + outdir);
}
}
// ======================== 诊断 ========================
/**
* 打印每个条目的解码结果及原始元信息,用于定位编码问题。
*/
private static void diagnose(file zipfile) throws ioexception {
system.out.println("================ 诊断:条目元信息 ================");
system.out.println();
try (zipfile zf = zipfile.builder()
.setfile(zipfile)
.setcharset(fallback_charset)
.setuseunicodeextrafields(true)
.get()) {
enumeration<ziparchiveentry> entries = zf.getentries();
while (entries.hasmoreelements()) {
ziparchiveentry entry = entries.nextelement();
if (entry.isdirectory()) continue;
string name = entry.getname();
boolean efsflag = entry.getgeneralpurposebit().usesutf8fornames();
int rawflag = entry.getrawflag();
string namesource = entry.getnamesource() != null ? entry.getnamesource().name() : "null";
byte[] rawname = entry.getrawname();
string rawhex = rawname != null ? hexdump(rawname) : "n/a";
system.out.println("--------------------------------------------------");
system.out.println("getname() : " + name);
system.out.println("usesutf8fornames(efs) : " + efsflag
+ " (efs = bit11 of gp bit, 控制文件名 utf-8 解码,非 bit3)");
system.out.println("rawflag : " + rawflag
+ " (0x" + integer.tohexstring(rawflag) + ")");
system.out.println("namesource : " + namesource);
system.out.println("rawname hex : " + rawhex);
system.out.println("size : " + entry.getsize());
}
}
}
private static string hexdump(byte[] bytes) {
stringbuilder sb = new stringbuilder();
for (byte b : bytes) {
sb.append(string.format("%02x ", b));
}
return sb.tostring().trim();
}
// ======================== 生产级解压 ========================
/**
* 使用 commons compress zipfile 一次性解压 zip。
* efs flag=true → utf-8 解码,efs flag=false → gb18030 回退解码。
*
* <p>防 zip slip:所有写出路径必须落在 targetdir 之下。
*
* @param zipfile 待解压 zip 文件
* @param targetdir 解压目标目录
* @throws ioexception io 异常
*/
private static void extractzip(file zipfile, path targetdir) throws ioexception {
system.out.println("\n================ 生产级解压(单一 zipfile,一次遍历) ================");
system.out.println();
files.createdirectories(targetdir);
int successcount = 0;
int failcount = 0;
try (zipfile zf = zipfile.builder()
.setfile(zipfile)
.setcharset(fallback_charset)
.setuseunicodeextrafields(true)
.get()) {
enumeration<ziparchiveentry> entries = zf.getentries();
while (entries.hasmoreelements()) {
ziparchiveentry entry = entries.nextelement();
// ---- 路径处理 ----
// 统一处理 zip 内可能存在的 \ 和 / 混合分隔符
string entryname = entry.getname().replace('\\', '/');
while (entryname.startswith("/")) {
entryname = entryname.substring(1);
}
path entrypath = targetdir.resolve(entryname).normalize();
path root = targetdir.toabsolutepath().normalize();
// ---- zip slip 防护 ----
if (!entrypath.startswith(root)) {
system.out.println(" [拒绝写出(zip slip 攻击)] " + entryname);
failcount++;
continue;
}
if (entry.isdirectory()) {
files.createdirectories(entrypath);
system.out.println(" [目录] " + entry.getname());
continue;
}
// ---- 普通文件 ----
files.createdirectories(entrypath.getparent());
try (inputstream is = zf.getinputstream(entry);
outputstream os = files.newoutputstream(entrypath,
standardopenoption.create, standardopenoption.truncate_existing)) {
is.transferto(os);
}
boolean efs = entry.getgeneralpurposebit().usesutf8fornames();
system.out.println(" [文件] " + entry.getname()
+ " (" + (efs ? "utf-8" : fallback_charset.name()) + ")");
successcount++;
}
}
system.out.println();
system.out.println("解压结果: 成功=" + successcount + " 失败=" + failcount);
// 列出解压产物
system.out.println("\n输出目录内容:");
try (var stream = files.walk(targetdir)) {
stream.filter(p -> !p.equals(targetdir))
.sorted()
.foreach(p -> {
string rel = targetdir.relativize(p).tostring().replace('\\', '/');
string type = files.isdirectory(p) ? "[目录]" : "[文件]";
system.out.println(" " + type + " " + rel);
});
}
}
}
九、实际业务代码
最终业务里不需要再循环尝试 utf-8、gbk、gb18030。
直接:
try (zipfile zf = zipfile.builder()
.setfile(tempfile)
.setcharset(charset.forname("gb18030"))
.setuseunicodeextrafields(true)
.get()) {
java.util.enumeration<ziparchiveentry> en = zf.getentries();
while (en.hasmoreelements()) {
ziparchiveentry entry = en.nextelement();
if (entry.isdirectory()) continue;
string filename = entry.getname();
file extractedfile = new file(tempdirname, filename);
file parent = extractedfile.getparentfile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
try (java.io.inputstream is = zf.getinputstream(entry);
java.io.outputstream os = new java.io.fileoutputstream(extractedfile)) {
is.transferto(os);
}
if (extractedfile.exists()) {
processextractedannexfile(extractedfile, filename, result, errorresults, index, duplicatecheckmap, checkedempset);
index++;
} else {
log.error("文件解压失败:" + filename);
}
}
}十、为什么选择 gb18030?
测试中:
gbk → 正常 gb18030 → 正常
最终选择:
charset.forname("gb18030")
主要因为:
gb18030 字符覆盖范围更大 并且可以兼容绝大多数 gbk 中文内容
因此作为中国大陆 windows zip 的 fallback 更合适。
十一、最终结论
这个问题表面上是:
zip 中文文件名乱码
本质上却是:
同一个 zip 中存在不同编码来源的 entry
尤其是这种业务:
已有 zip → 用户直接往里面拖入新文件 → 不重新整体压缩
很容易形成:
旧 entry → utf-8 + efs 新 entry → gbk / gb18030 + 无 efs
因此不能简单给整个 zip 强制一种 charset。
最终方案:
zipfile.builder()
.setfile(tempfile)
.setcharset(charset.forname("gb18030"))
.setuseunicodeextrafields(true)
.get();处理逻辑:
efs=true → utf-8 efs=false → gb18030
最终实现:
一个 zip 一次打开 一次遍历 每个文件只解压一次 utf-8 中文正常 gbk 中文正常
以上就是java解压zip中文乱码问题排查与解决方法的详细内容,更多关于java解压zip中文乱码的资料请关注代码网其它相关文章!
发表评论