什么导致java.lang.IncompatibleClassChangeError错误?

262

我正在将一个Java库打包为JAR文件,在尝试调用其中的方法时,会抛出很多java.lang.IncompatibleClassChangeError错误,这些错误似乎是随机出现的。可能会出现哪些问题导致这个错误?


在我测试Apache FOP 1.0和Barcode4J的Eclipse项目中,随Barcode4J一起提供的附加库显然覆盖了FOP自带的库(其中一些版本号更高)。这是一个需要非常小心地将什么放入您的构建路径/类路径的情况。 - Wivani
21个回答

198
这意味着您对库进行了一些不兼容的二进制更改,但没有重新编译客户端代码。Java Language Specification §13详细说明了所有这些更改,其中最突出的是将非static非私有字段/方法更改为static或反之亦然。
重新编译客户端代码与新库,您应该就可以正常运行了。
更新:如果您发布公共库,请尽可能避免进行不兼容的二进制更改,以保留所谓的“二进制向后兼容性”。仅更新依赖JAR文件理想情况下不应该破坏应用程序或构建。如果确实需要破坏二进制向后兼容性,则建议在发布更改之前增加主要版本号(例如从1.x.y到2.0.0)。

3
由于某种原因,这里的开发人员在重新编译客户端代码时无法完全解决问题。如果他们编辑文件并重新编译,错误不再出现在那里,但是在引用该库的项目的其他位置,将会随机出现更多错误。我很好奇可能是什么原因导致这种情况。 - Zombies
6
你尝试过进行一次干净的构建(删除所有*.class文件)并重新编译吗?编辑文件也会产生类似的效果。 - notnoop
除非您认为JSP是这样的,否则不要动态生成代码。我们尝试删除类文件,但似乎没有帮助。奇怪的是,这对我来说似乎没有发生,但其他开发人员却会遇到这个问题。 - Zombies
2
你能确保在进行清理构建时,编译使用的是与运行相同的JAR包吗? - notnoop
是否与32位或64位平台有关,我发现只在64位平台上出现了一个错误。 - cowboi-peng

110

你的新打包库与旧版本不具备向后二进制兼容性(BC)。因此,一些未重新编译的库客户端可能会抛出异常。

这是 Java 库 API 中引起使用旧版本库构建的客户端在新版本上运行时可能抛出 java.lang.IncompatibleClassChangeError 的更改的完整列表(即破坏了 BC):

  1. 非final字段变为static字段
  2. 非常量字段变为非静态字段
  3. 类变为接口
  4. 接口变为类
  5. 如果你向类/接口添加一个新字段(或添加新的超类/超接口),则客户端类 C 的一个超级接口的静态字段可能会隐藏从 C 的超类继承的已添加字段(名称相同)(非常少见的情况)。

注意:其他不兼容的更改也会导致许多其他异常:NoSuchFieldError、NoSuchMethodError、IllegalAccessError、InstantiationError、VerifyError、NoClassDefFoundError 和 AbstractMethodError。

关于 BC 的最好论文是 Jim des Rivières 写的 “Evolving Java-based APIs 2: Achieving API Binary Compatibility”

还有一些自动工具可以检测这种更改:

使用 japi-compliance-checker 检查你的库:

japi-compliance-checker OLD.jar NEW.jar

clirr工具的使用方法:

java -jar clirr-core-0.6-uber.jar -o OLD.jar -n NEW.jar

祝你好运!


73

虽然这些答案都是正确的,但解决这个问题通常更加困难。它通常是由于classpath上存在两个略有不同版本的相同依赖项导致的,几乎总是由于与最初编译时使用的不同超类在classpath上 或者 其中某些传递闭包的导入不同,但通常在类实例化和构造函数调用时发生。(成功加载类和ctor调用后,你会得到 NoSuchMethodException 或其他类似的异常)。

如果行为看起来随机,那么很可能是多线程程序在类加载不同传递依赖项时基于哪些代码被首先执行而导致的。

要解决这些问题,请尝试以-verbose作为参数启动VM,然后查看在异常发生时正在加载的类。你应该会看到一些令人惊讶的信息。例如,同时具有多个副本的相同依赖项和版本,如果你知道它们被包含了,你可能从未接受过或预期过。

使用Maven解决重复的JAR包最好结合使用Maven的maven-dependency-pluginmaven-enforcer-plugin,或者使用SBT的Dependency Graph Plugin。然后将这些JAR包添加到您的顶级POM文件的< dependencyManagement >部分或作为SBT中导入的依赖项元素(以删除这些依赖项)。

祝你好运!


7
“verbose”参数帮助我确定哪些JAR包存在问题。谢谢。 - Virat Kadaru
1
作为对冗长输出的替代方案,你可以尝试使用IDE(例如我的情况是IntelliJ IDEA)中的“依赖分析器”。筛选警告信息并查看被警告的依赖项。 - GerardV

8
我还发现,当使用JNI从C++调用Java方法时,如果您以错误的顺序传递参数给被调用的Java方法,那么当您在调用的方法内部尝试使用这些参数时,您将会收到此错误(因为它们不是正确的类型)。一开始我感到很惊讶,JNI在调用方法时并没有为您执行此检查作为类签名检查的一部分,但我认为他们不执行此类检查是因为您可能正在传递多态参数,他们必须假设您知道自己在做什么。

