
1. JAVA解压rar文件到底难在哪JAVA解压rar文件这件事乍看是个调个库、几行代码的小需求真动起手来十个人里有八个要在选型上先卡两天。我第一次接触这个需求是接手一个对接外部数据的模块——对方固定每周丢一个 rar 包过来里面是若干 CSV 清单文件。我当时的想法很朴素java.util.zip不是现成的吗结果打开文档才发现JDK 从来没打算支持 rarApache Commons Compress 那套 family 里也没有 rar 的位置。最后的方案是绕了三圈才定下来中间试过纯 Java 库、试过调外部程序、也试过本地库绑定每一种都有自己的脾气。核心矛盾在哪儿rar 不是公开免费格式。压缩部分的算法和实现属于私有授权公开出来的只有解压侧unrar的源码而且授权条款里明确限制了使用场景不允许拿它去反向实现压缩。这就导致了两个后果一是 JVM 生态里没有任何官方认证的完整实现所有能用的方案都带着各自的能力边界二是这些方案在 RAR5、加密头、分卷、solid 压缩这几项上表现差异极大选错了不是跑不动而是跑到一半抛个异常很难定位。所以这篇不讲哪个库最好而是把三条可行路线掰开揉碎讲清楚纯 Java 的 junrar、调外部命令行的 unrar 和 7z、以及基于本地库绑定的 7-Zip-JBinding。每条路线我都会给出能直接抄的代码、关键参数的解释、以及我自己踩过的坑。这篇文章适合两类人看一类是刚接手文件解析需求、想快速判断哪条路能走通的开发同学另一类是线上已经在跑、但偶尔爆出乱码或者解压失败、想搞清楚根因的维护者。哪怕你只做过 zip 解压跟着看下去也能明白 rar 这边的差异在哪、要额外防哪些问题。2. 三条技术路线的能力地图与选型逻辑2.1 rar 格式的授权现状决定了生态格局要理解为什么 Java 里解 rar 这么别扭得先看清楚格式本身的授权结构。rar 的压缩实现是闭源的官方只开放了解压部分的源码且条款里禁止用它来逆向构造压缩器。这就意味着开源社区能做的只有解压器并且在官方放出 RAR5 规范之前RAR5 的支持长期是空白或者残缺的。RAR4 的格式文档相对公开社区可以实现得比较完整RAR5 的结构更复杂压缩方法从 store 到各种变体、还有加密头header encryption这一类特性第三方实现很难全部覆盖。落到实际选择上这个授权结构直接决定了三件事。第一纯 Java 方案的完整性天花板很低尤其是在 RAR5 上很多库只能处理存储不压缩或者最简单的几种情况。第二命令行方案之所以稳是因为它调的是官方发布的 unrar 二进制能力上限就是官方能力上限没有实现不全这一说。第三本地库绑定方案JNI 那一类本质上是把 7-Zip 的 C 解压引擎包装成 Java 接口7-Zip 对 rar 的读取支持相当成熟因此这条路的能力接近命令行但代价是要处理本地库的加载和平台匹配。理解了这层你就不会再去纠结为什么没有一个十全十美的 Java 库——问题不在库的作者偷懒而在于格式本身把这条路堵住了。我的建议是先明确你的归档包是哪一代 rar、有没有密码、有没有分卷、是不是 solid 压缩这四个问题问清楚选型立刻收窄。2.2 三种方式的能力边界对照在写具体代码之前先拉一张对照表方便你快速定位自己属于哪种场景。这张表是我自己在几个项目里反复验证后总结的标注有限的地方都需要你拿真实样本做一次回归测试不能只看文档。能力维度junrar纯 Java命令行 unrar / 7z7-Zip-JBindingJNIRAR4 普通压缩支持稳定支持支持RAR5 压缩包部分支持压缩方法和加密头常抛异常完整支持支持较好solid固实压缩兼容性一般大包易出问题完整支持支持加密包密码支持基础密码加密文件名不支持完整支持支持分卷压缩需要自定义 VolumeManager自动识别同目录其余卷需要实现卷回调外部依赖无需要目标机器有可执行文件需要匹配平台的本地库部署体积一个几百 KB 的 jar可执行文件约 1MB 上下全平台包体较大性能中等纯 Java 逐块读高原生实现高可移植性最好差受系统环境影响中等需要平台包这张表最关键的一行是RAR5 压缩包。很多人第一次用 junrar 失败就是把 RAR5 包丢进去然后看到一堆看不懂的异常。命令行方案不存在这个问题因为官方工具本身就支持。7-Zip-JBinding 介于两者之间7-Zip 对 RAR5 的读取已经很成熟但你在引入依赖时必须把平台包一起带上。2.3 选型时我实际用的判断顺序真实项目里我不会一上来就看技术参数而是按下面这个顺序做排除效率最高。先问归档包是谁产出的、用什么工具压的。如果对方用的是比较新的压缩工具默认配置大概率是 RAR5这时候纯 Java 方案直接出局别再浪费时间。如果对方是老系统、明确告诉你是 rar4junrar 就有戏因为它零外部依赖打包部署最省心尤其适合那种交付给客户私有化部署的系统——你没法要求客户在自己机器上装什么。再看运行环境是不是我方能控。如果是自己的容器镜像装个 unrar 或者 7z 就是 Dockerfile 里一行的事命令行方案立刻变成首选稳、快、能力全。如果是客户环境、或者是那种必须纯 jar 交付的场景命令行方案的风险就太大了目标机器上有没有这个可执行文件、版本对不对、路径在不在 PATH 里全是不可控因素。最后看并发量和性能要求。命令行方案每次调用都要 fork 一个进程虽然有操作系统缓存兜底但高频调用下进程创建的开销是实打实的。我做过一个粗略的压测单机并发 20 个解压任务纯 Java 方案的平均耗时反而比进程方案低——不是算法快是省掉了进程启动和 JVM 之外的内存拷贝。当然这个数字跟包大小强相关包越大原生实现的优势越明显。所以小包高频选纯 Java大包低频选命令行或 JNI这个经验值在我手上基本没翻过车。2.4 无论选哪条路都必须先做的三件防护这条我要单独拎出来说因为很多人只关注能不能解开忽略了解开之后会不会出事。三个防护缺一不可。第一是路径穿越。归档包里的文件名是攻击者可以完全控制的一个叫../../etc/xxx或者..\\..\\windows\\xxx的条目如果你直接new File(destDir, entryName)文件就写到目录外面去了。这类问题在 zip 上有专门的名字rar 一样存在。防护方式统一解压前把目标路径规范化校验它必须落在目标目录之下。第二是解压炸弹。一个几百 KB 的压缩包解出几十 GB 是常见手法你的磁盘会被瞬间打满。防护方式是两条限制单文件解压后的字节上限限制整个归档解压后的总字节上限。判断依据取FileHeader里记录的解压后大小不要等写完再检查。第三是符号链接。某些归档里会塞入指向系统关键路径的符号链接解开之后你再往里写文件实际写到了链接指向的位置。稳妥做法是把符号链接当成普通文件跳过或者干脆整体拒绝。提示这三条防护跟用哪个库无关属于应用层的责任。库只负责把字节吐给你写到哪里、写多少是你自己的代码在决定。3. 方式一junrar 纯 Java 解析零外部依赖3.1 junrar 的定位与它擅长的场景junrar 是社区里维护得比较久的一个纯 Java 实现把官方的 unrar 解压逻辑移植到了 JVM 上不需要任何本地库或者外部可执行文件。它的最大价值就两个字干净。一个 jar 引进去Windows、Linux、macOS、各种精简容器里行为完全一致不用担心目标机器缺什么。这一点在私有化交付、离线环境、还有那种客户只允许上传 jar 包的场景下几乎是决定性的。但它的边界也很清楚。RAR4 是它的主战场处理普通压缩、带密码的包基本没什么问题。到了 RAR5 就分化得厉害一些用存储方式不压缩打的包能读一旦用了较新的压缩方法或者开了加密头就会在解析头部的时候直接抛异常。我遇到最多的是读文件头阶段失败异常信息很抽象光看堆栈根本判断不出是格式不支持还是包坏了。所以我的做法是用 junrar 之前先确认包里到底是 RAR4 还是 RAR5别等跑到线上才发现。判断版本的方法很简单用十六进制查看器打开文件头RAR4 的魔数是52 61 72 21 1A 07 00RAR5 是52 61 72 21 1A 07 01 00。差在最后那个字节很好区分。3.2 依赖引入与最小可用代码Maven 坐标如下版本号建议用 7.x 系列5.x 的老版本在编码处理和 API 设计上都比较落后。dependency groupIdcom.github.junrar/groupId artifactIdjunrar/artifactId version7.5.5/version /dependency最小可用的解压代码如下。注意这里我没有用一行extractAll而是自己遍历条目逐个写文件原因后面会讲——extractAll不做路径校验是个隐患。import com.github.junrar.Archive; import com.github.junrar.rarfile.FileHeader; import java.io.*; public class JunrarExtractor { public static void extract(File rarFile, File destDir, String password) throws Exception { destDir.mkdirs(); String base destDir.getCanonicalPath() File.separator; try (Archive archive new Archive(rarFile, password)) { FileHeader fh; while ((fh archive.nextFileHeader()) ! null) { String name fh.getFileName(); if (name null || name.trim().isEmpty()) { continue; } // 目录条目直接创建后跳过 if (fh.isDirectory()) { new File(base name).mkdirs(); continue; } File target safeResolve(base, name); target.getParentFile().mkdirs(); try (OutputStream os new BufferedOutputStream( new FileOutputStream(target), 1 16)) { archive.extractFile(fh, os); } } } } private static File safeResolve(String base, String entryName) throws IOException { File f new File(base entryName); String canonical f.getCanonicalPath(); if (!canonical.startsWith(base)) { throw new IOException(非法条目路径疑似路径穿越: entryName); } return f; } }1 16就是 64KB 的缓冲区这个值不是随便定的。太小比如 1KB会让 syscall 次数暴增解压大文件时 CPU 有明显抖动太大比如 1MB对小文件是浪费而且每一路并发都占着这么多堆外内存。64KB 是我在几台机器上试下来综合最优的跟磁盘块大小和操作系统页大小也比较合拍。3.3 密码、编码、RAR5 这三个高频坑密码问题。junrar 的Archive构造函数接受密码字符串。要注意的是文件内容加密和文件名加密是两回事。前者只要有密码就能解开后者在 RAR5 里叫加密头junrar 基本处理不了会直接报错。所以如果你手上这个包是打开就要求密码、连文件列表都看不到的类型junrar 这条路可以直接放弃了。编码问题。这个坑特别隐蔽。RAR4 的文件名在头部里没有强制指定编码实际取决于打包时那台机器的系统码页国内环境下很多包里的名字是 GBK 字节。junrar 默认按 UTF-8 解结果就是解出来的文件名全是乱码。表现是文件确实写出来了只是名字长得像一串问号。处理办法是做一个疑似乱码修复把已经解出来的字符串按 ISO-8859-1 还原成原始字节再按 GBK 重新解一次看哪个结果更像正常文件名。private static String fixEncoding(String name) { if (name null) { return null; } try { byte[] raw name.getBytes(ISO-8859-1); String gbk new String(raw, GBK); // 简单启发式含中文且无替换符认为修复成功 if (gbk.indexOf(\uFFFD) 0 !gbk.equals(name)) { return gbk; } } catch (UnsupportedEncodingException ignored) { // 走原值 } return name; }这段代码不优雅但它管用。我在三个不同的上游数据源上验证过两处是 GBK、一处是 UTF-8用这个启发式都能正确处理。当然更稳的做法是让上游在打包时显式指定 UTF-8只不过这件事往往不在你能控制的范围内。RAR5 问题。前面说过表现是解析头部就抛异常。真正的解法只有换方案——要么改用命令行要么改用 JNI 绑定。别指望加个参数就能绕过压缩方法不支持就是不支持。3.4 分卷压缩包的处理思路分卷在 RAR 里通常命名成xxx.part1.rar、xxx.part2.rar或者老式的xxx.rar、xxx.r00、xxx.r01。junrar 7.x 允许你传入一个VolumeManager实现在需要下一卷时由你去找到对应文件并返回。接口签名在不同小版本间有过调整具体以你引入的版本为准逻辑都是同一个拿到当前卷的文件名按命名规则推出下一卷判断存在性后返回。这里的关键经验是命名规则不要写死。我见过上游时而用.part1.rar、时而用.r00如果你的推导逻辑只认一种换一个数据源就崩。我的做法是先把同一目录下所有同名系列文件扫出来按数字后缀排序形成一个队列然后按顺序取下一卷。这个逻辑实现起来多写二十行但稳得多。注意分卷解压时最好把同目录下所有相关卷先做一个完整性检查文件数是否齐全、每卷开头魔数是否正确再开始解。中途缺卷报出来的异常通常很难看懂。4. 方式二ProcessBuilder 调外部命令行工具4.1 生产系统为什么偏爱这条路命令行方案在正统技术审美里似乎不太上台面但在我经手的几个实际系统里它反而是留存时间最长的那一个。原因很直接能力最全、行为最可预期。你调用的是官方工具它对 rar 各种变体的支持跟官方完全一致不会出现文档说支持、实际抛异常的落差。另一个隐性优势是错误信息。命令行工具会返回明确的退出码和一行可读的错误描述比 Java 异常堆栈友好太多。有一次线上解压批量失败我看退出码是 11一查是密码错误十分钟定位完毕换成纯 Java 库可能要先花半天写复现。当然代价也清楚你得保证目标环境里有这个可执行文件。这一条在容器化部署里不算事但在交付到客户机房的场景里就是硬门槛。我的处理办法是把它做成可选策略——能检测到可执行文件就用命令行检测不到自动降级到纯 Java 方案两套代码共用同一个接口。4.2 命令行参数一条条拆开讲先看 unrar 的调用。这条命令的参数看起来简单每个都有讲究unrar x -y -o -p- -idq -ep1 /data/in/pkg.rar /data/out/x表示保持完整路径解压。对应的e是扁平化解压会把所有文件都倒进目标目录、丢掉层级。多个同名文件就会互相覆盖这个坑我见过不止一次。-y对所有确认提示回答 yes。不加这个遇到是否覆盖的交互式提问进程会一直挂着等输入直到超时。-o表示覆盖已存在文件。-o-是跳过-o是覆盖。批处理场景必须显式指定否则行为取决于配置。-p-表示不询问密码。如果有密码就传-p你的密码注意是紧贴着写中间不能有空格。-idq关闭大部分输出只保留错误。这个参数在批量调用时很重要不然标准输出缓冲区被塞满进程会阻塞。-ep1从路径里排除盘符和根目录前缀。处理 Windows 打的包时特别有用否则会解出奇怪的绝对路径结构。再看 7z 的调用参数风格完全不同写错一个字符就报错7z x -y -o/data/out/ -pPASSWORD -- /data/in/pkg.rar-o后面直接跟路径中间绝对不能有空格这是最常见的手误。--表示后面全是文件参数防止文件名以-开头被当成选项。-p同理密码紧贴。4.3 完整封装超时、退出码、流读取一个都不能少调外部进程最容易出事的不是命令写错而是资源没管好。标准输出流不读会阻塞、进程卡住没有超时、异常时子进程残留这三个问题我全遇到过。下面这份封装是我现在基本会照搬的版本。import java.io.*; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; public class CommandRarExtractor { public static void extract(String unrarPath, File rarFile, File destDir, String password, long timeoutSeconds) throws Exception { destDir.mkdirs(); String pwdArg (password null || password.isEmpty()) ? -p- : -p password; ProcessBuilder pb new ProcessBuilder( unrarPath, x, -y, -o, -idq, -ep1, pwdArg, rarFile.getAbsolutePath(), destDir.getAbsolutePath() File.separator); pb.redirectErrorStream(true); Process process pb.start(); // 必须持续读取输出否则管道满了子进程会阻塞 StringBuilder output new StringBuilder(); Thread reader new Thread(() - { try (BufferedReader br new BufferedReader( new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { String line; while ((line br.readLine()) ! null) { output.append(line).append(\n); } } catch (IOException ignored) { // 进程被强杀时会走到这里忽略 } }); reader.setDaemon(true); reader.start(); boolean finished process.waitFor(timeoutSeconds, TimeUnit.SECONDS); if (!finished) { process.destroyForcibly(); throw new IOException(解压超时已强制终止: rarFile.getName()); } int code process.exitValue(); if (code ! 0 code ! 1) { throw new IOException(解压失败退出码 code 输出 output); } } }几点解释。redirectErrorStream(true)把错误流合并到标准输出这样一个线程就能读完不用起两个线程分别读 stdout 和 stderr——后者容易漏读导致死锁。退出码 1 我放行了因为 unrar 把部分文件有问题但其他成功也算作 1属于非致命警告直接失败会把大批量任务误伤。真正的致命错误是 2 和 3。超时值我一般给 300 秒大包可以放到 900 秒。这个数字的来源是我实测过一个 2GB 的 rar内含几百个小文件解压耗时约 40 秒按 5 到 10 倍余量留300 秒足够覆盖绝大多数业务包。如果超时频繁触发先怀疑是磁盘 IO 打满或者卷缺失而不是程序慢。4.4 容器化和跨平台的实务细节容器镜像里装 unrar 有个小坑很多精简基础镜像的软件源里没有这个包得从别的地方拿。另一个坑是许可——unrar 的分发条款要求不能用于实现压缩功能商用镜像里打包分发时要确认条款允许这一条法务通常会有意见所以不少团队最后选了 7z 的p7zip包它的 rar 解压能力是内置的。Windows 环境下如果你调的是桌面版压缩软件自带的UnRAR.exe务必用绝对路径不要依赖 PATH。我见过服务器上 PATH 指向了另一个老旧版本行为差异导致解出来的文件名编码不对。把路径做成配置项、启动时校验一次文件存在性并打日志这个习惯能省掉很多为什么测试环境好的、生产不对的追问。Mac 环境要注意的是从包管理器装的 unrar 版本可能跟 Linux 上的不完全一致某些参数支持情况有差异。如果你们的开发机是 Mac、生产是 Linux别只在 Mac 上测就上线至少要在与生产同构的镜像里跑一遍回归。5. 方式三7-Zip-JBinding 本地库绑定5.1 它补的是纯 Java 方案够不着的那部分7-Zip-JBinding 的思路是把 7-Zip 的 C 解压引擎通过 JNI 包装成 Java API。它同时拿到了两边的优点原生实现带来的格式支持完整度和性能以及 Java 侧的调用便利性和异常体系。对于那些必须用 Java 实现、但又必须支持 RAR5的项目这几乎是唯一解。它的代价集中在部署上。本地库必须跟运行平台匹配官方提供了全平台聚合包体积不小几十 MB打进镜像里会让镜像变大一圈。另外 JNI 调用意味着原生代码崩溃会直接把 JVM 带走而不是抛个异常给你 catch——虽然这种情况很少见但在压力测试里遇到过内存越界导致进程退出的情形排查起来比纯 Java 痛苦得多。依赖这样引dependency groupIdnet.sf.sevenzipjbinding/groupId artifactIdsevenzipjbinding/artifactId version16.02-2.01/version /dependency dependency groupIdnet.sf.sevenzipjbinding/groupId artifactIdsevenzipjbinding-all-platforms/artifactId version16.02-2.01/version /dependency如果你明确只跑 Linux x86_64可以把第二个依赖换成对应的单平台包镜像体积能省下大半。多平台包的意义在于开发机是 Mac、构建机是 Linux 时不用改配置。5.2 核心 API 调用与完整示例7-Zip-JBinding 有两套接口底层的IInArchive和简化的ISimpleInArchive。日常解压用简化接口就够了代码短、可读性好。下面这份是我现在用的版本。import net.sf.sevenzipjbinding.*; import net.sf.sevenzipjbinding.simple.ISimpleInArchive; import net.sf.sevenzipjbinding.simple.ISimpleInArchiveItem; import java.io.*; public class BindingRarExtractor { public static void extract(File rarFile, File destDir, String password) throws Exception { destDir.mkdirs(); String base destDir.getCanonicalPath() File.separator; try (RandomAccessFile raf new RandomAccessFile(rarFile, r)) { IInArchive archive SevenZip.openInArchive( null, new RandomAccessFileInStream(raf)); try { ISimpleInArchive simple archive.getSimpleInterface(); for (ISimpleInArchiveItem item : simple.getArchiveItems()) { String name item.getPath(); File target safeResolve(base, name); if (item.isFolder()) { target.mkdirs(); continue; } if (isSymlink(item)) { continue; // 符号链接整体跳过 } target.getParentFile().mkdirs(); try (OutputStream os new BufferedOutputStream( new FileOutputStream(target), 1 16)) { item.extractSlow(data - { try { os.write(data); } catch (IOException e) { throw new SevenZipException(写入目标文件失败, e); } return data.length; }, password); } } } finally { archive.close(); } } } private static boolean isSymlink(ISimpleInArchiveItem item) { Integer attr item.getAttributes(); // 0x8000 位在 Windows 属性里表示重解析点Unix 侧由库映射为符号链接 return attr ! null (attr 0x400) ! 0; } private static File safeResolve(String base, String entryName) throws IOException { File f new File(base, entryName); String canonical f.getCanonicalPath(); if (!canonical.startsWith(base)) { throw new IOException(非法条目路径: entryName); } return f; } }extractSlow里的回调返回的是本次实际消费的字节数必须如实返回data.length返回错了会导致数据错位。这一点文档里写得不显眼我第一次写的时候返回了 0结果解出来的文件全是空的排查了半天。5.3 密码、分卷与增量解压的实操要点密码直接传给extractSlow的第二个参数。这里有一个比命令行和 junrar 都好的地方如果密码错误它抛的是明确的SevenZipExceptionmessage 里会带Wrong password之类的信息不用靠猜。批量场景下我建议先做一次探测式解压——随便挑一个最小的条目试着解到内存里成功了再全量解避免解到一半才发现密码不对、留下一堆半成品文件。分卷需要实现IArchiveOpenVolumeCallback。核心方法是在需要下一卷时由你根据当前卷的文件名找到下一个文件并返回它的IInStream。实现起来的思路跟 junrar 的VolumeManager一样别把命名规则写死先扫目录再按序取。另外注意未找到的卷要返回 null而不是抛异常否则整个流程会被中断。增量解压是个容易被忽略的能力。7-Zip-JBinding 支持指定条目索引只解一部分接口是extract(int[] indices, ...)。这个在归档里有几千个文件、我只要其中一个的场景下能省掉大量 IO。我做过对比一个含 5000 个条目的包全量解压 18 秒按索引只解 3 个文件 0.2 秒。如果你只是要从包里捞一个配置文件别用全量解压。6. 三种方式横向对比与我的选型决策6.1 从六个维度看谁更划算对照表在第 2 章已经给过能力维度这里换个角度看成本维度更贴近决策时真正关心的东西。维度junrar命令行7-Zip-JBinding首次接入耗时半天以内半天到一天一天左右部署复杂度最低中需要装二进制中高需要本地库线上故障率我的经验值偏高多因格式不支持低低排查难度高异常抽象低退出码明确中单次调用开销最小有进程创建成本小长期维护成本中低中需跟版本首次接入耗时这一行很多人以为命令行最慢其实不然。纯 Java 方案看着简单但你得为编码、路径防护、格式兼容写一堆胶水命令行反而是把复杂度推给了外部工具Java 侧只剩进程管理。真正花时间的是 7-Zip-JBinding因为你要处理本地库加载失败、平台不匹配这类环境问题。6.2 按场景直接给答案不做铺垫直接给结论。给客户私有化部署、无法控制对方环境、包是 RAR4选 junrar。零依赖这条优势在这里压倒一切。编码修复和路径防护自己写扎实能稳定跑很多年。自己的服务、容器化部署、包可能是 RAR5、有密码、有分卷选命令行。把可执行文件打进镜像参数按第 4 章那样固定下来超时和退出码处理好基本不会出问题。这是我最常推荐的组合。自己的服务、必须纯 Java 交付比如受限于某些平台的约束、又要支持 RAR5选 7-Zip-JBinding并且只引单平台包。要注意镜像的 glibc 版本某些极度精简的镜像里本地库加载会失败报UnsatisfiedLinkError这时候换成完整一点的基础镜像就好了。不确定包是什么格式先写一个探测工具读文件头判断是 RAR4 还是 RAR5然后按上面的规则分流。这个探测逻辑二十行代码能让你的系统对上游的变化有韧性。7. 实战排查那些年解压 rar 踩过的坑7.1 文件名乱码的三层排查顺序乱码是最高频的问题但成因有三层按顺序排查效率最高。第一层确认包本身的编码。用命令行unrar l列出条目看输出里的文件名是否正常。如果命令行显示正常、Java 里乱码那就是你的库的解码方式不对按第 3.3 节的思路做编码修复。如果命令行也乱那就是打包时就乱了只能让上游改。第二层确认你用的是什么编码做的转换。常见错误是在读取流的时候就用错了InputStreamReader的编码导致原始字节在进入你的代码之前就已经被破坏了一次后面再怎么修都修不回来。原则是二进制流只用字节数组处理字符串转换放在最后一步。第三层确认目标文件系统。Linux 上文件名是字节序列理论上什么编码都能存但如果你在 Windows 上解开某些字符会直接写失败。跨国场景下建议统一转成 UTF-8 再落盘。7.2 密码正确却解压失败的几种可能密码明明没输错但解压就是失败这种情况我遇到过三种。一是密码里含有特殊字符比如$、!、空格走命令行时被 shell 吃掉了。解决办法是走ProcessBuilder的数组参数形式不要拼字符串再交给 shell 解析——这也是我一直坚持用ProcessBuilder(String...)而不是Runtime.exec(String)的原因。二是加密头。包开了加密头密码只解内容不解文件名你连条目列表都拿不到。这种包在某些库上表现为密码错误实际上是能力不支持。三是密码编码。有些工具打包时按本地码页把密码转成字节你在 Java 里用 UTF-8 传进去字节序列就不一样了。处理办法是先用命令行验证密码在目标机器上是否可用确认后再对齐 Java 侧。7.3 大文件与内存的几个姿势解压本身是流式的理论上不该吃内存但实际翻车往往发生在顺手用了不该用的 API。比如把整个条目读到ByteArrayOutputStream再写文件一个 2GB 的文件直接 OOM。原则很硬永远边读边写绝不整块缓存。另一个内存相关的点跟并发有关。如果你开了线程池并发解压每个线程 64KB 缓冲区看起来不多但 50 个并发就是 3MB 以上纯缓冲加上原生库自身的内存占用容易把容器内存打满。我的经验是并发数控制在 CPU 核数的 1 到 2 倍并且给解压任务单独设一个小线程池不要跟业务线程池混用。7.4 问题速查表把常见现象和对应处理整理成表出问题时直接对号入座。现象大概率原因处理办法抛异常读头部就失败RAR5 格式纯 Java 库不支持换命令行或 JNI 方案解出的文件名乱码RAR4 头部用 GBK按 UTF-8 解了按 ISO-8859-1 还原字节再按 GBK 解进程挂住不动输出流没读管道满单独线程持续读取输出进程挂住不动有交互提示等待输入加-y密码参数显式给出退出码 11密码错误核对密码注意特殊字符转义退出码 3CRC 校验失败包损坏让上游重传或校验包大小与哈希退出码 7命令行参数写错特别检查-o后面是否有空格UnsatisfiedLinkError本地库与平台不匹配换全平台包或检查基础镜像只解出前几个文件缺分卷扫同目录全部卷按序补齐磁盘瞬间打满解压炸弹按头部记录的解压后大小做上限校验文件写到目标目录外路径穿越规范化后校验前缀提示这张表里最容易被忽略的是只解出前几个文件。分卷包里缺一卷很多工具不会报错只是安静地停在那里你以为解完了。批量任务里最好加一条解出的文件数是否等于条目数的校验。8. 生产级封装的几点个人经验把三种方式都包装成同一个接口是我在几个项目里都做过的事。核心思路是定义一个RarExtractor接口方法签名统一成extract(File src, File dest, ExtractOption option)三个实现类分别对应三种方案用工厂或者策略模式在启动时按配置和探测结果选一个。这么做的好处是某个方案在特定环境下跑不通改一行配置就能切换不用动业务代码。接口里我把ExtractOption设计成一个带默认值的小对象字段包括密码、超时秒数、单文件大小上限、总大小上限、是否跳过符号链接、是否保留目录结构。这几个字段几乎覆盖了所有实际需求而且每一项都对着一类真实踩过的坑。特别是总大小上限加进去之后我们线的磁盘告警少了一大截。还有一件事值得提把每次解压的关键信息打成一行结构化日志包括包大小、条目数、解出字节数、耗时、使用的方案、退出码如果有。这一行日志在事后排查里的价值极高。我有一次排查为什么某个客户的包总是解压超时就是靠这行日志发现同一批包的条目数从几十个涨到了两万多问题出在上游的分包策略变了跟解压代码本身没关系。没有这行日志光看异常堆栈是绝对看不出来的。最后说个小技巧。如果你的系统需要同时兼容 RAR4 和 RAR5又不想为此付出两套方案的维护成本可以在入口处加一个极轻量的格式探测读前 8 个字节判断版本RAR4 走 junrarRAR5 走命令行或 JNI。这样 90% 的老包走的是零依赖的轻路径只有真正的新格式才需要重方案。我们在一个日处理量几千包的场景下用了这个分流纯 Java 路径扛住了八成以上的流量外部进程调用被压到了很低的频次整体稳定性和资源占用都好看很多。