Change classloader

2020-02-09 09:01发布

问题:

I'm trying to switch the class loader at runtime:

public class Test {
    public static void main(String[] args) throws Exception {
        final InjectingClassLoader classLoader = new InjectingClassLoader();
        Thread.currentThread().setContextClassLoader(classLoader);
        Thread thread = new Thread("test") {
            public void run() {
                System.out.println("running...");
                // approach 1
                ClassLoader cl = TestProxy.class.getClassLoader();
                try {
                    Class c = classLoader.loadClass("classloader.TestProxy");
                    Object o = c.newInstance();
                    c.getMethod("test", new Class[] {}).invoke(o);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                // approach 2
                new TestProxy().test();
            };
        };
        thread.setContextClassLoader(classLoader);
        thread.start();
    }
}

and:

public class TestProxy {
    public void test() {
        ClassLoader tcl = Thread.currentThread().getContextClassLoader();
        ClassLoader ccl = ClassToLoad.class.getClassLoader();
        ClassToLoad classToLoad = new ClassToLoad();
    }
}

(InjectingClassLoader is a class extending the org.apache.bcel.util.ClassLoader which should load the modified versions of classes before asking it's parent for them)

I'd like to make the result of "approach 1" and "approach 2" exactly same, but it looks like thread.setContextClassLoader(classLoader) does nothing and the "approach 2" always uses the system classloader (can be determined by comparing tcl and ccl variables while debugging).

Is it possible to make all classes loaded by new thread use given classloader?

回答1:

The anonymous class you are creating via new Thread("test") { ... } has an implicit reference to the enclosing instance. Class literals within this anonymous class will be loaded using the enclosing class's ClassLoader.

In order to make this test work, you should pull out a proper Runnable implementation, and load it reflectively using the desired ClassLoader; then pass that explicitly to the thread. Something like:

    public final class MyRunnable implements Runnable {
        public void run() {
            System.out.println("running...");
            // etc...
        }
    }

    final Class runnableClass = classLoader.loadClass("classloader.MyRunnable");
    final Thread thread = new Thread((Runnable) runableClass.newInstance());

    thread.setContextClassLoader(classLoader); // this is unnecessary unless you you are using libraries that themselves call .getContextClassLoader()

    thread.start();


回答2:

I think InjectingClassLoader may be important here. Remember how classloading delegation works - if more than one classloader in your hierarchy can find the class, the top-most classloader will be the one that loads. (See Figure 21.2 here)

Since InjectingClassLoader doesn't specify a parent in its constructor, it will default to the constructor in the abstract ClassLoader, which will set the current context classloader as InjectingClassLoader's parent. Therefore, since the parent (old context classloader) can find TestProxy, it always loads the class before InjectingClassLoader gets a chance to.