有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

反射如何避免Java中未使用的代码引发NoClassDefFoundError

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

在大多数情况下,我已经非常容易地编写出这样的代码,工作得很好

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

即使PlatformOne在运行时不存在,事先检查意味着代码不会运行,也不会抛出错误。这种技术适用于绝大多数情况,但是我遇到过一个抛出错误的情况

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

这里有一个例子:

接口:

public interface DeleteInterface {

    void test(DeleteInterface delete);

}

类别:

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

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


共 (3) 个答案

  1. # 1 楼答案

    Java验证器可能在完全加载类之前抛出NoClassDefFoundError,因为必须存在其他验证,比如方法返回类型,此外,您在主类中这样做,启动时JRE会扫描主类,如堆栈跟踪中所示
    将不需要现有代码的代码移动到另一个类中,然后移动到要使用它的位置,首先检查该类是否存在,然后从该额外类调用方法:

    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();
        }
    

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

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

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

    如果所需的类在运行时不存在,肯定会抛出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.forName检查该类是否存在。例如

    // 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")) {
        ...
    }
    
  3. # 3 楼答案

    事实上,我今天遇到了这个问题

    确保没有在系统类装入器中两次装入同一类

    也就是说,我在前端线程中引用了一个a.b.class,我试图引用一个具有相同路径和类名的库方法,因此为我抛出了相同的错误

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

    希望这能有所帮助