PAYATHON 2026

如何定位 Eclipse Tomcat 中导致 InvalidJarIndexException 的 JAR

排障赵师傅

结论

InvalidJarIndexException 通常表示某个 JAR 的 META-INF/INDEX.LIST 已经过期,或与 JAR 的实际内容不一致。异常栈中的 Registry、StandardServer、Connector 只是触发类加载的位置,不一定来自有问题的 JAR。

排查时可以先做以下四项检查:

  1. 在 Eclipse 中为 sun.misc.InvalidJarIndexException 设置异常断点,通过 sun.misc.URLClassPath$JarLoader.getResource() 栈帧查看正在处理的 JAR 和资源名。
  2. 检查 Tomcat、应用及插件实际加载范围内所有 JAR 的 META-INF/INDEX.LIST。
  3. 使用 -verbose:class 记录成功加载的类及其来源,再结合 Tomcat 的多级 ClassLoader 配置确认搜索路径。
  4. 对自己可以控制的应用 JAR,备份后删除或重新生成 INDEX.LIST。JAR Index 是可选功能,普通 Web 应用通常不依赖它。

调试器显示的当前 JAR 可能只是索引指向的目标。真正需要修复的文件有时是另一个更早加载的 JAR,因为过期的 INDEX.LIST 保存在那里。

INDEX.LIST 记录的内容

JAR Index 位于:

META-INF/INDEX.LIST

它不记录 Java 方法,通常也不会逐个列出所有 class,而是记录相关 JAR 的名称,以及各 JAR 中可以找到的 package 或资源路径前缀。

典型内容如下:

JarIndex-Version: 1.0

main.jar
com/example
META-INF

lib/dependency.jar
org/example/library

如果索引声称 dependency.jar 中包含 org/example/library,实际文件中却没有对应的 package 或资源,旧版 JDK 的 URLClassPath 可能会把它视为无效索引,并抛出 sun.misc.InvalidJarIndexException。

索引与文件不一致,通常有以下几种原因:

  • 被引用的 JAR 已更新,主 JAR 的索引却没有重新生成;
  • 依赖 JAR 被移动、重命名或删除;
  • 不同版本发行包中的 JAR 混在了一起;
  • 构建或复制过程保留了旧的 INDEX.LIST;
  • 同名 JAR 同时出现在不同的 ClassLoader 路径中。

用 Eclipse 异常断点定位

只看 Tomcat 输出的异常栈,通常无法确定涉及哪个 JAR。可以让线程在异常抛出时暂停,直接查看当时的加载状态。

在 Eclipse 中按以下步骤操作:

  1. 打开 Debug 视图。
  2. 新建 Java Exception Breakpoint。
  3. 填写异常类型:
sun.misc.InvalidJarIndexException
  1. 同时勾选 caught 和 uncaught。
  2. 以 Debug 模式启动 Tomcat。
  3. 线程暂停后,在调用栈中找到类似下面的栈帧:
sun.misc.URLClassPath$JarLoader.getResource(...)

检查该栈帧中的局部变量和对象字段,主要查看:

  • name:当时正在查找的 class 或资源;
  • jar:当前打开的 JarFile;
  • csu、base 或相关 URL 字段:JAR 的实际位置;
  • index:当前使用的 JarIndex;
  • loader:索引指向的另一个 JarLoader。

不同 JDK 6 更新版本的字段名可能略有差异,以调试器实际显示的内容为准。

如果当前栈帧显示的是 dependency.jar,还不能据此认定它就是坏文件。继续查看前一个 JarLoader 栈帧中的 index,确认哪个 JAR 的索引把资源查找引向了 dependency.jar。dependency.jar 本身可能完全正常,问题出在前一个 JAR 的过期索引中。

检查 JAR 是否包含索引

使用 JDK 自带的 jar 命令:

jar tf example.jar | grep '^META-INF/INDEX.LIST$'

有输出表示索引存在,没有输出则表示不存在。

查看索引内容:

unzip -p example.jar META-INF/INDEX.LIST

也可以先解压到临时目录:

mkdir /tmp/jar-check
cd /tmp/jar-check
jar xf /path/to/example.jar META-INF/INDEX.LIST
sed -n '1,200p' META-INF/INDEX.LIST

