JDK-6273389 : Request more diagnostic info from NoClassDefFoundError
  • Type: Enhancement
  • Component: hotspot
  • Sub-Component: runtime
  • Affected Version: 5.0,6
  • Priority: P4
  • Status: Closed
  • Resolution: Duplicate
  • OS: linux
  • CPU: x86
  • Submitted: 2005-05-19
  • Updated: 2013-05-01
  • Resolved: 2013-05-01
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 7
7-poolResolved
Related Reports
Duplicate :  
Duplicate :  
Relates :  
Relates :  
Description
Would like to see more helpful diagnostic info printed in the detail message of a NoClassDefFoundError when it is thrown in the normal circumstances: a class could not resolve a dependency. Consider the following demo:

---%<---
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.CodeSource;
import java.security.cert.Certificate;
public class TestNCDFE {
    public static void main(String[] args) throws Exception {
        String c1 = C1.class.getName();
        String c2 = C2.class.getName();
        URL u1 = new URL("http://nowhere.net/one.jar");
        URL u2 = new URL("http://nowhere.net/two.jar");
        ClassLoader bootstrap = ClassLoader.getSystemClassLoader().getParent();
        ClassLoader l1 = new TestLoader(u1, c1, bootstrap);
        ClassLoader l2 = new TestLoader(u2, c2, l1);
        System.out.println("We can load C2 OK: " + l2.loadClass(c2).newInstance());
        System.out.println("But should get NCDFE on C1: " + l2.loadClass(c1).newInstance());
    }
    private static final class TestLoader extends URLClassLoader {
        private final URL dummy;
        private final String loadable;
        public TestLoader(URL dummy, String loadable, ClassLoader parent) {
            super(new URL[] {dummy}, parent);
            this.dummy = dummy;
            this.loadable = loadable;
        }
        protected Class findClass(String name) throws ClassNotFoundException {
            if (name.equals(loadable)) {
                try {
                    InputStream is = TestNCDFE.class.getClassLoader().getResourceAsStream(name.replace('.', '/') + ".class");
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    byte[] buf = new byte[4096];
                    int i;
                    while ((i = is.read(buf)) != -1) {
                        baos.write(buf, 0, i);
                    }
                    byte[] data = baos.toByteArray();
                    return defineClass(name, data, 0, data.length, new CodeSource(dummy, (Certificate[]) null));
                } catch (IOException e) {
                    throw new ClassNotFoundException(name, e);
                }
            } else {
                throw new ClassNotFoundException(name);
            }
        }
        public String toString() {
            return super.toString() + "[" + dummy + "]->" + getParent();
        }
    }
    public static final class C1 {
        public C1() {
            new C2();
        }
    }
    public static final class C2 {}
}
---%<---

Here the dependencies are backwards: C1 is trying to refer to C2, yet C1's loader cannot access C2's loader.

If you try to run it in JDK 6.0, you get

---%<---
We can load C2 OK: TestNCDFE$C2@d9f9c3
Exception in thread "main" java.lang.NoClassDefFoundError: TestNCDFE$C2
        at TestNCDFE$C1.<init>(TestNCDFE.java:50)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:494)
        at java.lang.Class.newInstance0(Class.java:350)
        at java.lang.Class.newInstance(Class.java:303)
        at TestNCDFE.main(TestNCDFE.java:18)
---%<---

You can see the class that could not be found (C2) and in this example you can also guess at which class was referring to it (C1). What you don't see is an explanation for why there was a problem, because there is no information about what was loaded from where. Now if you get a NCDFE, either

1. You really deleted the referred-to .class file, which the JRE can't hope to help you with, but this is pretty easy to figure out - it's not there.

2. The referred-to .class file exists, but in the wrong loader. Sometimes this is quite subtle, e.g. in this example I tried to load both classes from l2, so it takes some thought to realize that the actual loader for c1 was l1 and therefore c1 cannot find c2 even though c2 is accessible from l2. You can figure out what is going on if you understand class loaders a bit, but you need a clue - and the current JRE does not give you the critical information.
###@###.### 2005-05-19 22:55:57 GMT
Would also appreciate having ClassCastException's include more details. Currently if you mix up your class loaders incorrectly it is possible to have code that says

SomeClass o = (SomeClass) something();

fail with

java.lang.ClassCastException: pkg.SomeClass

which is mystifying since that looks to be the very class you were casting to. Whereas in fact the return value of something() was assignable to a different instance of SomeClass.class! For the particular case that a JVM cast operation results in a CCE where the actual type and the desired type have the same name (or more generally where the actual type is assignable to a class with the same name as the desired type), the detail message reported by the JVM should include a toString() representation of both ClassLoader's, e.g.

java.lang.ClassCastException: pkg.SomeClass from java.net.URLClassLoader@123[file:/a.jar] is not assignable to pkg.SomeClass from java.net.URLClassLoader@456[file:/b.jar]

Comments
WORK AROUND Some people request that a NCDFE include the CNFE as a cause. The tricky bit is that one NCDFE in an application-defined loader can have as its "cause" many CNFEs, but only one is interesting, and it is impossible for the VM to know which one that is because it does not know which parent loader "should" have loaded the class. Throwable.initCause does not support attaching more than one cause, though you can work around this by daisy-chaining them. For example, a possible rewrite of ClassLoader.loadClass would be as follows: protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // First, check if the class has already been loaded Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClass0(name); } // BEGIN CHANGES } catch (ClassNotFoundException e1) { // If still not found, then invoke findClass in order // to find the class. try { c = findClass(name); } catch (ClassNotFoundException e2) { Throwable t = e2; while (t.getCause() != null) { t = t.getCause(); } t.initCause(e1); throw e2; } } // END CHANGES } if (resolve) { resolveClass(c); } return c; } Then the VM should simply attach the CNFE from the initiating loader's loadClass as a cause to the NCDFE it produces. The above should work even if some of the CNFEs from findClass have their own causes already, due to the while-loop. The drawback is that the wording of the stack trace will be misleading (e1 did not actually cause e2), but at least all diagnostic info will be there, assuming every ClassLoader.findClass is modified to include useful information in a CNFE it throws (e.g. attempted URLs, in the case of URLClassLoader).
16-10-2006

WORK AROUND None known. Classes are resolved in the VM so you can't make a clever ClassLoader that throws a nicer message. ###@###.### 2005-05-19 22:55:57 GMT
19-05-2005

SUGGESTED FIX What I would like to see is something like this: java.lang.NoClassDefFoundError: TestNCDFE$C2 (looked in TestNCDFE$TestLoader@abcdef[http://nowhere.net/one.jar]->sun.misc.Launcher$ExtClassLoader@123456; referred to from TestNCDFE$C1 at http://nowhere.net/one.jar) i.e. with a detail message of classNameNotFound + " (looked in " + referrer.getClassLoader() + "; referred to from " + referrer.getName() + " at " + referrer.getProtectionDomain().getCodeSource().getLocation() + ")" Such a message would make it much clearer what is going on, thus simplifying debugging: 1. Yes, C1 does depend on C2. 2. C1 was loaded from one.jar. 3. Classes in one.jar cannot access two.jar (where I know C2 is). I think such a change would not involve any kind of API change; just a patch to whatever part of the VM handles resolution of class references in bytecode. Of course for this kind of debugging to work well, ClassLoader.toString() has to be meaningful. If you are defining your own class loaders, you can do this yourself. It would be nice if URLClassLoader did it too: just print a list of URLs followed by its parent. ###@###.### 2005-05-19 22:55:57 GMT
19-05-2005