ORIGINAL DRAFT

The ability to distribute modules, in encapsulated package, that can be activated by simply dropping a module file into a specified directory is a powerful notion. This ‘plug-in’ concept can make an architecture incredibly flexible, allowing developers to add modules without requiring any changes to the main program. This also makes it possible for users to get the modules they want with relative ease and to upgrade the software with minimal effort.

This article centers on the development of a JAR file-based module architecture. To use it, you can define a common interface for modules of a given type, and then package each concrete implementation of that interface in a separate JAR file. Your application can, using this infrastructure, scan specified directories for module files and automatically make them available to your program through a factory mechanism.

To maximize flexibility, the ModuleFactory supports multiple module types. In other words, any number of instances for a given interface type may exist, but numerous interface types can also co-exist in the same application.

When you want to retrieve an instance of your module type, you specify the interface it implements along with an arbitrary key Object which you can use to select the best module for a given task. Each module instance takes responsibility for deciding whether it can handle the type of Object you pass in as a key. The factory returns the first suitable match it finds.

This mechanism is incredibly powerful. It can be used to select suitable content handlers, protocols, filters, operations, transformations, program extensions and more. Because the key is a Java Object, you can select a suitable module based on any criteria. For example, you might use a String object to recognize mime type names, file extensions or even regular expressions. You could also select a module based on an object type (using instanceof) or based on an arbitrary number of instance variable values in a compound object. The module infrastructure provides sufficient flexibility to let you use the most suitable approach in your application.

The ModuleInterface is key to making this model work. It looks like this:

public interface ModuleInterface
{
  public boolean handles(Object key);
}

Any module you implement must implement an interface that extends ModuleInterface, which means that each concrete implementation must provide a handles method, returning true or false for a given Object argument. By convention, you should return false if the key object is of the wrong type. Beyond this, you are free to use any object you want as a key.

For a given module type, you must implement the same interface. The ModuleFactory will return a suitable module for a given key or a null value when you call it’s getInstance method. The getInstance method expects a Class argument, which specifies the interface for a given module type (an interface that extends ModuleInterface) and an arbitrary key Object, which your modules can deal with however you see fit.

Figure 1 shows the classes we’ll need to make all this work as transparently as possible. In this diagram, you’ll see a TestModule, which extends ModuleInterface, and two concrete implementations that exemplify the way you might design a module. The TestModule, ModuleOne and ModuleTwo are there for illustration purposes and are not part of the Module infrastructure.

Figure 1: Dynamic Module Class Diagram.

Figure 1: Dynamic Module Class Diagram.

Let’s take a quick look at each class to get a sense of it’s purpose and functionality.

We’ve already covered the purpose of ModuleFactory. It represents the high-level API you’ll be using to fetch module implementations in your program. The ModuleRegistry class is a map that collects registered modules. The ModuleScanner scans one or more specified directories looking for JAR files that contain modules.

The ModuleSpec class captures module specifications, which includes a JAR file URL, used by the ModuleClassLoader to instantiate object instances, as well as a module name, type (interface class) and concrete class reference, which is an actual instance loaded from the JAR file at runtime.

The ModuleScanner actually opens JAR files looking for module metadata that defines the module name, type and concrete class. Let’s take a look at a concrete implementation in the form of TestModule, ModuleOne and ModuleTwo. We’ll cover how to define, package and distribute a simple example to illustrate how you can apply this code in your application.

Our TestInterface is intentionally simple and merely prints a line taken from the string argument.

public interface TestInterface
  extends ModuleInterface
{
  public void println(String line);
}

We’ll make each module print enough information to make it clear that the right module is being called in our testing.

public class ModuleOne
  implements TestInterface
{
  public boolean handles(Object key)
  {
    return key.toString().equalsIgnoreCase("one");
  }

  public void println(String line)
  {
    System.out.println("One " + line);
  }
}

ModuleTwo is virtually identical and merely replaces the key-matching string with "two" and the print line code to show "Two" as a prefix to the printed line.