如果只是读取文件,使用 unzip -p 更方便,也不会修改原 JAR。

检查 JAR 是否损坏

先验证 ZIP/JAR 结构:

unzip -t example.jar

也可以执行:

jar tf example.jar >/dev/null

unzip -t 可以发现文件截断、CRC 错误等物理损坏。即使测试通过,也不能说明 INDEX.LIST 一定正确。语法完整的索引仍可能引用不存在的 JAR,或把 package 指向一个并不包含该 package 的 JAR。

批量检查某个目录下的 JAR:

find /path/to/tomcat /path/to/application -type f -name '*.jar' -print0 |
while IFS= read -r -d '' jarfile
do
    if ! unzip -tqq "$jarfile"; then
        printf '损坏的 JAR: %s\n' "$jarfile"
    elif jar tf "$jarfile" | grep -q '^META-INF/INDEX.LIST$'; then
        printf '包含 INDEX.LIST: %s\n' "$jarfile"
    fi
done

扫描范围最好限定在 Tomcat 和应用实际加载的目录内,无需直接扫描整个 /usr。系统中存在某个 JAR,并不表示当前 Tomcat 进程会加载它。

判断索引是否过期

JDK 6 没有通用命令可以针对任意部署结构完整验证 JAR Index 的语义。可以逐项检查以下内容:

  1. 第一行是否为:
JarIndex-Version: 1.0
  1. 索引列出的关联 JAR 是否存在。
  2. 相对路径以持有索引的 JAR 所在位置为基准时,是否仍然有效。
  3. 每个 package 或资源路径是否确实存在于对应的 JAR 中。
  4. 索引中的 JAR 名称、大小写和目录层级是否与部署文件一致。

例如,索引声称 lib/foo.jar 包含 com/example/foo,可以这样检查:

jar tf lib/foo.jar | grep '^com/example/foo/'

如果没有匹配项,该索引很可能已经过期。

还可以查找哪些索引提到了调试器中发现的目标 JAR:

find /path/to/runtime -type f -name '*.jar' -print0 |
while IFS= read -r -d '' jarfile
do
    if unzip -p "$jarfile" META-INF/INDEX.LIST 2>/dev/null |
       grep -Fq 'dependency.jar'; then
        printf '%s\n' "$jarfile"
    fi
done

这种方法通常可以找到真正持有错误索引的上游 JAR。

删除或重新生成索引

对于自行构建、未签名且不依赖 JAR Index 的应用 JAR,可以先备份,再删除索引进行验证:

cp application.jar application.jar.bak
zip -d application.jar META-INF/INDEX.LIST

如果删除索引后 Tomcat 能够启动,基本可以确认故障与该索引有关。

也可以重新生成索引:

cp application.jar application.jar.bak
jar i application.jar

jar i 会直接修改 JAR,因此执行前必须备份。还要确认 JAR 的 manifest、Class-Path 和相关依赖布局正确,否则重新生成的索引仍可能与实际部署结构不符。

不要直接修改第三方厂商 JAR、JDK 自带 JAR 或由操作系统包管理器维护的文件。可以重新安装对应软件包,或用同一发行版本中的原始 JAR 替换。

如果 JAR 已签名,删除或重新生成 INDEX.LIST 会改变归档内容,可能导致签名校验失败。这种情况应重新构建并签名,或取得未损坏的原始文件。

确认 Tomcat 的实际类加载路径

