views:

837

answers:

2

I'm looking into dynamic modification of classpath. I found one solution that works nicely but it does so using an explicit call to addURL(). (presumably at startup)

However, I would like to intercept the class-loading process at runtime to locate classes if the default classloader can't seem to find them. I tried to subclass ClassLoader so it just delegates findClass() and loadClass() to the default, and print out a debug line telling me these methods have been called, but they never seem to get called when my class uses dependent classes via implicit classloading, e.g.

// regular object instantiation with 'new'
BrowserLauncher launcher;
launcher = new BrowserLauncher();

// static methods
Foobar.doSomethingOrOther();

// Class.forName()
Class cl = Class.forName("foo.bar.baz");

// reflection on a Class object obtained statically
Class<Foobar> cl = Foobar.class;
// do something with cl, like call static methods or newInstance()

How does classloading work under these circumstances? (vs. the simpler case where Classloader.loadClass() is called explicitly)

Here's my attempt at a custom classloader, below. If I use DynClassLoader0.main() with an arguments list of {"some.package.SomeClass", "foo", "bar", "baz"}, and some.package.SomeClass references other classes found in external .jar files, using one of the methods listed above, why doesn't my DynClassLoader0's findClass() and loadClass() get called? The only time loadClass gets called is the explicit call to loadClass in the main() function below.

package com.example.test.classloader;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class DynClassLoader0 extends ClassLoader {
    public DynClassLoader0()
    {
     super();
    }
    public DynClassLoader0(ClassLoader parent)
    {
     super(parent);
    }
    public void runMain(String classname, String[] args) throws ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException
    {
 // [***] here we explicitly use our classloader.
     Class<?> cl = loadClass(classname);
     Method main = cl.getMethod("main", String[].class);
     main.invoke(null, new Object[] {args});
    }

    @Override protected Class<?> findClass(String name) throws ClassNotFoundException
    {
     System.out.println("findClass("+name+")");
     return super.findClass(name);
    }

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

    static public void main(String[] args)
    {
     // classname, then args
     if (args.length >= 1)
     {
      String[] classArgs = new String[args.length-1];
      System.arraycopy(args, 1, classArgs, 0, args.length-1);

      ClassLoader currentThreadClassLoader
       = Thread.currentThread().getContextClassLoader();
      DynClassLoader0 classLoader = new DynClassLoader0(currentThreadClassLoader);
      // Replace the thread classloader - assumes
      // you have permissions to do so
      Thread.currentThread().setContextClassLoader(classLoader);

      try {
       classLoader.runMain(args[0], classArgs);
      }
      catch (Exception e) {
       e.printStackTrace();
      }
     }
     else
     {
      System.out.println("usage: DynClassLoader {classname} [arg0] [arg1] ...");
     }
    }
}

edit: I have looked through these questions already:

edit: I thought what kdgregory is saying below is correct, that once I use my classloader explicitly (see line in code with [***] as a comment), all the code that executes from that class will cause implicit classloading from the same classloader. Yet my DynClassLoader0.loadClass() never gets called except during the outermost explicit call.

+3  A: 

To quote from the ClassLoader JavaDoc:

The methods and constructors of objects created by a class loader may reference other classes. To determine the class(es) referred to, the Java virtual machine invokes the loadClass method of the class loader that originally created the class.

In other words, once you load a class, that class tries to load other classes through the classloader that loaded it. In a normal Java application, that is the system classloader, which represents the classpath passed to the JVM, or the boot classloader, used to load the JVM runtime.

Depending on your needs, there's a variant of Class.forName() that takes a classloader as an argument. If you use this to load a particular class, then references within that class should use the specified classloader.


Edit: I started tracing through your example, but decided it would just be easier to give my own. If you're going to write your own classloader, I suggest starting with the existing URLClassLoader, because it handles a lot of the behind-the-scenes stuff.

So, MyClassLoader takes a single JARfile/directory and loads classes for that directory alone. I've overridden the three methods called to load a class, and simply log their invocation (using System.err because it doesn't buffer output, unlike System.out).

My example uses a library that I'm currently working on; it was convenient, but you can pick any library you want as long as it's not already in your classpath.

The main() method loads a class via MyLoader. Then I invoke a method on that class, in a way that I know will throw an exception that's also part of the library. Note that I invoke the method by reflection: since the library is not on my Eclipse classpath, I couldn't compile it with an explicit reference.

