Java Tip 39: The trick to using a basic Java 1.1 network and file class loader

Having trouble with that class loader? Here's how to use one in Java 1.1 that works either over the 'Net or from a local file

One of the most frequent advanced Java questions I encounter is, "I'm having trouble loading classes over the Internet." In fact, in my own experience, when I finally had to deploy an application on an intranet, my class loaders failed. My attempts to take a shortcut and use the built-in RMIClassLoader succeeded with Java 1.0 applets but failed with Java 1.1 programs.

For his "Java In Depth" column in the October '96 issue of JavaWorld, Chuck McManis discussed in "The basics of Java class loaders." Based on that column, this Java Tip demonstrates how to successfully use the Java 1.1 class loader and goes a lot further.

Java class loader requirements

Let's first examine our requirements for a class loader. We'd like one that:

  • can load classes from any legitimate URL pointing to a class file

  • can also load classes from any local class file; this is useful for

    testing or simulating an Internet server on a client.

  • can optionally translate periods in the class name to a preferred character, such as "_"; this is useful for placing classes from different packages/directories into the same directory

  • follows the rules that all class loaders must obey

High-level design

The most flexible approach to the above requirements is these three classes:

     public abstract class MultiClassLoader extends ClassLoader
     public class URLClassLoader extends MultiClassLoader
     public class FileClassLoader extends MultiClassLoader

This allows the type of class loader to be determined at runtime and the rest of the application is unaffected.

Unit test

Hmmmm, fulfilling the above requirements doesn't sound too hard. Let's start with a "Use Case," namely a code example of how we'll use MultiClassLoader. This is best done by designing our test class, called TestLoader. Now, let's write the TestLoader unit test class. This shows how to set the class loader type at runtime, and then uses it in a variety of ways to illustrate how to use a class loader effectively. Here is the source code for TestLoader.java.

Let's examine the heart of the test code:

    // Init the preferred class loader
    MultiClassLoader loader = null;
    if (args.length == 0) {
        String root = "http://www.mindspring.com/~happyjac/";
        loader = new URLClassLoader(root);
    } else {
        loader = new FileClassLoader("store\\");
    }
    loader.setClassNameReplacementChar('_');
    // A series of tests:
    Class testClass = null;
    try {
        testClass = loader.loadClass("Hello");
    } catch(Exception ex) {
        print("Load failed");
        ex.printStackTrace();
        return;
    }
    print("Loaded class " + testClass.getName() );
    try {
        Runnable hello = (Runnable)testClass.newInstance();
        hello.run();
    } catch(Exception ex) {
        print("Failed to instantiate");
        ex.printStackTrace();
    }

Good tests are understandable and short. The test first creates an instance of class loader called "loader". Then it uses an optional feature to translate periods to "_".

Next we decide whether to load from the Internet or from a local file, depending on whether any command-line argument was entered. Note that you will need to set the root or filePrefix to whatever source URL you're using as a base. We have subclassed MultiClassLoader, providing a clean way to provide different specific class sources.

Then we attempt to load the Hello class, and we abort if the load fails. If an exception is thrown, it will be displayed on the command line. This is done with only one line of code, which illustrates the architectural strength of Java:

     testClass = loader.loadClass("Hello");

So far, so good. We've loaded the class, called testClass. Now we need an instance of testClass, easily done with the newInstance() method. But we also need to cast the Object returned by newInstance() to a class known by the class loader that loaded TestLoader. This is usually the primordial class loader. One way is to provide the desired interface locally, such as EntryPoint.class. But this violates the strategy of thin client, so instead we cast Object to Runnable, which is provided by the Java core API. This is all done in one of the most powerful lines of Java code you will encounter:

     Runnable hello = (Runnable)testClass.newInstance();

The Runnable interface has only one method, run(), so we

next do:

     hello.run();

And poof! -- we're done. That's it: three important lines of code, and the rest is done by Java and our MultiClassLoader. This three-line technique is known as "bootstrapping the client." Relax and rest awhile, three lines of code is hard work. < g >

A common mistake is to cast the object to something like EntryPoint, and when it works, to assume that all's well. Actually, the most likely reason it worked is because the primordial or another class loader found your local copy of EntryPoint.class using CLASSPATH when findSystemClass() was called; you probably thought it was loaded from elsewhere.

When Hello starts running, it runs an additional test to prove that MultiClassLoader is used to load "foreign" classes referenced by Hello, and that casting to foreign interfaces works. The test classes are Hello.java, Worker.java, and Person.java.

Examining MultiClassLoader

Let's examine the end result of loading Hello.class with our class loader. We now have Hello running. Any classes referenced by Hello will be loaded by the class loader that loaded Hello -- namely MultiClassLoader. The source code is MultiClassLoader.java.

As we can see from our previous test, the key method is:

  public Class loadClass(String className) throws ClassNotFoundException {
      return (loadClass(className, true));
  }

The method loadClass(String className) is a convenience method that calls another method that does the real work. This other method, loadClass(className, true), is the abstract method in java.lang.ClassLoader that all subclasses must implement. Here's the code:

