JarInputStream中JarEntry.getSize()返回-1的问题咨询
JarInputStream无法获取条目大小,但JarFile可以的问题分析与解决
问题现象
部分Jar文件通过JarInputStream能正常返回entry.getSize()的值,部分则不行:
- 可正常返回的情况:2013年以来在OSX Java 8及更高版本创建的Jar文件、
mongo-spark-connector-10.0.0.jar等; - 无法返回的情况:
antlr-runtime-4.7.2.jar、mongodb-driver-core-4.9.0.jar、hadoop-azure-3.2.0.jar等; - 所有Jar文件通过
JarFile均可正常获取大小。
当前场景是从数据库获取完整Jar字节数组,初始化JarInputStream的代码如下:
byte[] bb = get complete set of bytes; // 打印bb.length为14523,确认正常 ByteArrayInputStream bas = new ByteArrayInputStream(bb); JarInputStream js = new JarInputStream(bas);
遍历Jar条目的代码:
JarEntry entry; while ((entry = js.getNextJarEntry()) != null) { if (!entry.isDirectory() && entry.getName().endsWith(".class")) { String className = entry.getName().replace('/', '.').substring(0, entry.getName().length() - 6); long len = entry.getSize(); long zlen = entry.getCompressedSize(); System.out.println(" class [" + className + "]: Z " + zlen + "; unZ " + len); if(len > 0) { byte[] classBytes = new byte[len]; js.read(classBytes); System.out.println("captured [" + className + "]"); classBytesMap.put(className, classBytes); } ...
循环能正常提取类名,但entry.getSize()和entry.getCompressedSize()始终返回-1(使用OpenJDK 17.0.7版本)。最终目标是遍历JarInputStream并提取类名-类字节的映射关系。
测试验证
将byte[] bb写入本地文件XX2.jar,用JarFile读取时能正常获取大小:
JarFile jf = new JarFile("XX2.jar"); for (java.util.Enumeration<JarEntry> e = jf.entries(); e.hasMoreElements();) { entry = e.nextElement(); if (!entry.isDirectory() && entry.getName().endsWith(".class")) { String className = entry.getName().replace('/', '.').substring(0, entry.getName().length() - 6); int x = (int)entry.getSize(); int cx = (int)entry.getCompressedSize(); System.out.println(" class [" + className + "]: Z " + cx + "; unZ " + x); } }
输出:
class [org.bson.AbstractBsonReader$1]: Z 506; unZ 792 class [org.bson.AbstractBsonReader$Context]: Z 497; unZ 1227 class [org.bson.AbstractBsonReader$Mark]: Z 813; unZ 2201
但用FileInputStream包装成JarInputStream读取该文件时,依然无法获取大小:
JarInputStream jarInputStream = new JarInputStream(new FileInputStream("XX2.jar")); // 循环正常执行,但size和compressedSize仍为-1
完整演示示例:
import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import java.util.jar.JarFile; import java.io.FileInputStream; class jartest { public static void showEntry(JarEntry entry) { if (!entry.isDirectory() && entry.getName().endsWith(".class")) { String className = entry.getName().replace('/', '.').substring(0, entry.getName().length() - 6); int x = (int)entry.getSize(); int cx = (int)entry.getCompressedSize(); System.out.println(" class [" + className + "]: Z " + cx + "; unZ " + x); } } public static void main(String[] args) { try { JarEntry entry; String fname = "XX2.jar"; // 此方式正常工作 JarFile jf = new JarFile(fname); for (java.util.Enumeration<JarEntry> e = jf.entries(); e.hasMoreElements();) { entry = e.nextElement(); showEntry(entry); } // 此方式无法正常工作 JarInputStream js = new JarInputStream(new FileInputStream(fname)); while ((entry = js.getNextJarEntry()) != null) { showEntry(entry); } } catch(Exception e) { System.out.println("fail: " + e); } } }
运行输出:
$ java jartest class [grun$ExecutionContextImpl]: Z 706; unZ 1245 class [grun]: Z 3210; unZ 6476 class [grun$ExecutionContextImpl]: Z -1; unZ -1 class [grun]: Z -1; unZ -1
原因分析
Jar文件的元数据存储有两种方式:
- 中央目录(Central Directory):位于Jar文件末尾,包含所有条目的完整元数据(大小、压缩大小等),
JarFile直接读取这部分数据,因此能获取准确值; - 局部文件头(Local File Header):每个条目的开头部分,部分Jar文件的局部头可能未写入完整大小信息(比如现代构建工具为减小体积省略了这些字段)。
JarInputStream只能读取当前条目的局部头,无法提前访问末尾的中央目录,因此当局部头缺失大小信息时,会返回-1。
不同工具或版本创建的Jar文件,对局部头的写入策略不同,导致了这种差异。
解决方法
方案1:直接读取流直到条目结束
不依赖条目大小元数据,用ByteArrayOutputStream收集所有字节:
JarEntry entry; while ((entry = js.getNextJarEntry()) != null) { if (!entry.isDirectory() && entry.getName().endsWith(".class")) { String className = entry.getName().replace('/', '.').substring(0, entry.getName().length() - 6); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = js.read(buffer)) != -1) { baos.write(buffer, 0, bytesRead); } byte[] classBytes = baos.toByteArray(); System.out.println("captured [" + className + "], size: " + classBytes.length); classBytesMap.put(className, classBytes); js.closeEntry(); // 关闭当前条目,准备读取下一个 } }
方案2:使用临时文件转JarFile读取(若允许创建本地文件)
将数据库获取的字节数组写入临时Jar文件,再用JarFile读取:
// 创建临时文件 File tempJar = File.createTempFile("temp", ".jar"); try (FileOutputStream fos = new FileOutputStream(tempJar)) { fos.write(bb); } try (JarFile jf = new JarFile(tempJar)) { Enumeration<JarEntry> entries = jf.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); if (!entry.isDirectory() && entry.getName().endsWith(".class")) { String className = entry.getName().replace('/', '.').substring(0, entry.getName().length() - 6); byte[] classBytes = new byte[(int) entry.getSize()]; try (InputStream is = jf.getInputStream(entry)) { is.read(classBytes); } classBytesMap.put(className, classBytes); } } } finally { tempJar.deleteOnExit(); // 程序退出时删除临时文件 }
内容的提问来源于stack exchange,提问作者Buzz Moschetti
相关产品推荐
相关产品推荐

