Design with dynamic extension

How dynamic extension works in Java and how to use it in your designs

1 2 3 Page 2
Page 2 of 3

In this version of class Cat, the first argument to the application is passed to forName(). (Note that this is an example of a type name being passed around as a String, a telltale sign of an application that uses dynamic extension.) Assume that the application is invoked with the argument "Mouse". This String will be passed to forName(), which will attempt to load the type.

Figure 4 shows what the name spaces look like after scurry() is invoked on the Mouse object. Once again, only one name space, that of the primordial loader, exists. This looks the same as that of the previous application that didn't use dynamic extension.

This is because forName() follows the same rule the JVM follows when it resolves a symbolic reference in the constant pool of a class file. forName() will use the same class loader to load the requested class that loaded the class that contained the code that invoked forName(). Because Cat, the class that invoked forName(), was loaded by the primordial loader, forName() will use the primordial loader to load Mouse.

Figure 4. forName() follows the rule

Class java.lang.Class

To understand dynamic extension, you need to know a bit about class java.lang.Class. For every type that a JVM loads, it creates an instance of class java.lang.Class to "represent" the type to the rest of the application. Given a reference to a type's Class instance, you can access the information that the JVM extracted from the class file that defined the type.

For example, you can find out whether the type is a class or an interface. If it is a class, you can find out if it is abstract and get a reference to the Class instance for its superclass. You can get an array of Class instances for it directly implements, if any. If the SecurityManager allows it, you can even find out what fields and methods the class contains and invoke the methods.

The most important aspect to dynamic extension is that if you have a reference to a Class instance for a non-abstract class, you can instantiate an instance of the class by invoking newInstance() on the Class object. If forName() is successful at loading the type (or if the type was already loaded into the current name space), forName() will return a reference to the Class instance that represents the type. By invoking newInstance() on the Class object returned by forName(), the Cat application creates an instance of class Mouse.

Interacting with a dynamically loaded class

When Cat invokes scurry() on the Rodent reference myToy, the JVM does a dynamic bind and executes Mouse's implementation of scurry(). The application prints out "Mouse scurrying.". This is true even though Cat didn't know about Mouse at compile time. At compile time, Cat knew only about Rodent. This is the most common way to interact with a dynamically loaded class: to invoke methods defined in a supertype (superclass or superinterface) of the dynamically loaded class on an instance of that class.

Another way to interact with a dynamically loaded type is to use reflection to find interesting methods in the object, and to invoke these methods. This approach is taken by bean builder tools.

When you drop a bean into a bean builder, the bean builder dynamically extends itself with the classes of the bean. As the bean builder has no idea what kind of bean is coming, it uses reflection to find methods that adhere to JavaBeans style rules, and uses those methods to manipulate the bean.

Jumping out of the current name space

So, how does an application ever get out of the primordial name space? It does this by creating a class loader object and asking that class loader object to load the type.

To ask a class loader to load a class, simply invoke loadClass() on the class loader object, thereby passing in the fully qualified name of the type to load. The loadClass() method basically performs the same task as forName().

But loadClass() is rebellious -- it doesn't follow the Resolution Rule. Instead of using the same class loader to load the requested type as it loaded the class containing the code that invoked loadClass(), it uses itself to load the class. (After all, it is a class loader.) Thus, it loads the requested type into its own name space.

Here's an example of a Cat application that uses a class loader object to load a Rodent:

// In file dynaext/ex3/Cat.java
public class Cat {
    public static void main(String[] args)
        throws ClassNotFoundException, IllegalAccessException,
               InstantiationException {
        RodentClassLoader rcl = new RodentClassLoader();
        Class c = rcl.loadClass(args[0]);
        Rodent myToy = (Rodent) c.newInstance();
        myToy.scurry();
    }
}
// In file dynaext/ex2/Rodent.java
public class Rodent {
    public void scurry() {
        System.out.println("Rodent scurrying");
    }
}
// In file dynaext/ex2/Mouse.java
public class Mouse extends Rodent {
    public void scurry() {
        System.out.println("Mouse scurrying");
    }
}

This

Cat

application first instantiates

RodentClassLoader

(shown below). The resulting object is a class loader object because class

RodentClassLoader

extends class

java.lang.ClassLoader

.

Cat

invokes

loadClass()

on this class loader object, passing it the first argument to the application. Assuming the first argument is

"Mouse"

,

loadClass()

will attempt to load

Mouse

into its name space. If it is successful,

loadClass()

will return a reference to the

Class

instance that represents the newly loaded type. After that, this

Cat

application looks the same as the previous

Cat

that used

forName()

. But the name spaces look different. Figure 5 shows what the name spaces of this

Cat

application look like after the execution of the

scurry()

method. The application has two name spaces because it has two class loaders -- the primordial class loader and the class loader object it instantiated,

RodentClassLoader

. Although every other class was loaded by the primordial loader and is, therefore, in its name space, class

Mouse

was loaded by the class loader object, so it sits in the class loader object's name space.

Figure 5. Two name spaces

Writing a class loader

Now, the reason you would usually write a class loader in the first place is to customize the loading process. The purpose of any class loader is (given a fully qualified name) to produce a binary stream of data (usually in the Java class file format) that represents that type and to import that type into the JVM. So the designers of each JVM implementation get to decide how their primordial class loader is going to produce the binary stream of data given the fully qualified name. The JVM that I received in the JDK for Windows 95 looks down the directories and zip files listed in an environment variable called the

