使用DataInputStream/DataOutputStream打包Jar文件条目后读取触发java.io.UTFDataFormatException的问题求助
让我来帮你拆解这个问题:你的代码在写入Jar条目内容时存在两个致命错误,直接导致后续条目的数据格式完全混乱,最终触发UTFDataFormatException:
第一次读取的条目内容被彻底漏掉
在test()方法里,你先执行了int entryRealSize = stream1.read(buffer);,只把这个数值用来打印长度,却完全没把这部分读取到的字节写入DataOutputStream。紧接着你又进入循环读取剩余内容,这就导致第一次读取的entryRealSize字节直接丢失了。条目大小记录完全错误
你把entryRealSize(也就是单次读取的字节数)当成了整个条目的总大小写入输出流,但这个值只是缓冲区一次能装下的长度,根本不是条目的总字节数。后续你又写入了剩余的内容,导致输出流里记录的"条目大小"和实际写入的字节数严重不匹配。
这纯属巧合!第一个条目大概率内容非常短,刚好在第一次read(buffer)就读取完了所有内容——此时entryRealSize正好等于条目的总大小,而且后续循环stream1.read(buffer)返回-1,没有写入额外内容。所以第一个条目的名称、大小、内容都能正确对应,读取时自然没问题。
但后续条目内容更长,你记录的大小远小于实际写入的字节数,这些多出来的字节直接覆盖了下一个条目的名称数据。当下一次readUTF()试图读取下一个条目的名称时,拿到的是二进制内容(完全不是合法的UTF-8字节序列),自然就触发了格式异常。
我们需要正确读取Jar条目的所有内容,并且准确记录条目的总大小。这里提供两种可靠的修复方式,同时优化你的读取代码:
方式一:利用ZipEntry的getSize()获取总大小(推荐)
JarEntry继承自ZipEntry,可以直接通过getSize()获取条目总字节数,然后按长度读取内容:
public static void main(String[] args) throws Throwable { test(); System.out.println("========================================================================================="); final DataInputStream dataInputStream = new DataInputStream(new FileInputStream(new File("C:\\Users\\Admin\\Desktop\\randomJarOut"))); for (int entryTotal = dataInputStream.readInt(), i = 0; i < entryTotal; ++i) { final String utf = dataInputStream.readUTF(); System.out.println("Entry name: " + utf); final int entrySize = dataInputStream.readInt(); final byte[] array = new byte[entrySize]; dataInputStream.readFully(array); // 替换逐个字节读取,更高效可靠 System.out.println("Entry bytes length: " + array.length); } dataInputStream.close(); } // 解压原Jar并打包到新文件的方法 private static void test() throws Throwable { JarInputStream countStream = new JarInputStream(new FileInputStream(new File("C:\\Users\\Admin\\Desktop\\randomJar.jar"))); final DataOutputStream outputStream = new DataOutputStream(new FileOutputStream(new File("C:\\Users\\Admin\\Desktop\\randomJarOut"))); // 第一步:统计条目总数 int entryCount = 0; for (ZipEntry entry; (entry = countStream.getNextJarEntry()) != null; ) { entryCount++; } countStream.close(); // 关闭统计用的流 // 第二步:重新打开流读取每个条目的内容 JarInputStream contentStream = new JarInputStream(new FileInputStream(new File("C:\\Users\\Admin\\Desktop\\randomJar.jar"))); outputStream.writeInt(entryCount); byte[] buffer = new byte[2048]; for (JarEntry entry; (entry = contentStream.getNextJarEntry()) != null; ) { long totalSize = entry.getSize(); String entryName = entry.getName(); if (totalSize == -1) { // 处理无法获取大小的条目:先把内容读到临时流里 ByteArrayOutputStream tempBaos = new ByteArrayOutputStream(); int readLen; while ((readLen = contentStream.read(buffer)) != -1) { tempBaos.write(buffer, 0, readLen); } byte[] entryBytes = tempBaos.toByteArray(); System.out.println("Writing: " + entryName + " Length: " + entryBytes.length); outputStream.writeUTF(entryName); outputStream.writeInt(entryBytes.length); outputStream.write(entryBytes); } else { // 已知大小的条目:按总长度读取并写入 System.out.println("Writing: " + entryName + " Length: " + totalSize); outputStream.writeUTF(entryName); outputStream.writeInt((int) totalSize); int remaining = (int) totalSize; while (remaining > 0) { int readLen = contentStream.read(buffer, 0, Math.min(remaining, buffer.length)); outputStream.write(buffer, 0, readLen); remaining -= readLen; } } } // 关闭所有流 outputStream.flush(); outputStream.close(); contentStream.close(); }
方式二:动态读取所有内容到字节数组(兼容特殊条目)
如果有些Jar条目无法通过getSize()获取长度(比如动态生成的条目),可以先把条目内容全部读取到ByteArrayOutputStream中,再获取总长度:
public static void main(String[] args) throws Throwable { test(); System.out.println("========================================================================================="); final DataInputStream dataInputStream = new DataInputStream(new FileInputStream(new File("C:\\Users\\Admin\\Desktop\\randomJarOut"))); for (int entryTotal = dataInputStream.readInt(), i = 0; i < entryTotal; ++i) { final String utf = dataInputStream.readUTF(); System.out.println("Entry name: " + utf); final int entrySize = dataInputStream.readInt(); final byte[] array = new byte[entrySize]; dataInputStream.readFully(array); // 优化读取逻辑 System.out.println("Entry bytes length: " + array.length); } dataInputStream.close(); } private static void test() throws Throwable { JarInputStream stream = new JarInputStream(new FileInputStream(new File("C:\\Users\\Admin\\Desktop\\randomJar.jar"))); final DataOutputStream outputStream = new DataOutputStream(new FileOutputStream(new File("C:\\Users\\Admin\\Desktop\\randomJarOut"))); // 先收集所有条目名称并统计数量 int entryCount = 0; List<String> entryNames = new ArrayList<>(); byte[] buffer = new byte[2048]; for (ZipEntry entry; (entry = stream.getNextJarEntry()) != null; ) { entryCount++; entryNames.add(entry.getName()); // 先读完当前条目内容(避免流位置错乱) while (stream.read(buffer) != -1) {} } stream.close(); // 重新打开流读取每个条目完整内容 JarInputStream contentStream = new JarInputStream(new FileInputStream(new File("C:\\Users\\Admin\\Desktop\\randomJar.jar"))); outputStream.writeInt(entryCount); for (String entryName : entryNames) { contentStream.getNextJarEntry(); ByteArrayOutputStream entryBaos = new ByteArrayOutputStream(); int readLen; while ((readLen = contentStream.read(buffer)) != -1) { entryBaos.write(buffer, 0, readLen); } byte[] entryBytes = entryBaos.toByteArray(); System.out.println("Writing: " + entryName + " Length: " + entryBytes.length); outputStream.writeUTF(entryName); outputStream.writeInt(entryBytes.length); outputStream.write(entryBytes); } outputStream.flush(); outputStream.close(); contentStream.close(); }
内容的提问来源于stack exchange,提问作者Donald C. Spencer

