8

I'm trying to override the system's class loader using the flag -Djava.system.class.loader=MyLoader. However, MyLoader is still not being used when classes are loaded.

MyLoader's code:

public class MyLoader extends ClassLoader {
    public MyLoader(ClassLoader parent) {
        super(S(parent));
    }

    private static ClassLoader S(ClassLoader cl) {
        System.out.println("---MyLoader--- inside #constructor(" + cl + ")...");
        return cl;
    }

    @Override
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        System.out.println("---MyLoader--- inside loadClass(" + name + ", " + resolve + ")...");
        return super.loadClass(name, resolve);
    }
}

This is the main code:

public class Main {
    public static void main(final String args[]) throws Exception {
        System.out.println("---Main--- first line");
        System.out.println("---Main--- getSystemClassLoader(): " + ClassLoader.getSystemClassLoader());
        System.out.println("---Main--- getSystemClassLoader()'s loader: " + ClassLoader.getSystemClassLoader().getClass().getClassLoader());
        Call("javax.crypto.Cipher");
    }

    public static void Call(final String class_name) throws Exception {
        System.out.println("---Main--- calling Class.forName(" + class_name + ")...");
        Class.forName(class_name);
        System.out.println("---Main--- call complete");
    }
}

This is the output using the command java -Djava.system.class.loader=MyLoader -verbose -Xshare:off Main (cf. Eclipse run config):

[Opened C:\Program Files\Java\jre7\lib\rt.jar]
[Loaded java.lang.Object from C:\Program Files\Java\jre7\lib\rt.jar]
[Loaded java.io.Serializable from C:\Program
Files\Java\jre7\lib\rt.jar]
// etc etc... omitted since it's too long
[Loaded MyLoader from file:/C:/Documents%20and%20Settings/Owner/Desktop/Programs/Eclipse%20Workspace%202/Test93/bin/]
---MyLoader--- inside #constructor(sun.misc.Launcher$AppClassLoader@158046e)...
[Loaded sun.launcher.LauncherHelper from C:\Program Files\Java\jre7\lib\rt.jar]
[Loaded java.lang.StringCoding from C:\Program Files\Java\jre7\lib\rt.jar]
[Loaded java.lang.StringCoding$StringDecoder from C:\Program Files\Java\jre7\lib\rt.jar]
---MyLoader--- inside loadClass(Main, false)...
[Loaded Main from file:/C:/Documents%20and%20Settings/Owner/Desktop/Programs/Eclipse%20Workspace%202/Test93/bin/]
[Loaded java.lang.Void from C:\Program Files\Java\jre7\lib\rt.jar]
---Main--- first line
---Main--- getSystemClassLoader(): MyLoader@8697ce
---Main--- getSystemClassLoader()'s loader: sun.misc.Launcher$AppClassLoader@158046e
---Main--- calling Class.forName(javax.crypto.Cipher)...
[Opened C:\Program Files\Java\jre7\lib\jce.jar]
[Loaded javax.crypto.Cipher from C:\Program Files\Java\jre7\lib\jce.jar]
---Main--- call complete

As can be seen, even though Main is loaded using MyLoader, javax.crypto.Cipher is not loaded using MyLoader. The output shows that MyLoader.loadClass is only called once.

Why is MyLoader.loadClass not even called when javax.crypto.Cipher is being loaded from jce.jar?

Jason Law
  • 965
  • 1
  • 9
  • 21
Pacerier
  • 86,231
  • 106
  • 366
  • 634

2 Answers2

12

Your problem is that your custom class loader is being used to load Main, but its loadClass simply delegates to the parent class loader to load Main. Therefore. within Main, if you called Main.class.getClassLoader(), it would return the sun.misc.Launcher$AppClassLoader, not MyLoader.

To see what class loader will be used for Class.forName(String) calls and symbolic references from your own class, you should print getClass().getClassLoader() (or MyClass.class.getClassLoader() from a static method). The class loader that is used is the one that defined the class whose code is currently being executed. This is the rule everywhere except when using reflection (Class.forName(String, boolean, ClassLoader)).

