Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

JVM Languages

A Java 2 Network Class Loader


Feb01: A Java 2 Network Class Loader

Dynamically loading classes

Lorenzo is a Ph.D. student in computer science at University of Florence, Italy. He can be contacted at [email protected]. Donato is a software engineer at Intrage S.p.A., an Italian web company. He can be contacted at [email protected].


Java is useful for writing distributed applications, thanks to its independence from the underlying operating system and computer architecture, not to mention all the classes that the Java library provides — especially those in the java.net package. Moreover, Java provides a means to synchronize multiple threads, which is useful in distributed applications.

But when it comes to distributed applications, one useful feature is that Java loads classes dynamically (that is, at run time, when they are needed), so it is possible for a program to load classes that hadn't even been created when the program was first written. Typically, Java automatically loads classes dynamically, but the language also lets you write customized class loaders if you want to load classes from another source (instead of the ordinary paths in the CLASSPATH).

In this article, we present NetworkClassLoader, which lets you load classes from a (possibly remote) server. This server can be seen as an application server that provides its applications to every client that requires them. We'll implement in Java a mechanism similar to those of the Java-enabled browsers. In this scenario, a client can require the main class for an application residing on a server. Then, whenever a new class or a resource is needed by the application, it is automatically requested to the remote server, provided it is not present in the local file system of the client. Thus, when a new version of an application has to be installed, it will only have to be installed or updated in the server filesystem.

How to Write Your Own Class Loader