When I run this program (under Sun JDK 1.5 for Linux), I see a lot of calls to loadClass(), both for classes in my library and for those on the classpath. This is expected: the ParseUtil class references a lot of other classes, and will use MyLoader (ie, its classloader) to load them. For those classes that MyLoader can't find locally, it delegates up the loader tree.

The exception is thrown, and when I print out its classloader I see that it's the same as the MyLoader instance I created. I also print out the loader for Exception.class, and it's null -- which the JavaDoc for Class.getClassLoader() says indicates the boot classloader.

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;


public class ClassLoaderExample
{
    private static class MyClassLoader
    extends URLClassLoader
    {
        public MyClassLoader(String path)
        throws Exception
        {
            super(new URL[] { new File(path).toURL() });
        }

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException
        {
            System.err.println("findClass(" + name + ")");
            return super.findClass(name);
        }

        @Override
        protected synchronized Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            System.err.println("loadClass(" + name + "," + resolve + ")");
            return super.loadClass(name, resolve);
        }

        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException
        {
            System.err.println("loadClass(" + name + ")");
            return super.loadClass(name);
        }
    }


    public static void main(String[] argv)
    throws Exception
    {
        ClassLoader myLoader = new MyClassLoader("/home/kgregory/Workspace/PracticalXml-1.1/target/classes/");
        System.out.println("myLoader = " + myLoader);

        Class<?> parseUtilKlass = myLoader.loadClass("net.sf.practicalxml.ParseUtil");
        Method parseMethod = parseUtilKlass.getDeclaredMethod("parse", String.class);

        try
        {
            parseMethod.invoke(null, "not at all valid XML");
        }
        catch (InvocationTargetException e)
        {
            Throwable ee = e.getCause();
            System.out.println("exception:" + ee);
            System.out.println("exception loader = " + ee.getClass().getClassLoader());

            System.out.println("Exception.class loader = " + Exception.class.getClassLoader());
        }
    }
}


Edit #2, based on today's comments.

A classloader is expected to delegate requests to its parent before it attempts to fulfill the request itself (this is in the ClassLoader JavaDoc). There are a couple of benefits to this practice, foremost being that you won't unintentionally load incompatible instances of the same class.

J2EE classloaders amend this model: the classloader used to load a WAR will attempt to resolve classes before the loader for a containing EAR, which in turn attempts to resolve classes before the container's classloader. The goal here is isolation: if both the WAR and its EAR contain the same library, it's probably because the two need differing versions (that, or they have a sloppy build process). Even in the J2EE case, I believe that the container classloader delegates in the standard way.

kdgregory
"In other words, once you load a class, that class tries to load other classes through the classloader that loaded it." That's what I thought but it doesn't seem to be working 8-( I can't tell what I'm doing wrong.
Jason S
Thanks -- I'll try running your program and seeing how mine differs!
Jason S
Ah: I think I may understand the issue. If I use your approach AND I load a class that's not already on the classpath, it works correctly and recursively loads all the dependent classes through the classloader. If I use your approach and I load a class that is already on the classpath, it doesn't recursively load dependent classes.
Jason S
Does anyone know why this is the case?
Jason S
+1  A: 

In your code the call to super.loadClass() delegates the loading of the class to the parent classloader (just look at the implementation of java.lang.ClassLoader#loadClass). So it is not your instance of DynClassLoader0 that loads the class, but the currentThreadClassLoader (which you took from Thread.currentThread().getContextClassLoader()) that you passed as a constructor parameter to DynClassLoader0. And when the loaded class refers to other classes, they are then also loaded by that classloader and not your DynClassLoader0.

Esko Luontola
The JVM doesn't look to the context classloader to load new classes -- I originally thought it did, but the ClassLoader JavaDoc says otherwise, and a test program fails.
kdgregory
I know I'm delegating to the parent classloader in my DynClassLoader0.loadClass() by calling super.loadClass() -- my question is why DynClassLoader0.loadClass() would not get called.
Jason S
WAIT: I think I am starting to understand the dilemma. Look at the quote in kdgregory's answer ("The methods and constructors of objects created by a class loader may reference other classes. To determine the class(es) referred to, the Java virtual machine invokes the loadClass method of the class loader that originally created the class.") Is there any way to use the default class loader to do the class-creation magic, but tell the JVM, no look at my classloader for downstream classes that may need to be loaded in the future?
Jason S