The ClassLoader.loadClass method allows arbitrary user-defined behavior to occur during loading of classes. Notably, the method can throw any exception type, including RuntimeExceptions and Errors.
5.3 claims that, if any error occurs, "an instance of a subclass of LinkageError" may be thrown. It does not account for the possibility of exceptions that are not LinkageErrors.
The following test confirms that a RuntimeException thrown by ClassLoader.loadClass is exposed directly by class resolution.
~~~
public class Test {
public static void main(String... args) throws Exception {
PreemptingClassLoader l = new PreemptingClassLoader("A", "B");
Class<?> c = l.loadClass("A");
Runnable r = (Runnable) c.getDeclaredConstructor().newInstance();
r.run();
}
}
~~~
public class A implements Runnable {
public void run() {
System.out.println(B.foo);
}
}
~~~
public class B {
public static String foo = "foo";
}
~~~
import java.util.*;
import java.io.*;
public class PreemptingClassLoader extends ClassLoader {
private final Set<String> names = new HashSet<>();
public PreemptingClassLoader(String... names) {
for (String n : names) this.names.add(n);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (!names.contains(name)) return super.loadClass(name, resolve);
Class<?> result = findLoadedClass(name);
if (result == null) {
if (name.equals("B")) throw new RuntimeException("Hi, mom"); // INSERTED TO TRIGGER EXCEPTION
String filename = name.replace('.', '/') + ".class";
try (InputStream data = getResourceAsStream(filename)) {
if (data == null) throw new ClassNotFoundException();
try (ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
int b;
do {
b = data.read();
if (b >= 0) buffer.write(b);
} while (b >= 0);
byte[] bytes = buffer.toByteArray();
result = defineClass(name, bytes, 0, bytes.length);
}
}
catch (IOException e) {
throw new ClassNotFoundException("Error reading class file", e);
}
}
if (resolve) resolveClass(result);
return result;
}
}