In his article "Java Custom Class Loaders" (DDJ, June 2000), Brian Roelofs presented specific steps for creating customized class loaders. Consequently, we will simply provide an overview of the main parts of this process: Whenever a class A is needed during the execution of your program (if it's not already loaded), the class loader that first loaded the class that needs class A, say class B, is required to load class A. If your class loader has loaded class B, then it is up to your class loader to load class A and to return a Class object for that class. Actually, the classes that can be found in the local filesystem should be loaded by the ordinary Java class loader, also called the "primordial class loader." It could be dangerous to load a class in a java.lang package from another source because those classes gain many access privileges. Moreover, loading two classes belonging to the same package from different sources could cause some run-time errors (the main trick to show that "Java is not type safe" was to load classes compiled in different moments that would cause a type error if loaded together). Additionally, loading the same class twice would produce a run-time error. So before loading a class that is not found in the local filesystem, the class loader's class cache should be checked to test if that class is already there; that is, if it has already been loaded. In Java 1.1, it was your job to take care of all these problems. Indeed, you had to inherit from java.lang .ClassLoader and implement the abstract method loadClass, which is called when a class is to be loaded. This method has to perform the following steps:

1. Check if the class has already been loaded by inspecting the class cache of the class loader.

2. Check to see whether the class can be loaded by the system class loader.

3. In case the previous tests have failed, try to load the class from a known source.

4. Define the class; that is, create a Class object from the byte code (obtained at the previous step).

5. Check whether the class has to be resolved (linked), and in that case, resolve it.

6. Return the Class object to the caller.

As you can see, step 3 is the one that you want to customize, while the other ones are standard (and, therefore, should be executed exactly that way to provide a correct and safe implementation).

In Java 2, you are fortunately free of many of these burdens. The main part of the customization is not the loading of a class, but fetching the raw data to create a Class object from somewhere (a particular location in the filesystem, for example, the Internet). In fact, the only method that has to be redefined is findClass(String className), which is called if a class is found in neither the loader cache nor in the local filesystem (that is, by the primordial class loader). This method is an implementation of the pattern Template Method: The base class implements the main algorithm, allowing you to customize certain steps without having to worry about the algorithm itself. Thus, Example 1 is a typical implementation of this method.

With this new model, you can also chain together many class loaders: The constructor of ClassLoader can also be passed another ClassLoader that will become its parent class loader. Requests to load classes are delegated to the parent. If the parent fails to load them, it will delegate them to the child by calling findClass. This could also be seen as an implementation of the Decorator pattern. findClass should always throw an exception if it cannot find the class so that another class loader can have a chance to find it.

The Architecture of the System

NetworkClassLoader is the client part of our client-server system, while the classes are stored in and provided by the class server (ClassServer).

  • The server is basically a multithreaded server that continuously listens on a certain port for incoming connections. Upon receiving a connection request, it spawns a concurrent thread, which will take care of that connection. It provides classes to the clients upon request.

  • The client instantiates a network class loader, specifying the Internet address (host and port) of the class server. Whenever a class that cannot be loaded by the system class loader is needed, it will request it to the server.

The classes of our system are contained in the package loader.

The Client

The client program has to create a NetworkClassLoader (Listing One). It typically loads the first class (which is not supposed to be in the local filesystem) through this loader's method loadClass and then it creates a new instance by using the method newInstance of the Class object returned by the loader:

ClassLoader loader = new NetworkClassLoader(hostName, port);
Class c = loader.loadClass(className);
Object main = c.newInstance();

The Internet address of the remote ClassServer is passed to the constructor of NetworkClassLoader. className is a String that specifies the name of the class to be loaded. The object created by newInstance is not assigned to a reference of class className. This cannot be done because it is assumed that the class we are trying to load from the Internet is not present in our class path. In any case, it is still impossible to assign such an object to a reference of the same type. You could force the loader to load the class from the Internet, even if it's present in the local class path. But in this case, the class of the reference would be loaded by the system class loader (that is, from the local filesystem), while the object would be instantiated by a Class loaded by NetworkClassLoader. The problem is that two classes loaded by different class loaders are incompatible (due to security reasons, these classes will belong to different namespaces).

If using an Object is too restrictive (because even after the instantiation, it is necessary to set some attributes of the object), you could use a common superclass (or an interface). For instance, if there is an interface, AppFoo, that is present in the local filesystem, and the class that we are going to load from the Internet, say RealApp, is known to implement this interface, it would then be possible to execute this code:

Class c = loader.loadClass("RealApp");
AppFoo app = (AppFoo) c.newInstance();
app.setTitle("My Application");
app.openFile("foo.txt");
...
app.run();

In fact, AppFoo (class or interface) would be loaded by the system class loader, so it would be common both to the reference and to the object created through the Class object returned by NetworkClassLoader.

We'll now examine the method findClass. In our implementation, the byte array with the contents of a .class file is downloaded from the network, and so the method findClass (see Listing One) looks something like this:

if (!connected)
  connect();
  classBytes = loadClassFromServer(className);
  classClass = defineClass(className, classBytes, 0, classBytes.length);
  return classClass;

The first time, a connection is established (method connect) with the ClassServer. To request a class's byte code to the server, we send a ResourceRequest, through serialization, with the name of the class. The bytecode (byte array) will be returned in a ResourcePacket. If an error occurs in the server, it is communicated in the response packet. Then the byte array is transformed in a Class object by calling the method defineClass, implemented in the superclass.

The Server

ClassServer is essentially a multithreaded server that listens for incoming connections on a predefined port. Every time a connection request is received, a new thread (WorkerClassServer) is spawned, and the server keeps on listening for connections. Thus, many clients can be served concurrently. In fact, every client will be served by a distinct WorkerClassServer. WorkerClassServer (available electronically; see "Resource Center," page 5) reads a ResourceRequest from the socket connected to the client, loads the contents of the .class file (method getClassBytes) for the requested class, and then it sends back a ResourcePacket with the byte array (the contents of the file). The name of the class is specified in the standard Java format (package names are separated by dots), but it is transformed in a local filesystem path by using the underlying operating-system file path separator (obtained through the property file.separator).

A cache of .class file contents is shared by all WorkerClassServer, so if a class is requested by two clients, it is loaded from the filesystem only once.

The file is searched in the local class path by calling the static method ClassLoader.getSystemResourceAsStream(className), which returns an InputStream associated with the file searched in the paths specified in the class path.

Resources

Loading classes is not the only responsibility of a class loader — it also locates resources requested by classes. These resources are typically image files, audio files, or everything a class requests through the method Class.getResource(String name), which returns a URL object. For instance, you can load a GIF image for an icon like this:

ImageIcon image = new ImageIcon(get Class().getResource("openFile.gif"));

Or you can play a .au file like this

AudioClip audioClip = Applet.newAudioClip(getClass().getResource("spacemusic.au"));
audioClip.play();

The resource name is automatically changed by prepending the package name and converting every "." into "/". Quoting from the Sun Java documentation: "The rules for searching resources associated with a given class are implemented by the defining class loader of the class." After the modification to the name of the resource, Class.getResource invokes the homonymous method of its own class loader. Instead of redefining this method, the delegation model is once again used. If the resource is not found by the parent class loader, the method findResource(String name) is invoked. This is the method that the customized class loader has to redefine in order to find the resource and return a URL for it.

In our implementation, the resource is requested to the class server (again by means of a ResourceRequest), and if obtained, it is saved on the local file system as a temporary file. Then the URL of the newly stored resource is returned so that the parent class loader is able to return the resource to the class. Even in this case a table of resources is kept so that an already stored resource is not requested again to the server. As we use the method File.createTempFile, those temporary files will be stored in the system's temporary file directory and automatically removed when the JVM terminates.

Requesting and Getting a File

The loader will request a class or a (binary) resource to the server by means of ResourceRequest, and will receive the response (and possibly the byte array) by means of ResourcePacket. Upon requesting a file, the type of the file (only CLASS or BINARY in our implementation) has to be specified. This differentiation is necessary because the server translates class file paths by replacing "." with "/" and by appending ".class," while resource paths are left unchanged (the right path is already provided by Class.getResource). If there is a problem, the class server may specify the error in ResourcePacket (for instance, the requested class or resource is not available on the server, or the request is not valid).

An Example

To test NetworkClassLoader, we've provided a TestApp in the package loaderapp. This is a simple application (using the Swing library) that uses images and sounds. To test the application on a single machine, you need to locate the loaderapp package where the class server can find it, but the client cannot. For instance, the loader classes can be put in a directory in the CLASSPATH (say, "myclasses"), while the loaderapp classes are put into a temp directory (say, "temp"). By making sure that the current directory (.) is included in the CLASSPATH, you can launch the class server from the temp directory and the client from a different directory. Images and audio files have to be placed in the same place where the .class files of loaderapp are placed. Two classes are provided to launch the class server and NetworkClassLoader, respectively: loader.RunServer and loader.RunLoader. The server can also be passed the port number for incoming connections. RunLoader is simply an application that takes the address and port number of the server and the name of the class to be loaded as parameters. It creates a NetworkClassLoader, and once the class is loaded, an instance of that class is created. Obviously, the NetworkClassLoader could be instantiated directly in your own application. (Additionally, many NetworkClass Loaders, communicating with different servers, could be instantiated by the same application.)

For example, the server could be started like this: java loader.RunServer 9999. The loaderapp.TestApp could be loaded (and thus started) by this: java loader.RunLoader localhost 9999 loaderapp.TestApp. The loader prints all the classes that are loading on the screen (including system classes), and it specifies which ones are loaded from the server; the loader does the same with resources. The class server, on the other side, prints all class and resource requests. You will notice that both classes and resources are requested only once to the server. Moreover, the loader will print class names using indentation, thus showing that loading a class A may require loading many other classes before the loading of A is completed. Figure 1 shows the application frame and two terminals: The upper one is executing the server, and the other is executing the loader. The printing of class and resource names is the only reason why we also redefine the method loadClass.

DDJ

Listing One

package loader;

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

public class NetworkClassLoader  extends ClassLoader {
    private String hostName = null;
    private int serverPort;
    private Socket socket = null;
    private ObjectInputStream is = null;
    private ObjectOutputStream os = null;
    private boolean connected = false;
    private int tab = -1; // just to print with indentation
    private Hashtable resourceTable = new Hashtable(); // key name, value File

    public NetworkClassLoader() {
        this("localhost", 5050);
    }
    public NetworkClassLoader(String hostName, int serverPort) {
        super();
        this.hostName = hostName;
        this.serverPort = serverPort;
    }
    protected Class findClass(String className)
        throws   ConnectClassServerException, JavaPackageException,
                 ClassNotFoundException, ClassFormatError {
        byte[] classBytes = null;
        Class classClass = null;
        // try with the network server
        try {
            // connect to the ClassServer
            if (!connected)
                connect();
            classBytes = loadClassFromServer(className);
        } catch (IOException ioe){
            disconnect();
            throw new ConnectClassServerException(ioe.toString());
        }
        // convert the byte array into a Class and put it in the cache.
        classClass = defineClass(className,classBytes,0,classBytes.length);
        if (classClass == null)
            throw new ClassFormatError(className);
        Print(className + " loaded from the SERVER");
        return classClass;
    }        //end loadClass()
    protected URL findResource(String name)
    {
      URL resourceURL;
      try {
          File localResourceFile = (File) resourceTable.get(name);
          // we have to download it
          if (localResourceFile == null) {
              Print("findResource: " + name + " at the SERVER");
              byte[] resourceBytes = loadResourceFromServer(name, "BINARY");
              if (resourceBytes == null) {
                  Print("Resource " + name + " not found on server!");
                  return null;
              }
              localResourceFile=createLocalResourceFile(name,resourceBytes);
              resourceTable.put(name, localResourceFile);
              Print("stored locally: " + localResourceFile);
          }
          return getLocalResourceURL(localResourceFile);
      } catch (Exception e) {
          Print("Exception " + e);
      }
      return super.findResource (name);
    }
    protected URL getLocalResourceURL(File file) throws MalformedURLException
    {
        return file.toURL();
    }
    protected File createLocalResourceFile(String name, byte[] bytes) 
        throws MalformedURLException, FileNotFoundException, IOException
    {
        File resFile = File.createTempFile
            ("__temp_res_", "_" + createLocalResourceName(name));
        resFile.deleteOnExit();

        FileOutputStream fostream = new FileOutputStream(resFile);
        fostream.write(bytes, 0, bytes.length);
        fostream.close();
        return resFile;
    }
    protected String createLocalResourceName(String name)
    {
        return name.replace('/', '_');
    }
    public synchronized Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        ++tab;
        Print("Loading class " + name);
        Class result =  super.loadClass(name, resolve);
        --tab;
        return result;
    }
    protected void Print(String s)
    {
        for (int i = 0; i < tab; ++i )
            System.out.print(" ");
        System.out.println(s);
    }
    protected void connect() throws UnknownHostException, IOException {
        System.out.println("Connecting to the ClassServer " 
                                          + hostName + ":" + serverPort);
        socket = new Socket(hostName, serverPort);
        connected = true;
        os = new ObjectOutputStream(new 
                           BufferedOutputStream(socket.getOutputStream()));
        os.flush();
        is = new ObjectInputStream(new 
                           BufferedInputStream(socket.getInputStream()));

        System.out.println("Connected");
    }        //end connect()
    protected void disconnect(){
        try {
            connected = false;
            os.close(); os = null;
            is.close(); is = null;
            socket.close(); socket = null;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }        //end disconnect()
    protected byte[] loadClassFromServer(String className)
        throws ClassNotFoundException, SocketException, IOException
    {
        byte[] classBytes = loadResourceFromServer(className, "CLASS");
        if (classBytes == null)
            throw new ClassNotFoundException(className);
        return classBytes;
    }   //end loadClassFromServer()

    protected byte[] loadResourceFromServer(String resourceName, String type)
        throws FileNotFoundException, ClassNotFoundException,
               SocketException, IOException
    {
        byte[] fileBytes = null;
        // load the file data from the connection

        // send the name of the file
        sendRequest(resourceName, type);
        
        // read the packet
        ResourcePacket resourcePacket = (ResourcePacket) is.readObject();
        
        if (! resourcePacket.isOK())
            throw new FileNotFoundException(resourcePacket.getError());
        fileBytes = resourcePacket.getResourceBytes();
        return fileBytes;
    }   //end loadResourceFromServer()
    protected void sendRequest(String name, String type)
    throws IOException
    {
        os.reset();
        os.writeObject(new ResourceRequest(name, type));
        os.flush();
    }
}  //end class NetworkClassLoader

Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.