Tomcat 使用的并不是一个平铺的 CLASSPATH。Tomcat 5.5 有多个 ClassLoader 层级,通常包括:

  • JVM bootstrap 和 extension classes;
  • Tomcat system ClassLoader;
  • common、server、shared loader;
  • 每个 Web 应用自己的 WEB-INF/classes;
  • 每个 Web 应用自己的 WEB-INF/lib/*.jar。

具体目录和委派规则还会受到 conf/catalina.properties、启动脚本和 Eclipse 插件配置的影响。仅查看启动 Eclipse 时的环境变量 CLASSPATH,无法还原实际的类搜索过程。

先检查 Tomcat 配置:

grep -E '^(common|server|shared)\.loader=' \
    /path/to/tomcat/conf/catalina.properties

然后检查 Eclipse 启动的 Java 进程参数。Tomcat 运行期间可以执行:

ps -ef | grep '[o]rg.apache.catalina.startup.Bootstrap'

如果 Linux 提供 /proc,可以查看未被 ps 截断的参数:

tr '\0' '\n' < /proc/PID/cmdline

查看进程环境:

tr '\0' '\n' < /proc/PID/environ |
grep -E '^(CLASSPATH|JAVA_HOME|JRE_HOME|CATALINA_HOME|CATALINA_BASE)='

将 PID 替换为 Tomcat Java 进程的实际 PID。比较 JAVA_HOME、JRE_HOME 和 Eclipse Server Runtime 中选择的 JRE,确认升级 JDK 后没有出现编译使用一个 JDK、Eclipse 使用一个 JRE,而 Tomcat 又使用另一个 JRE 的情况。

记录类的来源和加载顺序

在 Tomcat 的 JVM 参数中加入:

-verbose:class

JDK 6 通常会输出类似信息:

[Loaded org.apache.commons.modeler.Registry from file:/path/to/commons-modeler.jar]

这些信息可以说明成功加载的类来自哪里,适合排查以下问题:

  • 同一个库存在多个版本;
  • Tomcat 加载了预期之外的系统 JAR;
  • WEB-INF/lib 中的库被父 ClassLoader 中的旧版本遮蔽。

-verbose:class 主要记录成功的类加载,无法完整显示每一次失败的资源查找,也不能单独证明哪个 INDEX.LIST 无效。排查时需要结合异常断点。

如果仍无法确认文件访问顺序,可以在测试环境中使用 strace:

strace -f -e trace=open,openat -o /tmp/tomcat-open.log \
    /path/to/tomcat/bin/catalina.sh run

旧版系统的 strace 可能不支持 openat,此时改为:

strace -f -e trace=open -o /tmp/tomcat-open.log \
    /path/to/tomcat/bin/catalina.sh run

然后筛选 JAR:

grep '\.jar' /tmp/tomcat-open.log

该日志记录的是操作系统层面的文件打开顺序,与 ClassLoader 的逻辑搜索顺序并不完全相同,因为已经打开的 JAR 可能会被缓存。它可以辅助确认进程实际访问过哪些文件。

缩小排查范围

如果包含索引的 JAR 很多,可以用二分法缩小范围:

  1. 备份当前 Tomcat 和应用部署目录。
  2. 移除应用自身 WEB-INF/lib 中一半的非必要 JAR。
  3. 重启 Tomcat,观察异常是否仍然出现。
  4. 根据结果继续拆分存在问题的那一半。
  5. 找到最小集合后,检查其中所有 INDEX.LIST、manifest Class-Path 和重复版本。

对于不能随意移除的核心依赖,可以复制一套测试部署目录后再操作。

还可以在应用自有、未签名的 JAR 副本中批量删除 META-INF/INDEX.LIST。如果异常消失,再逐个恢复索引,就能确定问题来源。不要对未备份的生产文件或系统 JAR 执行这种操作。

常见误判

Registry.registerComponent() 等位置反复报错,不等于存在六个损坏的 JAR。同一个过期索引可能在多个组件初始化过程中被反复触发。

find /usr -name '*.jar' 只能查找文件,无法证明这些文件属于当前 Tomcat 的加载路径。应以 Java 进程参数、Tomcat loader 配置、Web 应用目录和调试器中的 URL 为准。

没有 INDEX.LIST 并不是错误。它只是可选的优化文件,绝大多数 JAR 没有索引也能正常加载。

当前 JAR 中没有索引,也不能排除 JAR Index 问题。索引可能保存在更早加载的另一个 JAR 中,并把查找错误地转发到了当前 JAR。

还要核对 JDK 升级前后的启动配置。升级可能改变 Eclipse 使用的 JRE、Tomcat 的 extension directory、启动参数或可见的系统库。异常可能仍由旧索引触发,但运行时环境的变化会让这个问题突然暴露出来。

备注:内容仅供参考。