示例C ++ JNI代码:

void invokeFooDoSomething() {
    jobject javaFred = FredFactory::getFred(); // Get a Fred jobject
    jobject javaFoo = FooFactory::getFoo(); // Get a Foo jobject
    jobject javaBar = FooFactory::getBar(); // Get a Bar jobject
    jmethodID methodID = getDoSomethingMethodId() // Get the JNI Method ID


    jniEnv->CallVoidMethod(javaFoo,
                           methodID,
                           javaFred, // Woops!  I switched the Fred and Bar parameters!
                           javaBar);

    // << Insert error handling code here to discover the JNI Exception >>
    //  ... This is where the IncompatibleClassChangeError will show up.
}

Java代码示例:

class Bar { ... }

class Fred {
    public int size() { ... }
} 

class Foo {
    public void doSomething(Fred aFred, Bar anotherObject) {
        if (name.size() > 0) { // Will throw a cryptic java.lang.IncompatibleClassChangeError
            // Do some stuff...
        }
    }
}

1
谢谢提示。当我使用错误的实例(this)调用Java方法时,我也遇到了同样的问题。 - sstn

5

我曾经遇到相同的问题,后来发现我在使用Java 1.4版本运行应用程序,但应用程序是使用版本6编译的。

实际上,原因是存在重复库,其中一个位于类路径中,另一个包含在位于类路径内的jar文件中。


你是如何找到问题根源的?我也遇到了类似的问题,但不知道如何追踪重复的依赖库。 - beterthanlife
@beterthanlife 写一个脚本,在所有的jar文件中搜索重复的类(按其完全限定名称搜索,即包含包名) :) - Eng.Fouad
好的,使用NetBeans和maven-shade,我认为我可以看到所有被复制的类以及它们所在的jar文件。其中许多jar文件我在pom.xml中没有直接引用,因此我认为它们必须是由我的依赖项包含的。有没有一种简单的方法可以找出哪个依赖项正在包含它们,而不必查看每个依赖项的pom文件?(请原谅我的新手,我是Java初学者!) - beterthanlife
@beterthanlife 最好提出一个新问题来询问 :) - Eng.Fouad
我通过查看Gradle依赖关系图(./gradlew :<name>:dependencies)找到了一个重复的jar包。因此,我找到了将旧版本库引入项目的罪魁祸首。 - nyx

2
在我的情况下,我通过以下方式遇到了这个错误。 我项目的 pom.xml 定义了两个依赖项 AB。 而且,AB 都定义了对同一构件(称其为 C)的依赖,但是它们使用了不同的版本 (C.1C.2)。 当出现这种情况时,对于 C 中的每个类,Maven 只能从两个版本中选择一个版本的类 (在构建 uber-jar 时)。 它将根据其 依赖关系管理 规则选择“最近”的版本,并输出警告 "We have a duplicate class..."。 如果方法/类签名在版本之间发生更改,则如果在运行时使用了不正确的版本,则可能会导致 java.lang.IncompatibleClassChangeError 异常。

高级:如果 A 必须使用 C 的 v1 版本,而 B 必须使用 C 的 v2 版本,则我们必须将 CAB 的 POM 文件中重新定位,以避免在构建最终依赖于 AB 的项目时出现重复类警告。


2
在我的情况下,当我在部署在Websphere 8.5上的应用程序中添加com.nimbusds库时,出现了错误。下面的异常发生了:
Caused by: java.lang.IncompatibleClassChangeError: org.objectweb.asm.AnnotationVisitor 解决方案是从库中排除asm jar。最初的回答。
<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>nimbus-jose-jwt</artifactId>
    <version>5.1</version>
    <exclusions>
        <exclusion>
            <artifactId>asm</artifactId>
            <groupId>org.ow2.asm</groupId>
        </exclusion>
    </exclusions>
</dependency>

1

我在使用glassfish卸载和重新部署war文件时遇到了这个问题。我的类结构像这样,

public interface A{
}

public class AImpl implements A{
}

它被更改为

public abstract class A{
}

public class AImpl extends A{
}

停止并重新启动域后,它正常工作了。我使用的是Glassfish 3.1.43。


1
我有一个Web应用程序,可以在我的本地机器的Tomcat(8.0.20)上完美部署。然而,当我把它放到qa环境(Tomcat-8.0.20)中时,它一直给我IncompatibleClassChangeError,并且抱怨我在扩展一个接口。这个接口被改为一个抽象类。我编译了父类和子类,但仍然遇到了同样的问题。最后,我想调试,所以我将父类的版本更改为x.0.1-SNAPSHOT,然后编译所有内容,现在它正在工作。如果有人在遵循这里给出的答案之后仍然遇到问题,请确保您的pom.xml中的版本也是正确的。更改版本以查看是否有效。如果是,那么解决版本问题。

1

出于某种原因,我正在进行一次大的重构,并开始遇到这个问题。我重命名了接口所在的包,问题得到解决。希望这可以帮到你。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接