当前位置: 代码网 > it编程>编程语言>Java > Java解压ZIP中文乱码问题排查与解决方法

Java解压ZIP中文乱码问题排查与解决方法

2026年09月01日 Java 我要评论
一、问题背景业务场景是员工附件 zip 批量导入。用户可以:已有 zip→ 直接打开→ 往里面拖入新文件→ 保存→ 上传系统不要求用户把 zip 全部解压后重新压缩。实际遇到的问题是:导入1/毕业证书

一、问题背景

业务场景是员工附件 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中文乱码的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com