Object mobility in the Jini environment

Reduce the complexity of installation and configuration of complex Jini applications with mobile code

Sir Thomas More, the English philosopher, published a book in 1516 about an imaginary island. Inhabitants of this distant land lived the most ideal life imaginable: the citizens were educated, work was not overbearing and could still provide for a good living, society and government were just, and equality and peace ruled. He named this island Utopia.

If Sir Thomas were to write in the present day, he surely would consider an ideal computing landscape for his island as well. We can also surmise that he would seldom mention software installation, version upgrades, or system configuration. Fortunately, we do not have to imagine a utopian world without these chores, because we can take advantage of technologies like Java, Jini, and code mobility to create a more ideal computing environment here and now.

Alan Cooper, inventor of Visual Basic, mentioned in an online ITworld.com interview (see Resources) that the complexity of software installation and the capabilities of software are typically inversely proportional: the more capable a software package, the more difficult its installation. Anyone who has ever set up an operating system or database-management system would instantly agree with Cooper.

An important Jini promise is to disrupt this trend. As Jini architect Jim Waldo put it: "I dream of a world in which my children will never have to use InstallShield." In a recent JavaWorldinterview (see Resources), Waldo referred to Java's ability to download code and objects on an as-needed basis from anywhere on the network.

Jini turns this capability into a practical benefit, providing a simple mechanism to locate objects on the network based on their functionality and download them to a client. Once a client obtains a reference to an object, it can use that reference to benefit from the object's functionality -- by calling methods on it, for instance. Anything that can be represented by a Java object (which includes practically any device or legacy code as well as Java programs) on the network can participate in a federation of such objects -- or services, as they are called in the Jini world.

To effectively use mobile objects, you must gain an understanding of how objects are located on the network, how they are loaded into a running application and participate in the application's execution, and the techniques used to make the object's class files available for downloading.

A class loader refresher

The Java Virtual Machine's (JVM) job is to execute Java bytecode. Bytecode is stored in Java classfiles, which are loaded into the JVM via a class loader. Loading a class means locating a classfile that contains the desired type, based on the type's name, and then creating the class from that file (see the Java Virtual Machine Specification, Chapter 5). Once a class is loaded into a VM, it is linked into the VM's execution state, which means that it becomes part of the program's execution. Finally, the VM initializes the class by calling a special initialization method, which essentially corresponds to static initialization of the class.

When a JVM starts up, it loads a class using a bootstrap class loader. This first class must have a public static void main(String[] argv) method. The VM calls that method after initializing the class, and then starts executing code specified in the main() method.

Code in the main method will reference classes not yet loaded into the VM. When the VM encounters such a reference, it asks its class loader to load, link, and initialize this class as well. The class loader uses a codebase to try to locate the requested classfile. The codebase is the location where the class loader searches for classfiles.

If there were only a bootstrap class loader, this process would not be very powerful, since all classes would have to be installed at a predetermined location. Fortunately, the JVM's class loader architecture is modular. In addition to its built-in class loader, a VM allows any number of user-defined class loaders.

Since the second version of the JVM specification (available since Java 1.2), class loaders have formed a hierarchical relationship. Every class loader has a parent class loader. The bootstrap class loader is the the root node of the tree; the VM supplies it. When a class references a type not yet loaded into the VM, it first asks its class loader's parent to load it. This parent class loader, in turn, will delegate the class loading to its parent, and so forth, until the delegation reaches the ultimate parent, typically the bootstrap class loader. If the bootstrap class loader can find the class, it will load and create it, and pass a reference to that class back to its child class loader, which in turn will pass it back to its child, and so on, all the way to the class loader that requested the class. If the bootstrap class loader cannot locate the class, it will ask its child to locate it. If that child cannot load it, it will, in turn, ask its child. Only when none of its parents are able to return a reference to a loaded class will the original class loader try to load the class. If, at that point, the class is still not found, a ClassNotFoundExceptionis thrown. This is the parent-delegation principle of class loading, which was first introduced in the 1.2 version of the JVM. (See Java Virtual Machine Specification, Section 5.3.2.)

In Java 2, the bootstrap class loader considers a CLASSPATHenvironment variable to determine its codebase. By default, the Java Core class library, supplied by the Java Runtime Environment (JRE), is also a part of that codebase. A different class loader, the Extension class loader, loads classes that are part of the Java Standard Extensions. Every other class is loaded by some other, user-defined, class loader.

Figure 1. The JVM's hierarchical class loader architecture

User-defined class loaders are specified in the Java programming language, and are created with code such as the following:

ClassLoader myClassLoader = new MyClassLoader(parentClassLoader);

Here parentClassLoader is a reference to a parent class loader instance. If a null is passed into the constructor, the bootstrap class loader will be the parent.

Here's an important method that the ClassLoader class specifies:

Class findClass(String className) {...}