Once a class is loaded from the parent class loader, any classes that it loads will also use that primitive class loader. So once Main is loaded from the sun.misc.Launcher$AppClassLoader class loader, all the classes that it calls will come from that same class loader, not from your own MyLoader. Similarly, once the javax.crypto.Cypher class is loaded from the null (aka Bootstrap) class loader, any classes that it mentions will also come from the bootstrap class loader except the classes it loads using reflection (SPI).

To stop loading classes from the sun.misc.Launcher$AppClassLoader class loader, set MyLoader's CLASSPATH to AppClassLoader's CLASSPATH and don't delegate classloading to AppClassLoader. Note that this will cause all the CLASSPATH classes to be loaded from MyLoader, but the classes from JDK will in general still be loaded from the null (Bootstrap) class loader.

To stop loading JDK classes from the bootstrap class loader, you must explicitly put the JDK into the classpath and modify loadClass to not check the parent first for some classes. Loading JDK classes from your own class loader is delicate; some classes (e.g. java.lang.String) must be loaded from the boostrap class loader. This is not something I have tried myself, but I have read that OSGi loads java.* from the bootstrap class loader but loads other JDK classes (e.g. sun.* and javax.*) from its own graph of class loaders.

/** Run with -Djava.system.class.loader=MyLoader to use this class loader. */
public static class MyLoader extends URLClassLoader {
    public MyLoader(ClassLoader launcherClassLoader) {
        super(getUrls(launcherClassLoader), launcherClassLoader.getParent());
    }

    private static URL[] getUrls(ClassLoader cl) {
        System.out.println("---MyLoader--- inside #constructor(" + cl + ")...");
        return ((URLClassLoader) cl).getURLs();
    }

    @Override public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        System.out.println("---MyLoader--- inside loadClass(" + name + ", " + resolve + ")...");
        return super.loadClass(name, resolve);
    }
}

As for the SPI factories within JDK (think XML parsers and crypto implementations), they use reflection to load named classes from either the ContextClassLoader or the SystemClassLoader or one after the other, because they want you to be able to define your own implementation, and the bootstrap class loader does not load user-defined classes. There seems to be no consistency in which one of the two they used, and I wish they just took a ClassLoader parameter instead of guessing.

Pacerier
  • 86,231
  • 106
  • 366
  • 634
