Java Jar及类冲突分析思路

88 阅读3分钟

冲突异常

使用Java的同学,一定经常见到如下异常:

  1. ClassNotFoundException
  2. NoClassDefFoundError
  3. NoSuchMethodError
  4. NoSuchFieldError

查看JavaDoc:

ClassNotFoundException

Thrown when an application tries to load in a class through its string name using:
The forName method in class Class.
The findSystemClass method in class ClassLoader .
The loadClass method in class ClassLoader.

如其名,就是类找不到,在调用Class.forName()/ClassLoader.findSystemClass()/ClassLoader.loadClass()时如果在类路径找不到类就会抛出异常。

这个情况一般就是classpath下没有该类对应的jar。

NoClassDefFoundError

Thrown if the Java Virtual Machine or a ClassLoader instance tries to load in the definition of a class (as part of a normal method call or as part of creating a new instance using the new expression) and no definition of the class could be found.
The searched-for class definition existed when the currently executing class was compiled, but the definition can no longer be found.

在普通方法调用或者创建新实例时找不到类,这个属于JVM的ERROR错误,严重程度比异常更高。主要有两种场景:

1)编译时存在某个类,但是运行时找不到;

2)使用的类未初始化成功,需要检查类变量/静态代码块等初始化语句的正确性。

为什么编译时存在某个类,运行时找不到呢?

一般就是需要的jar不存在,或者存在pom坐标相同的jar版本冲突。也就是说编译时是存在的,编译后jar被删除了(在classpath中找不到),或者因为编译后只有旧版本的jar或新jar,没有指定的类。

NoSuchMethodError/NoSuchFieldError

与NoClassDefFoundError类似,分别是方法和字段找不到了,但也有不同。

即使是两个坐标不同的Jar,也可能导致类的方法或字段找不到,因为有的jar使用shaded模式打包,把其它的jar打包到自己的jar中,出现“子jar”与实际需要的jar冲突,JVM加载时首先加载了“子jar”中错误的同全限定名的类,在这个类里面没有需要的方法或字段。

举个例子,有一段NoSuchFieldError的报错,如下图

image.png 说在ResponsesMeta类第100行(指源码,非反编译的java行)的init()方法中,调用的某个类/对象的字段不存在,看源码:

responseMap.putIfAbsent(Status.TOO_MANY_REQUESTS.getStatusCode(), COMMON_EXCEPTION_JAVA_TYPE);

点进去是javax.ws.rs.core.Response.Status#TOO_MANY_REQUESTS,这个类在javax.ws.rs-api-2.1.jar中,在classpath下搜索,是存在的

image.png

定位分析

  1. 如果jar打包时保留了class信息,可以通过zgrep进行过滤搜索

试图通过zgrep搜索,找不到

image.png

  1. 通过Arthas查找类的加载路径

    1)搜索指定类

    image.png

    发现类是存在的,接下来通过jad反编译查找其所在路径。

    2)反编译指定类

    image.png

    找到位置,即/opt/mateinfo/app/webapps/sia/WEB-INF/lib/jersey-bundle-1.19.1.jar。

    本地反编译这两个jar,果然具有相同的全限定名。

    image.png

    至于后续处理,这个案例是排除无用的jersey-bundle解决了。

    如果两个jar都必须要,可以尝试通过控制classpath加载顺序规避;再极端一些,如果新版本的jar不能向前兼容,就需要考虑通过类加载器进行隔离了。