Overriding this method can customize the way the specified class is located by the VM. If the class cannot be found, the VM throws a ClassNotFoundException. This method is typically called automatically by a ClassLoader's method:

Class loadClass(String className)

loadClass()does the following:

  1. It calls findLoadedClass(className), which tells it if the specified class has already been loaded
  2. It calls loadClass(className) on the parent class loader, and, ultimately, on the bootstrap class loader
  3. It calls findClass(className)

It's important to remember that, based on the parent-delegation principle, the search for the class is ultimately delegated to the bootstrap class loader first, and then proceeds down the succession of child class loaders. If any class loader in this chain finds the classfile in its codebase, the VM will load, link, and initialize that class, and will not search any further.

The benefits of object serialization

The JVM is able not only to load classes, but also to load objects (instances of classes) from the network via the Java object serializationmechanism (see the Java Object Serialization Specification for more details). Object serialization stores an object in a stream (i.e., in serialized form) in such a way that a Java program can reconstruct the object's state at a later time. You can save the binary stream representing the serialized object on stable storage, such as in a file or in a database management system, or send it over a wire.

For an object to be serialized, it has to implement either the java.io.Serializableor java.io.Externalizableinterfaces, the latter being a subtype of Serializable. Serializable is a marker interface: it specifies no methods and merely indicates an object that has serializable state. All subclasses of a Serializableclass are also serializable.

If instances of a class need a special way to serialize their state, that class can implement the Externalizable interface, which mandates two methods:

void readExternal(ObjectInput inputStream);
  void writeExternal(ObjectOutput outputStream);

The main difference between Externalizable and Serializableis that the latter serializes, by default, the entire object graph, including states of an object's superclass. Externalizable gives you complete control over the serialization process.

If a supertype of a Serializable object is not itself serializable, then the object can assume the responsibility for saving and reconstructing the supertype's state (public, protected, and package fields if they share a package). For this to work, the superclass must have a public no-arg constructor. Further, if an object at runtime refers to a nonserializable object, the serialization system will throw a NotSerializableException, since it cannot write the complete object graph to the serialized stream.

Not all objects are suitable for serialization. Threads, for instance, do not have state that can later be recreated. (Actually, it is possible, but very difficult, to do so.) There can also be objects that should not be serialized for semantic reasons. Even if an object is serializable, you may not need to write all object fields to the stream during serialization. For instance, a variable that loses its meaning in a different execution context (such as something indicating the current time) should be marked transient, and will instead be initialized to its default value when reading it from the stream.

If an object holds multiple references to another object, this second object is serialized only once, and subsequent references to it will include a handle as a reference instead.

Along with instance data, the object serialization system writes a special object to the stream to represent the serializable object's class. This object is of the type java.io.ObjectStreamClass, and is essentially a descriptor for the Class object associated with the serializable object. It contains the class's name, its unique version number (serialVersionUID), and the class fields. In addition, it also has methods to obtain the actual class represented by this object, if, and only if, this class is already present in the local VM:

Class forClass();

If there is no class identified in the local VM that corresponds to this ObjectStreamClass, then a null value is returned.

Figure 2. An object graph written to a serialized stream

Codebase annotation

At this point, you have two sides of a story. On the one hand, the JVM's class loaders can find and load a class from their codebases, if they are given that class's name. On the other hand, you've seen that objects can be written to a stream and transported across VMs, and that object streams contain instance data to recreate the object as well as a descriptor of the object's class.

A complete story would need to connect these two sides. Given a class's name from the descriptor found in the serialized stream, the VM still needs to know where it should load the actual class from. Once it has that information, the VM can create a class loader with the codebase pointing to that location, load the class, link it to the current execution environment, and initialize its class data (i.e., set its static fields). Finally, the VM could use the data contained in the serialized object stream to create an instance of the class and initialize the object's instance data.

One solution would be to save the entire Class object corresponding to the serializable object's class instead of saving just a descriptor. However, that solution has two major downsides.

First, it would add significant overhead to each serial object stream. Because serialization means writing to the object stream, possibly transporting the object stream through a network, and then reading the stream, all these would take longer and would require more resources to perform with this overhead.

The second downside has to do with class evolution. Serializing an object means that it can survive the VM instance that created it, which implies that it is stored persistently across VM instances for longer periods of time. In this way, serialized objects are a persistent storage mechanism.

In practice, Java classes evolve over time: new fields might be added, methods may be implemented anew, bugs are discovered and fixed, and so on. With each of these changes, the class is altered. It would be unfortunate if you could not use information stored in serialized versions of an object with newer class versions. Good programming practice dictates that classes evolve in such a way that they preserve compatibility with older versions (i.e., that they be backwards compatible). Decoupling serialized objects from their corresponding Class objects allows for backwards-compatible class evolution, since newer class versions can still load serialized instance data from objects defined originally with an older version.

1 2 3 Page 1
Page 1 of 3