Book Home Java Distributed Computing Search this book

2.3. The ClassLoader

The Java runtime environment is based upon a virtual machine that interprets, verifies, and executes classes in the form of platform-independent bytecodes. In addition, the Java API includes a mechanism for you to load class definitions in their bytecode form, and integrate them into the runtime environment so that instances of the classes can be constructed and used. When your Java files are compiled, a similar mechanism is invoked whenever an import statement is encountered. The referenced class or package of classes is loaded from files in bytecode format, using the CLASSPATH environment variable to locate them on the local file system.

In addition to this default policy for loading classes, the java.lang.ClassLoader class allows the user to define custom policies and mechanisms for locating and loading classes into the runtime environment. The ClassLoader is an abstract class. Subclasses must define an implementation for the loadClass() method, which is responsible for locating the class based upon the given string name, loading the bytecodes comprising the class definition, and (optionally) resolving the class. A class has to be resolved before it can be constructed or before any of its methods can be called. Resolving a class includes finding all of the other classes that it depends on, and loading them into the runtime as well.

The ClassLoader is an important element of the network support in the Java API. It's used as the basis for supporting Java applets in most Java-enabled web browsers, for example. When an HTML page includes an APPLET tag that references a Java class on the HTTP server, a ClassLoader instance within the browser's Java runtime is used to load the bytecodes of the class into the virtual machine, create an instance of the class, and then execute methods on the new object. Note that this is different from the concept of distributing objects using RMI or CORBA. Rather than creating an object on one host and allowing a process on a remote host to call methods on that object, the ClassLoader lets an agent read the bytecodes making up a class definition, and then create an object within its own process. In the rest of this section we'll look at how we can directly use the ClassLoader interface to distribute classes in a network environment.

2.3.1. Loading Classes from the Network

Now, in looking at the overall object model defined by the Java API, we can think of the java.lang.ClassLoader class as an abstract interface for the loading of classes into the runtime environment, and the java.io.InputStream class as the basis for loading data into the runtime environment from different sources and in different formats. An obvious next step would seem to be to put them together, and form the basis for loading classes from all of the sources accessible from subclasses of InputStream. So that's just what we've done, and the result is the StreamClassLoader shown in Example 2-9.

Example 2-9. A Network ClassLoader

package dcj.util;

import java.lang.*;
import java.net.*;
import java.io.*;
import java.util.Hashtable;

public abstract class StreamClassLoader extends ClassLoader
{
  // Instance variables and default initializations
  Hashtable classCache = new Hashtable();
  InputStream source = null;

  // Constructor
  public StreamClassLoader()
  { }

  // Parse a class name from a class locator (URL, filename, etc.)
  protected abstract String  parseClassName(String classLoc)
    throws ClassNotFoundException;

  // Initialize the input stream from a class locator
  protected abstract void    initStream(String classLoc) 
    throws IOException;

  // Read a class from the input stream
  protected abstract Class   readClass(String classLoc, String className)
    throws IOException, ClassNotFoundException;

  // Implement the ClassLoader loadClass() method.
  // First argument is now interpreted as a class locator, rather than
  // simply a class name.
  public Class loadClass(String classLoc, boolean resolve)
    throws ClassNotFoundException
    {
      String className = parseClassName(classLoc);
      Class c = (Class)classCache.get(className);

      // If class is not in cache...
      if (c == null)
        {
          // ...try initializing our stream to its location
          try { initStream(classLoc); }
          catch (IOException e)
            {
              throw new ClassNotFoundException(
                          "Failed opening stream to URL.");
            }

          // Read the class from the input stream
          try { c = readClass(classLoc, className); }
          catch (IOException e)
            {
              throw new ClassNotFoundException(
                          "Failed reading class from stream: " + e);
            }
        }

      // Add the new class to the cache for the next reference.
      // Note that we cache based on the class name, not locator.
      classCache.put(className, c);

      // Resolve the class, if requested.
      if (resolve)
        resolveClass(c);

      return c;
    }
}

The abstract StreamClassLoader class provides a generic interface for implementing and using stream-based class loaders. It accomplishes this in part by changing the semantics of the string argument to the loadClass() method on ClassLoader. Whereas ClassLoader defines this argument as the name of the class being sought, the StreamClassLoader broadens the definition to include class "locators" in general. A class locator may be a URL, a host/port/filename combination, or some other means for addressing a class located on the network, or anywhere else accessible via an input stream. Subclasses of StreamClassLoader must define the class locator format they expect, by implementing the parseClassName() method.

