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

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

The project I am working on is an API to support two different platforms. At runtime only one of the two platforms will actually be available on the classpath.

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

For the most part, I have been pretty easily able to write code like this that works fine

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

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

Even if PlatformOne does not exist at runtime, the check beforehand means the code does not run and no error will be thrown. This technique works for the VAST majority of situations however there is one case that I have run into where an error is thrown.

如果 PlatformOne 还实现了一个不存在的接口 AND 与 ALSO 不存在的参数一起使用,则在加载包含类时立即抛出 NoClassDefFoundError,无论代码是否实际执行.

If PlatformOne also implements a nonexistent interface AND that is used with a parameter that ALSO does not exist, then a NoClassDefFoundError is thrown immediately when the containing class is loaded, regardless of whether the code actually executes or not.

这是一个例子:

界面:

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 会在运行时产生以下错误:

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

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

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

Why is an error thrown only for this specific case, and what's the best way to work around it without access to any of the target platforms' code?

推荐答案

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

Java validator might throw NoClassDefFoundError before even fully loading your class because of additional validations, like method return types must exist, additionally you are doing that in your Main class that is scanned by JRE on launch as you can see in stack-trace.
Move code that requires not-existing code to other class and then in place where you want to use it first check if that class exist and then invoke method from that extra 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();
    }

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

This is just safest option for such cases, also if this is very short code you can just use some local class: (but it does not look good in most cases)

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

相关文章