Laird Nelson Laird Nelson - 4 days ago 5
Java Question

Does URLClassLoader traverse MANIFEST.MF Class-Path headers properly?

UPDATE 1: Indeed, the URL format differences cause the error. Here is a unit test (cut and obfuscated by hand; hope I didn't miss anything) showing the problem:

@Test
public void wheresWaldo2() throws ClassNotFoundException, IllegalAccessException, InstantiationException, IOException, NoSuchMethodException {
// Find Waldo from file:/someLocation/waldo.jar. Prove that works.
URL waldosJar = new File("/someLocation/waldo.jar").toURI().toURL();
assertNotNull(waldosJar);
assertEquals("file", waldosJar.getProtocol());
String waldosPath = waldosJar.getPath();
assertNotNull(waldosPath);
assertTrue(waldosPath.endsWith("/waldo.jar"));

ClassLoader cl = new URLClassLoader(new URL[] { waldosJar }, this.getClass().getClassLoader());

Class<?> waldosClass = cl.loadClass("com.foobar.Waldo");
assertNotNull(waldosClass);
assertEquals("com.foobar.Waldo", waldosClass.getName());
assertSame(cl, waldosClass.getClassLoader());

Class<?> jimbosClass = cl.loadClass("com.foobar.Jimbo"); // Note: works
assertNotNull(jimbosClass);

// Find Waldo from jar:file:/someLocation/waldo.jar!/. Prove that works.
// This URL, when passed to a URLClassLoader, should result in the same
// functionality as the first one. But it doesn't.
waldosJar = new URL("jar:" + waldosJar.toExternalForm() + "!/");
assertEquals("jar", waldosJar.getProtocol());
assertEquals("file:" + waldosPath + "!/", waldosJar.getPath());

cl = new URLClassLoader(new URL[] { waldosJar }, this.getClass().getClassLoader());

waldosClass = cl.loadClass("com.foobar.Waldo");
assertNotNull(waldosClass);
assertEquals("com.foobar.Waldo", waldosClass.getName());
assertSame(cl, waldosClass.getClassLoader());

jimbosClass = cl.loadClass("com.foobar.Jimbo"); // XXX FAILS
}


UPDATE 0: The problem may have to do with the supposed, but not actual, equivalence between two
URL
s referring to a jar file. For example, the following two URLs are supposed to refer to the same file:


  • file:/myjar.jar

  • jar:file:/myjar.jar!/



When I pass a URL built from the first format to my machinery, I think things are working. When I pass a URL built from the second format, I get the results described below. I am testing more to confirm all this beyond a doubt.

ORIGINAL QUESTION

(I am aware of this question.)

I have a jar file,
waldo.jar
, that contains a
META-INF/MANIFEST.MF
that looks like this:

Manifest-Version: 1.0
Class-Path: jimbo.jar


It also has a class at the following location within it:

com/foobar/Waldo.class


The class' source code is essentially:

package com.foobar;

public class Waldo {
public Jimbo getJimbo() {
return null;
}
}


Next, in the same directory, I have a jar file,
jimbo.jar
, that contains a class at the following location within it:

com/foobar/Jimbo.class


That class' source code is essentially:

package com.foobar;

public class Jimbo {

}


Now I construct a
URLClassLoader
with a URL to
waldo.jar
. To review:
jimbo.jar
contains
Jimbo
and is "next to"
waldo.jar
, and is listed in
waldo.jar
's
META-INF/MANIFEST-MF
's
Class-Path
header appropriately.
waldo.jar
contains
Waldo
, that has a code reference to
Jimbo
. With me so far?

I can load
com.foobar.Waldo
just fine. But if I do something with
Waldo
that involves
com.foobar.Jimbo
, like, for example, calling
Waldo.class.getDeclaredMethod("getJimbo")
, I get a
NoClassDefFoundError
. Here is a sample stack:

java.lang.NoClassDefFoundError: com/foobar/Jimbo
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
at java.lang.Class.getDeclaredMethod(Class.java:2128)
at com.foobar.TestClassLoadingProblems.wheresWaldo(TestClassLoadingProblems.java:115)

Caused by:
java.lang.ClassNotFoundException: com.foobar.Jimbo
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at com.foobar.MyClassLoader.findClass(...) // calls super.findClass()
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)


This suggests to me that
URLClassLoader
is not consulting the
Class-Path
header properly in all cases (I'm aware of how it works in general).

Can anyone shed light on what is happening here?

Answer

This is due to a bug in the JDK.

Comments