CLASSPATH

. The primordial loaders of other JVMs can do it differently. Often, the reason you write a class loader is because you want to load classes in some other way than the default way provided by the primordial loader. You may, for example, want to download class files across a network. This is one reason why the class files for applets are loaded through class loader objects. You may want to decipher encrypted class files as they are read in. You may want to extract binary class definitions out of some proprietary database. You may want to load class files that end with the extension

.purple

instead of

.class

Or, you may even want to generate class definitions on the fly, so that your Java application can continually make itself smarter. Because you define class loaders in Java code, you are free to do anything to produce a binary stream of data in the Java class file format given a fully qualified name. (Well, at least you are free to write and compile the class loader. But in some runtime environments, the

SecurityManager

may prevent class loaders from being instantiated. Untrusted applets, for example, can't create class loader objects.) Class

RodentClassLoader

, shown below, loads classes in a special subdirectory of the current directory named

hole

. This class loader is a simple example of another potential reason for writing a class loader: to load class files not located in the class path.

// In file RodentClassLoader.java
import java.io.*;
import java.util.Hashtable;
public class RodentClassLoader extends ClassLoader {
    public synchronized Class loadClass(String typeName,
        boolean resolveIt) throws ClassNotFoundException {
        // See if type as already been loaded by
        // this class loader
        Class result = findLoadedClass(typeName);
        if (result != null) {
            // Return an already-loaded class
            return result;
        }
        // Check with the primordial class loader
        try {
            result = super.findSystemClass(typeName);
            // Return a system class
            return result;
        }
        catch (ClassNotFoundException e) {
        }
        // Don't attempt to load a system file except
        // through the primordial class loader
        if (typeName.startsWith("java.")) {
            throw new ClassNotFoundException();
        }
        // Try to load it from subdirectory hole.
        byte typeData[] = getTypeFromHole(typeName);
        if (typeData == null) {
            throw new ClassNotFoundException();
        }
        // Parse it
        result = defineClass(typeName, typeData, 0,
            typeData.length);
        if (result == null) {
            throw new ClassFormatError();
        }
        if (resolveIt) {
            resolveClass(result);
        }
        // Return class from hole
        return result;
    }
    private byte[] getTypeFromHole(String typeName) {
        FileInputStream fis;
        String fileName = "hole" + File.separatorChar +
            typeName.replace('.', File.separatorChar)
            + ".class";
        try {
            fis = new FileInputStream(fileName);
        }
        catch (FileNotFoundException e) {
            return null;
        }
        BufferedInputStream bis =
            new BufferedInputStream(fis);
        ByteArrayOutputStream out =
            new ByteArrayOutputStream();
        try {
            int c = bis.read();
            while (c != -1) {
                out.write(c);
                c = bis.read();
            }
        }
        catch (IOException e) {
            return null;
        }
        return out.toByteArray();
    }
}

I'm not going to delve into the nitty gritty details of how to write a class loader here. For that you can refer to Chapter 7 of

Inside the Java Virtual Machine,

which, as I mentioned above, is excerpted on my Web site. (See

Resources

for a link to the chapter.) Be sure to read the section entitled "Example: The Dynamic Extension of the

Greet

Application."

Design guidelines

Now that you've seen how Java's linking model supports dynamic extension, you may be wondering how you should put this cool architecture to use in your designs. The main design insight that I have for dynamic extension (which, while not that profound, is quite useful) is that dynamic extension facilitates customization. You can think of the chunks of Java code that your application loads dynamically as "plug-ins" or "personality modules." Perhaps you would like users to be able to plug their own algorithms into your product, to customize it to their needs and preferences. You can also design your product such that users can define a class that encapsulates the custom algorithm and then load it into your product dynamically. (This is the strategy pattern from the "Gang of Four" book, by the way, with dynamic extension added.) Or you can customize your own product for individual customers in the same way. Applets, for example, are a dynamic extension of a Java application fired off by a Web browser. They enable applet programmers to customize the browser. Servlets are dynamic extensions of a Web server that enable servlet programmers to customize the Web server. Resources, which are dynamically loaded based on the current locale, are customizations of internationalized Java programs that enable translators to localize the Java programs.

Interacting with dynamically loaded types

In general, you should interact with dynamically loaded types by instantiating them and invoking methods declared in a superclass or superinterface about which you know. This, for example, is how browsers interact with applets. Every applet you write must descend from a superclass that the browser knows about, java.applet.Applet. The browser dynamically loads and instantiates your applet and casts the resulting reference to type java.applet.Applet. It then invokes init(), start(), and stop() on your applet, methods which are declared in java.applet.Applet. The other way to interact with dynamically loaded types (by discovering interesting methods via reflection) is reasonable only if you have absolutely no knowledge of what's coming. If you are writing a bean builder, you'll need to do this. Otherwise, you'll usually write your program such that it expects, as the dynamic extension, some subclass of a known class or some class that implements a known interface. forName() versus class loader objects

Once you have decided to use dynamic extension, how do you choose between

forName()

and class loader objects? In general, you should try to use

forName()

if possible, because

forName()

is simpler to use and understand. You should use class loader objects if:

  1. you need name spaces;
  2. you need to customize the load process; or
  3. you need to dynamically replace existing customizations in a live Java application.

Using name spaces

Related:
1 2 3 Page 2
Page 2 of 3