为何调整Tomcat 8中WEB-INF/lib下Jar顺序会引发NoClassDefFoundError?
问题背景
我们有一个部署在Tomcat 8上的Web应用,近期发现团队部分开发者构建的制品(.war文件)会抛出NoClassDefFoundError异常,而其他开发者构建的相同代码却能正常运行。
异常日志
来自logs/localhost.2018-05-11.log的核心错误日志:
org.jboss.resteasy.spi.UnhandledException: java.lang.NoClassDefFoundError: Could not initialize class org.geotools.referencing.datum.DefaultEllipsoid ...
Caused by: java.lang.NoClassDefFoundError: Could not initialize class org.geotools.referencing.datum.DefaultEllipsoid
at org.geotools.referencing.GeodeticCalculator.(GeodeticCalculator.java:277) ...
该异常有时会伴随(先于)以下异常:
org.jboss.resteasy.spi.UnhandledException: java.lang.IncompatibleClassChangeError: Implementing class ...
关键发现
- 正常和异常制品的内容几乎完全一致,唯一明显区别是
WEB-INF/lib目录下Jar文件的排列顺序不同。 - 将异常war包的lib目录下jar先复制到临时目录再移回后(jar顺序变为字母序),重启Tomcat异常消失:
# 原异常顺序 $ ls -U WEB-INF/lib x.jar b.jar y.jar a.jar c.jar z.jar $ cp -p WEB-INF/lib/* /tmp/lib/ $ rm -r WEB-INF/lib $ mv /tmp/lib WEB-INF/lib # 调整后正常顺序(系统cp后变为字母序) $ ls -U WEB-INF/lib a.jar b.jar c.jar x.jar y.jar z.jar
- 正常war包的jar并非都按字母序,但存在多种正常/异常的排列组合。
- 启用Tomcat详细类加载调试后,两种情况均记录
DefaultEllipsoid类从正确的Jar加载:
[Loaded org.geotools.referencing.datum.DefaultEllipsoid from file: /opt/tomcat/temp/1-webapp/WEB-INF/lib/gt-referencing-11.4.jar]
问题根源分析
我之前处理过好几起类似的Jar顺序导致的类加载异常,结合你的场景来看,核心点不是找不到类,而是类初始化失败——这也是NoClassDefFoundError和ClassNotFoundException最关键的区别:前者是类找到了,但初始化时出了问题,后者是根本找不到类。
具体到你的情况,大概率是这几个原因:
类初始化的依赖顺序“踩坑”
DefaultEllipsoid这个类在初始化的时候,会依赖GeoTools库中其他几个核心类(比如元数据、API模块里的类)。Tomcat加载Web应用时,会按照WEB-INF/lib目录下Jar的顺序来加载类,要是依赖类所在的Jar被排在后面,那DefaultEllipsoid初始化时就会遇到依赖类还没加载/初始化的情况,直接导致自身初始化失败,抛出这个错误。隐藏的依赖冲突“影子类”
虽然类加载日志显示DefaultEllipsoid来自正确的Jar,但IncompatibleClassChangeError提示类结构不兼容,这说明可能某个第三方Jar里包含了GeoTools的旧版类(也就是“影子类”)。当Jar顺序不对时,这些旧版依赖类可能先被加载,等DefaultEllipsoid初始化时,调用的是旧版类的方法/结构,自然就出现不兼容的错误,进而导致初始化失败。Maven构建的随机性“背锅”
Maven默认的Jar打包顺序是跟着依赖树遍历顺序走的,不同开发者的本地仓库依赖版本、构建环境(比如不同的Maven配置)都可能导致最终war包的Jar顺序不一样。刚好某些顺序触发了上面的初始化冲突,就出现了“部分人构建正常,部分人出问题”的情况。
解决方案
针对上述原因,可以采取以下措施:
- 强制指定Jar的加载优先级
在Tomcat的Context.xml中为该Web应用配置JarScanner,确保GeoTools核心Jar优先加载,避免依赖顺序问题:
<Context> <JarScanner> <JarScanFilter defaultPluggabilityScan="false" scanClassPath="true" scanAllFiles="false" scanAllDirectories="false"/> <!-- 优先加载GeoTools核心Jar --> <JarScanPattern name="gt-referencing-*.jar" /> <JarScanPattern name="gt-metadata-*.jar" /> <JarScanPattern name="gt-api-*.jar" /> <!-- 再加载其他所有Jar --> <JarScanPattern name="*.jar" /> </JarScanner> </Context>
- 统一Maven构建的Jar顺序
在项目的pom.xml中配置maven-war-plugin,强制指定lib目录的Jar排序规则,确保所有开发者构建的war包Jar顺序完全一致:
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>3.3.2</version> <configuration> <webResources> <resource> <directory>${project.build.directory}/${project.build.finalName}/WEB-INF/lib</directory> <targetPath>WEB-INF/lib</targetPath> <!-- 按文件名字母序排序Jar --> <sort>true</sort> <sortType>alpha</sortType> </resource> </webResources> </configuration> </plugin> </plugins> </build>
- 排查并清理冲突依赖
使用Maven命令排查依赖树,找出是否存在包含GeoTools类的第三方Jar,排除这些冲突依赖:
mvn dependency:tree -Dverbose | grep "geotools"
如果发现其他依赖间接引入了旧版GeoTools,在pom.xml中排除该依赖:
<dependency> <groupId>第三方依赖groupId</groupId> <artifactId>第三方依赖artifactId</artifactId> <version>版本号</version> <exclusions> <exclusion> <groupId>org.geotools</groupId> <artifactId>gt-referencing</artifactId> </exclusion> <!-- 按需排除其他冲突的GeoTools模块 --> </exclusions> </dependency>
- 跟踪类初始化过程定位问题
添加JVM参数跟踪类初始化细节,进一步定位具体的失败点:
# 在Tomcat的catalina.sh中添加以下参数 JAVA_OPTS="$JAVA_OPTS -verbose:class -XX:+TraceClassInitialization"
通过日志可以看到DefaultEllipsoid初始化时具体加载了哪些依赖类,以及是否有异常抛出。
内容的提问来源于stack exchange,提问作者Chris Finley