Now we’re ready to define a metadata file that describes each instance of the TestModule. The Dynamic Module framework expects modules to be identified by a single metadata file stored in the META-INF directory of a JAR file, the same place you’ll find a manifest. The module metafile must be named "module.mf" and has the following format:

module.name=Module 1
module.type=TestInterface
module.class=ModuleOne

Metadata files are loaded as Java Properties and may include comments. The "module.type" and "module.class" properties must correctly reflect the type, which is an interface subclassing ModuleInterface, and class, which is the concrete implementation of that interface.

The interface (module.type) is expected to be part of the application (the calling classes), while the implementation (module.class) is expected to be in the JAR file that contains the metadata description ("META-INF/module.mf"). If these classes are part of a package hierarchy, the package specification should be part of the class names you provide.

To make the metadata easy to work with, the framework uses a ModuleSpec class, which is constructed with a File reference to the containing JAR file itself and the Properties object (module.mf) retrieved from that JAR file. ModuleSpec assigns key properties to internal values, loading classes in the process. It’s important to understand that an instance of both the type and module class are loaded when the ModuleSpec is created. If the module.type and module.class are incorrectly defined, this is where you’ll see the problem surface.

The ModuleClassLoader is responsible for loading classes from the JAR file and extends the standard URLClassLoader. It provides a simplified constructor that does not require a URL list and overrides the private addURL method to make it public. This allows us to instantiate a single ModuleClassLoader and to add URL references for each JAR file as needed. When a new ModuleSpec instance is created, the ModuleClassLoader can load an instance of the class that implements the ModuleInterface extension, making it available to the factory through the getInstance method.

ModuleFactory uses the ModuleScanner class to actually look for module JAR files. Listing 1 shows the ModuleScanner code, which provides a pair of scan methods that take either a single File argument or an array of File objects. These are directories that will be searched for module JAR files. A JAR file is considered to be a module if it contains a properly formatted ‘module.mf’ file in it’s META-INF directory

ModuleScanner exposes a getModuleSpecs method that returns the ModuleSpec objects associated with each module found in the listed scan directories. Notice that that the scanning involves a readModuleManifest method, which is responsible for getting the "module.mf" file content. The readModuleManifest in turn relies on a method called findModuleEntry that finds the actual Jar file entry.

Under Windows there is potential for problems if the case of the file or meta-inf directory is not exact. The findModuleEntry compensates for this by doing a case insensitive search through the entries. I spent more time than I should have debugging a case sensitive version, finding that a DOS box, Windows Explorer and an Open file dialog box tended to represent the case of the file and directory differently. It seemed like a good idea to make sure this wasn’t a barrier for others to developing successful modules.

ModuleFactory is a keystone in this infrastructure. You can see the code in Listing 2. Notice that we delegate some responsibilities to ModuleRegistry and ModuleScanner, so the code responsibilities are well separated. The two loadModules methods should be called somewhere in your program’s initialization process with either a single directory in which modules can be found or a set of directories. Both these calls delegate the actual collection of JAR file to the ModuleScanner.

ModuleScanner will collect each ModuleSpec, which we’ll register in the ModuleRegistry (effectively a Map of List objects associated with an interface type). When you call getInstance, the ModuleRegistry will return a list of module instances that expose the requested interface. The interface class you use to ask for modules should be the most specific interface that extends ModuleInterface.

The getInstance method walks the list of recognized modules, retrieved from the ModuleRegistry, and calls the handler method on each module looking for a match. The key object passed in as the second getInstance argument. The modules are tested in the order they are read from the file system, so if you need to control the order in which they are loaded, you may need to put module JAR files in different directories and use the loadModules method with an array of File paths arranged in the order you want to control.

The ModuleFactory getInstance method returns the first matching module. The module will always implement the ModuleInterface but is also expected to implement an interface which extends ModuleInterface. You can cast the returned class to the more specific interface and then use it as you see fit. Because you are retrieving module implementations by type, you can support multiple module types using the same infrastructure.