The other element of the StreamClassLoader framework is an implementation of loadClass() which allows subclasses to initialize and read their input streams to bring the requested class into the local environment. If the class locator string is successfully parsed by parseClassName(), then the StreamClassLoader calls initStream(), passing the class locator. This method should attempt to initialize the stream to the class specified by the locator. If successful, the StreamClassLoader next calls its readClass() method, passing the class locator and class name. This returns the newly constructed Class object, which is then optionally resolved and returned to the caller.

To demonstrate a practical extension of the StreamClassLoader, Example 2-10 shows a URLClassLoader, which loads classes that are located at URLs on HTTP servers. In this case, a class locator is expected to be in the form of a valid URL. The URLClassLoader utilizes the URL and URLConnection classes to implement the parseClassName(), initStream(), and readClass() methods, as you might expect.

Example 2-10. A URL-based ClassLoader

package dcj.util;

import java.lang.*;
import java.net.*;
import java.io.*;
import java.util.Hashtable;

public class URLClassLoader extends StreamClassLoader
{
  URL classURL = null;
  InputStream classStream = null;

  protected String parseClassName(String classLoc)
    throws ClassNotFoundException
  {
    String className = null;

    // Try constructing a URL around the class locator
    try { classURL = new URL(classLoc); }
    catch (MalformedURLException e)
      {
        throw new ClassNotFoundException("Bad URL \"" + classLoc +
                                         "\" given: " + e);
      }

    System.out.println("File = " + classURL.getFile());
    System.out.println("Host = " + classURL.getHost());

    // Get the file name from the URL
    String filename = classURL.getFile();

    // Make sure we're referencing a class file, then parse the class name
    if (! filename.endsWith(".class"))
      throw new ClassNotFoundException("Non-class URL given.");
    else
      className = filename.substring(0, filename.lastIndexOf(".class"));

    System.out.println("Classname = " + className);

    return className;
  }

  protected void initStream(String classLoc) throws IOException
  {
    // Ask the URL to open a stream to the class object
    classStream = classURL.openStream();
  }

  protected Class readClass(String classLoc, String className)
       throws IOException, ClassNotFoundException
  {
    // See how large the class file is...
    URLConnection conn = classURL.openConnection();
    int classSize = conn.getContentLength();
    System.out.println("Class file is " + classSize + " bytes.");

    // Read the class bytecodes from the stream
    DataInputStream dataIn = new DataInputStream(classStream);
    int avail = dataIn.available();
    System.out.println("Available = " + avail);
    System.out.println("URLClassLoader: Reading class from stream...");
    byte[] classData = new byte[classSize];
    dataIn.readFully(classData);

    // Parse the class definition from the bytecodes
    Class c = null;
    System.out.println("URLClassLoader: Defining class...");
    try { c = defineClass(classData, 0, classData.length); }
    catch (ClassFormatError e)
      {
        throw new ClassNotFoundException(
          "Format error found in class data.");
      }

    return c;
  }
}

The parseClassName() implementation attempts to construct a URL object from the class locator. If an exception is raised, then an invalid URL has been passed in, and a ClassNotFoundException is thrown by the method. If the URL is successfully constructed, it is queried for the file name portion of the URL. The file suffix is checked to ensure that a ".class" file is being referenced, then the base of the file name is returned as the class name. The initStream() implementation simply calls openStream() on the URL object constructed from the class locator. If an IOException results, it is allowed to propagate up the call stack to loadClass(), which assumes that the class file addressed by the URL is inaccessible, and throws a ClassNotFoundException. Finally, the readClass() method reads the class bytecodes into a buffer by calling readFully() on the InputStream from the URL. An IOException will be allowed to propagate up to loadClass(), which throws a ClassNotFound-Exception. After successfully reading the bytecodes, readClass() next calls defineClass() to parse the class definition into a Class object, which is returned to the caller. If defineClass() generates a ClassFormatError, then a ClassNotFoundException is thrown, which loadClass() allows to propagate to the caller. Although catching an error, as opposed to an exception, goes against Java design doctrine, in this particular situation it may be a useful thing to do. Notice, however, that we've chosen to "convert" the error into a ClassNotFoundException. By doing this, we're saying that a format error in the loaded class should be considered as a missing class in the next level up the call stack.

We could implement other subclasses of the StreamClassLoader that use other network protocols to import Java bytecodes into the local runtime. This, however, is left as an exercise for the reader.[4] We should note here that a Java-enabled browser uses something like our URLClassLoader to load classes for applets referenced in Web pages. A relative or absolute URL referring to a main applet class is passed to a network class loader, which does something along the lines of what happens in our readClass() method.

[4]: )



Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.