How to avoid NoClassDefFoundError thrown by unused

2019-07-29 01:56发布

问题:

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 pretty easily been able to write code like this that works fine

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

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.

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.

Here's an example:

Interface:

public interface DeleteInterface {

    void test(DeleteInterface delete);

}

Class:

public class DeleteClass implements DeleteInterface {

    @Override
    public void test(DeleteInterface delete) {
    }

}

Main:

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!");
    }

}

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?

回答1:

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


回答2:

I had this issue today actually.

Make sure that you are not loading the same class twice in the systems class loader.

I.E) I had a reference being made to a.b.class in a front-end thread, and I was attempting to reference a library method with the same path and class name, and thus threw the same error for me.

I changed the names in the agent references to be different from the front-end references and the error ceased.

hope this can help



回答3:

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

If the needed class does not exist at runtime, for sure, java.lang.NoClassDefFoundError will be thrown.

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

Please check if your code digested the error thrown, if yes, your app won't crash and can execute as normal. E.g. below code snippet will throw the NoClassDefFoundError but won't crash because that you digest the error.

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

If your use case is just to check if a particular class exists, then you can use Class.forName to check the class's existence. E.g.

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

Example for using it in code.

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