yonran
  • 18,156
  • 8
  • 72
  • 97
  • So are you saying that the reason that we see the message "inside loadClass" when the calls are replaced with `Call("javax.swing.plaf.synth.SynthLookAndFeel")` and `Call("com.sun.crypto.provider.DHKeyFactory")` is due to SPI? – Pacerier Aug 25 '14 at 22:07
  • No, the argument to Call always goes to your class loader. But if your class loader returns a class from an ancestor class loader, then when you actually _use_ that class, everything that it does will come from that other class loader except explicit reflection (SPI implementations). E.g. `Class.forName("javax.xml.xpath.XPathFactory").getMethod("newInstance").invoke(null);` causes `inside loadClass(javax.xml.xpath.XPathFactory, false)`, `inside loadClass(com.sun.org.apache.xpath.internal.jaxp.XPathFactoryImpl, false)`; other classes used by XPathFactory (e.g. XPathFactoryFinder) are missing! – yonran Aug 26 '14 at 15:09
  • So what's the reason that we see `inside loadClass(sun.awt.resources.awt` when we do `Call("javax.swing.plaf.synth.SynthLookAndFeel")`? Is it due to `SynthLookAndFeel`'s static initializer doing some reflection that calls `ClassLoader.getSystemClassLoader`? – Pacerier Aug 26 '14 at 16:39
  • Yes, you’re right (I misunderstood you earlier). Loading com.sun.crypto.provider.DHKeyFactory causes its superclass to be statically initialized, causing the X.509 CertificateFactory and RSA KeyFactory to load (see [crypto SPI](http://docs.oracle.com/javase/8/docs/technotes/guides/security/crypto/CryptoSpec.html#Engine)). Meanwhile, javax.swing.plaf.synth.SynthLookAndFeel’s static initializer uses the system class loader through [ResourceBundle](http://docs.oracle.com/javase/8/docs/api/java/util/ResourceBundle.html). I found this out by setting a breakpoint in MyLoader.loadClass. – yonran Aug 26 '14 at 18:22
  • Sorry, I was mistaken again. com.sun.crypto.provider.DHKeyFactory and superclass do not contain static initializers themselves. The X.509 and RSA providers are loaded as a result of JarFile verification, since DHKeyFactory comes from sunjce_provider.jar, which [is a signed jar](http://docs.oracle.com/javase/8/docs/technotes/tools/windows/jarsigner.html) containing META-INF/ORACLE_J.RSA. The system class loader is used as a result of an SPI provider search, just not from a static initializer in this case. – yonran Aug 26 '14 at 21:01
  • You mentioned that to stop loading JDK classes from the the bootclassloader, we need to explicitly put the JDK into the classpath. I've done that (`java -cp "a;b;c;etc" Main`) but the message "---MyLoader---" doesn't appear. Why does MyLoader not get called to load any JDK classes? – Pacerier Aug 27 '14 at 01:28
  • If you want to load _some_ JDK classes yourself, you have to call `c = findClass(name); if (resolve) resolveClass(c);` for those classes _instead of_ `super.loadClass(name, resolve)`. But this is very tricky because some classes *must* be loaded by the bootstrap class loader (or are in packages that were already loaded), so you will get exceptions if you get the whitelist wrong. But I do know that OSGi does this, so it must be possible for _some_ JDK classes. You should google org.osgi.framework.bootdelegation with felix or equinox if you are interested. – yonran Aug 27 '14 at 15:24
  • Is there any difference between the "classpath stealing method" vs the "load with embedded loader" method? "load with embedded loader" means inside `MyLoader`'s `loadClass` it has e.g. `return defineClass(name, ConvertToBytes(ext_loader.getResourceAsStream(name.replace('.', '/').concat(".class")), 0, cBytes.length)`. – Pacerier Aug 28 '14 at 15:10
  • Yeah, that has the same effect. The point is that the class that it returns will have your own class loader as the defining class loader. – yonran Aug 28 '14 at 17:25
8

The Javadoc of Class.forName(String) states (emphasis mine):

Returns the Class object associated with the class or interface with the given string name. Invoking this method is equivalent to: Class.forName(className, true, currentLoader) where currentLoader denotes the defining class loader of the current class.

In other words, the method doesn't automatically use the system classloader - it uses the loader that physically defined the class from which it's called. From the Java language spec, section 5.3:

A class loader L may create C by defining it directly or by delegating to another class loader. If L creates C directly, we say that L defines C or, equivalently, that L is the defining loader of C.

Your custom classloader doesn't create the Main class directly - it delegates to the parent loader to create the class, so it's that parent classloader that will be used when you call Class.forName(String) in a method of Main. If you want to use your custom classloader directly, you'll need to either explicitly specify it using the 3-arg variant of Class.forName or alter your custom classloader implementation so that it actually loads the class in question (typically by extending URLClassLoader).

Alex
  • 13,811
  • 1
  • 37
  • 50
  • +1, Thanks, Javadoc comments should really link to the JLS whenever they use a key word like this, since they are after all, the *defining* interface to the function calls. – Pacerier Aug 25 '14 at 16:23
  • Also, how can we make it such that whenever we call `new T();` or `T.F()`, the system doesn't use the defining class's loader, but uses getSystemClassLoader() instead? – Pacerier Aug 25 '14 at 16:24
  • 1
    In general, you have to use reflection in order to sidestep Java's default behavior with respect to classloaders, and even then it isn't always possible to do what you're asking. It sounds like you're trying to do something highly custom with classloaders, so you might want to take a step back and post another question describing your high-level requirements to validate your overall approach. – Alex Aug 25 '14 at 16:50
  • So are you saying that there's no solutions to hook onto the default classloading behavior? – Pacerier Aug 25 '14 at 17:26
  • 1
    I won't outright say that it's not possible, but it's difficult and if you try, you might find yourself digging into native JVM code. Lots of that behavior is specified by the language or JVM spec, and exists as a safety net to ensure that once a class is loaded, it can locate its dependencies. If you simply want to be notified when classes are loaded, you might want to look into bytecode instrumentation with agents. – Alex Aug 25 '14 at 17:55
  • Thanks for the tip, looking into it. – Pacerier Aug 25 '14 at 18:40