public synchronized Class loadClass(String className,
        boolean resolveIt) throws ClassNotFoundException {
    Class   result;
    byte[]  classBytes;
    monitor(">> MultiClassLoader.loadClass(" + className + ", " + resolveIt + ")");
    //----- Check our local cache of classes
    result = (Class)classes.get(className);
    if (result != null) {
        monitor(">> returning cached result.");
        return result;
    }
    //----- Check with the primordial class loader
    try {
        result = super.findSystemClass(className);
        monitor(">> returning system class (in CLASSPATH).");
        return result;
    } catch (ClassNotFoundException e) {
        monitor(">> Not a system class.");
    }
    //----- Try to load it from preferred source
    // Note loadClassBytes() is an abstract method
    classBytes = loadClassBytes(className);
    if (classBytes == null) {
        throw new ClassNotFoundException();
    }
    //----- Define it (parse the class file)
    result = defineClass(classBytes, 0, classBytes.length);
    if (result == null) {
        throw new ClassFormatError();
    }
    //----- Resolve if necessary
    if (resolveIt) resolveClass(result);
    // Done
    classes.put(className, result);
    monitor(">> Returning newly loaded class.");
    return result;
}

The method is not very different from Chuck's original article. Subclassing ClassLoader is much simpler that writing the entire class loader. Note the use of monitor() for debugging. The comments in the code listing are straightforward, so I won't give you a detailed explanation here. If you'd like one, though, read Chuck's original article.

The most important thing loadClass() does is to perform a series of steps in the correct order, to satisfy the design of class loaders. If these steps are out of order, one is missing, or one is improperly implemented, you will witness abnormal behavior or security breaks. Satisfying these requirements cannot be over-emphasized.

Notice the "<code>//----- Try to load it from preferred source" block. This is the heart of the entire class, and it illustrates the beauty of the class loader concept: All a class loader does is get an array of bytes from a source somewhere and feed these bytes to Java appropriately. Such sweet simplicity! In our case, we are calling an abstract method, implemented by subclasses URLClassLoader or FileClassLoader. Note how you could easily subclass to handle more sources, such as a relational database.

The subclasses source code is URLClassLoader.java and FileClassLoader.java.

Let's look at URLClassLoder's key method:

protected byte[] loadClassBytes(String className) {
    className = formatClassName(className);
    try {
        URL url = new URL(urlString + className);
        URLConnection connection = url.openConnection();
        if (sourceMonitorOn) {
            print("Loading from URL: " + connection.getURL() );
        }
        monitor("Content type is: " + connection.getContentType());
        InputStream inputStream = connection.getInputStream();
        int length = connection.getContentLength();
        monitor("InputStream length = " + length); // Failure if -1
        byte[] data = new byte[length];
        inputStream.read(data); // Actual byte transfer
        inputStream.close();
        return data;
    } catch(Exception ex) {
        print("### URLClassLoader.loadClassBytes() - Exception:");
        ex.printStackTrace();
        return null;
    }
}

Creating loadClassBytes() is not at all difficult, thanks to Java's many handy classes. All the real work is done in one line of code: inputStream.read(data);

A final word of caution: Each subclass of MultiClassLoader should be used for only one source. Do not change a class loader's source after the class loader is instantiated. This will prevent disasters like a class with the same name but a different source being loaded from the wrong source -- which could happen if the source was changed dynamically. It will also make security breaks more difficult. Avoid the temptation to reset the source. Instead, design your subclasses to set the preferred source once and only once, so help you Java!

How to run your very own test

First download the following files from here:

     Hello.class
     test_store_Person.class
     test_store_Worker.class

Warning: These are files that run as an application -- not as an applet. They are trusted and are merely compiled classes from the source files mentioned above.

Download MultiClassLoader, URLClassLoader, FileClassLoader, and TestLoader (all .java files). Put them in the same directory, named "test," and compile TestLoader, which automatically also compiles the other classes. Open your Internet connection, such as by dial-up. Then run TestLoader via the usual java TestLoader. You should see:

Loading from URL: http://www.mindspring.com/~happyjac/Hello.class
Loaded class test.store.Hello
Hello class instantiated
Hello.run() called
Loading from URL:    http://www.mindspring.com/~happyjac/test_store_Worker.class
Loading from URL:    http://www.mindspring.com/~happyjac/test_store_Person.class
Worker class instantiated
Worker.startWorking() called
Worker class instantiated
Person first name is FirstName
Tested casting Worker to Person
Test complete

To test the ability to load classes from a file, add a subdirectory to the "test" directory named "store." Download the files Hello.java, Person.java, and Worker.java. Compile them. Rename Person.class to test_store_Person.class and Worker.class to test_store_Worker.class. Then on the "test" directory, run java TestLoader x without your dial-up open. You should see output similar to the first test except that it's loaded locally.

That's it. Have fun with your very own class loader. It gives you the power (with permission) to load a class from anywhere on the network. This means that if, as Sun claims, "the network is the computer," then you are now the network's absolute master. Once you understand the power of custom class loaders, they are a viable alternative to browsers for thin client. Thanks to Chuck McManis for the concepts in his JavaWorld column on class loaders!

Jack Harich, aka "Happy Jack," is a fun-loving Renaissance man who switched to software after a career as a sculptor came to a quick end due to a neck injury. He's currently a consultant in Atlanta (the Silicon Cotton Field of the South) and is very active with the Atlanta Java User's Group, its Java As A Second Language SIG, and the Atlanta Java Consortium.

Learn more about this topic