There is a second getInstance method with no arguments that you should use to access the ModuleFactory itself. It follows the Singleton pattern and makes only a single static instance available throughout the system. You’ll see two other methods in the code. The register methods allow you to add one or more ModuleSpec objects to the registry. You normally won’t have to use those since the loadModules methods handle registering modules on your behalf.

The benefits of using a mechanism like this are many and varied. By encapsulating modules, you get the benefits of well-divided, loosely-coupled programming elements. By using separate JAR files, you get a distribution advantage that allows you to bundle different modules in customized product configurations. The architecture is open, so you can publish your interfaces and have other authors develop modules that work with your programs.

Listing 1

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.zip.*;
import java.util.jar.*;

public class ModuleScanner
{
  protected List moduleSpecList = new ArrayList();

  public void scan(File[] paths)
    throws IOException
  {
    for (int i = 0; i < paths.length; i++)
    {
      scan(paths[i]);
    }
  }
  
  public void scan(File path)
    throws IOException
  {
    File[] list = path.listFiles();
    for (int i = 0; i < list.length; i++)
    {
      File file = list[i];
      if (file.isFile() && file.getName().toLowerCase().endsWith(".jar"))
      {
        System.out.println(file.getName());
        Properties props = readModuleManifest(file);
        if (props == null) continue;
        ModuleSpec spec = new ModuleSpec(file.toURL(), props);
        moduleSpecList.add(spec);
        System.out.println(spec);
      }
    }
  }
  
  protected Properties readModuleManifest(File file)
    throws IOException
  {
    JarFile jar = new JarFile(file);
    JarEntry entry = findModuleEntry(jar, "meta-inf/module.mf");
    if (entry == null) return null;
    
    InputStream input = jar.getInputStream(entry);
    Properties props = new Properties();
    props.load(input);
    input.close();
    
    return props;
  }
  
  // Find the actual entry (avoid case issues and confirm existence)
  protected JarEntry findModuleEntry(JarFile jar, String expect)
  {
    Enumeration enum = jar.entries();
    while (enum.hasMoreElements())
    {
      JarEntry entry = (JarEntry)enum.nextElement();
      String name = entry.getName();
      if (name.equalsIgnoreCase(expect))
      {
        return entry;
      }
    }
    return null;
  }
  
  public ModuleSpec[] getModuleSpecs()
  {
    int size = moduleSpecList.size();
    ModuleSpec[] specs = new ModuleSpec[size];
    for (int i = 0; i < size; i++)
    {
      specs[i] = (ModuleSpec)moduleSpecList.get(i);
    }
    return specs;
  }
}

Listing 2

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

public class ModuleFactory
{
  protected static final ModuleFactory
    factory = new ModuleFactory();

  protected ModuleRegistry registry = new ModuleRegistry();
  protected ModuleScanner scanner = new ModuleScanner();
  
  public void loadModules(File path)
    throws IOException
  {
    scanner.scan(path);
    register(scanner.getModuleSpecs());
  }
  
  public void loadModules(File[] paths)
    throws IOException
  {
    scanner.scan(paths);
    register(scanner.getModuleSpecs());
  }
  
  public void register(ModuleSpec spec)
  {
    registry.registerModule(spec);
  }
  
  public void register(ModuleSpec[] specs)
  {
    for (int i = 0; i < specs.length; i++)
    {
      registry.registerModule(specs[i]);
    }
  }

  public Object getInstance(Class type, Object key)
  {
    List modules = registry.getModulesForType(type);
    int count = modules.size();
    for (int i = 0; i < count; i++)
    {
      ModuleSpec spec = (ModuleSpec)modules.get(i);
      ModuleInterface module = spec.getInstance();
      if (module.handles(key)) return module;
    }
    return null;
  }
  
  public static ModuleFactory getInstance()
  {
    return factory;
  }
}