如何避免 Java 中未使用的代码抛出 NoClassDefFoundError

How to avoid NoClassDefFoundError thrown by unused code in Java

我正在从事的项目是 API 以支持两个不同的平台。在 运行 时间,class 路径上实际上只有两个平台之一可用。

在大多数情况下,我很容易就能编写出像这样运行良好的代码

if (isPlatformOne()) {
    PlatformOne.doSomething();
}

即使PlatformOne在运行时不存在,事先检查意味着代码不运行并且不会抛出错误。这种技术适用于大多数情况,但有一种情况 运行 会引发错误。

如果 PlatformOne 也实现了一个不存在的接口 AND 并且与一个不存在的参数一起使用,那么当包含 class 被加载时会立即抛出 NoClassDefFoundError,不管代码是否实际执行。

举个例子:

接口:

public interface DeleteInterface {

    void test(DeleteInterface delete);

}

Class:

public class DeleteClass implements DeleteInterface {

    @Override
    public void test(DeleteInterface delete) {
    }

}

主要:

public class Test {

    private final boolean test; //Cannot be constant or compiler will erase unreachable code

    public Test() {
        test = false;
    }

    public static void main(String[] args) {
        if (new Test().test) {
            DeleteClass c = new DeleteClass();
            c.test(c);
        }

        System.out.println("SUCCESS!");
    }

}

从 jar 中删除 DeleteClassDeleteInterface 在 运行 时产生以下错误:

A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.NoClassDefFoundError: com/kmecpp/test/DeleteInterface
        at java.lang.Class.getDeclaredMethods0(Native Method)
        at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
        at java.lang.Class.privateGetMethodRecursive(Class.java:3048)
        at java.lang.Class.getMethod0(Class.java:3018)
        at java.lang.Class.getMethod(Class.java:1784)
        at sun.launcher.LauncherHelper.validateMainClass(LauncherHelper.java:544)
        at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:526)
Caused by: java.lang.ClassNotFoundException: com.kmecpp.test.DeleteInterface
        at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        ... 7 more

为什么只针对这种特定情况抛出错误,在不访问任何目标平台代码的情况下解决该错误的最佳方法是什么?

实际上我今天遇到了这个问题。

确保您没有在系统 class 加载程序中加载相同的 class 两次。

I.E) 我在前端线程中引用了 a.b.class,我试图引用具有相同路径和 class 名称的库方法,因此抛出对我来说同样的错误。

我将代理引用中的名称更改为与前端引用不同,错误停止了。

希望这对您有所帮助

Deleting DeleteClass and DeleteInterface from the jar produces the following error at runtime:

如果需要的class在运行时不存在,肯定java.lang.NoClassDefFoundError会被抛出。

Even if PlatformOne does not exist at runtime, the check beforehand means the code does not run and no error will be thrown.

请检查您的代码是否消化了抛出的错误,如果是,您的应用将不会崩溃并且可以正常执行。例如。下面的代码片段将抛出 NoClassDefFoundError 但不会崩溃,因为您消化了错误。

public bool isPlatformOne() {
    try  {
        ...
        return true;
    }  catch (ClassNotFoundException e) {
       return false;
    }
}

如果您的用例只是检查特定 class 是否存在,那么您可以使用 Class.forName 来检查 class 是否存在。例如。

// className is the fully qualified class name. 
public boolean hasClass(String className) {
    try  {
        Class.forName(className);
        return true;
    }  catch (ClassNotFoundException e) {
        return false;
    }
}

在代码中使用它的示例。

if (hasClass("android.support.v7.app.AppCompatActivity")) {
    ...
}

Java 验证器可能会在完全加载你的 class 之前抛出 NoClassDefFoundError 因为额外的验证,比如方法 return 类型必须存在,此外你正在这样做正如您在堆栈跟踪中看到的那样,您的 Main class 在启动时由 JRE 扫描。
将需要不存在代码的代码移动到其他 class,然后在您要使用它的地方首先检查 class 是否存在,然后从那个额外的 class 调用方法:

class MyExtraClass {
    public static void doStuff() {
        DeleteClass c = new DeleteClass();
        c.test(c);
    }
}

public boolean checkForClass(String className) {
    try  {
        Class.forName(className);
        return true;
    }  catch (ClassNotFoundException e) { return false; }
}

// somewhere in your code
    if (checkForClass("package.DeletedClass")) {
        MyExtraClass.doStuff();
    }

对于这种情况,这是最安全的选择,如果这是非常短的代码,您也可以使用一些本地 class:(但在大多数情况下看起来不太好)

// somewhere in your code
    if (checkForClass("package.DeletedClass")) {
        new Runnable() {
            @Override public void run() {
                DeleteClass c = new DeleteClass();
                c.test(c);
            }.run();
        }
    }