Plug-Ins & Java

Michael uses design patterns and concepts in the development of a Java plug-in framework.


December 01, 2004
URL:http://www.drdobbs.com/jvm/plug-ins-java/184405929

December, 2004: Plug-Ins & Java

Michael is a software engineer and researcher for the Department of Defense at the Naval Research Laboratory. Michael also founded and operates as CTO of Zizworks (http://www.zizworks.com/), a web-application and custom software development company.


With any application release, users always want more features, and developers always need to patch bugs and extend functionality. One way of extending software and providing more capabilities is by implementing a plug-in framework for applications. A plug-in is a software component that is loaded by an application rather than being run independently. The plug-in extends the application by providing new functionality or resources. For example, the Netscape browser uses plug-ins to provide Apple Quicktime and Macromedia Flash support. Plug-ins let you write applications and release plug-in APIs, letting other developers provide new plug-ins without having access to your original source code or needing to recompile the entire application. Developing with plug-ins also lets you upgrade single plug-ins as bugs are fixed, security holes are patched, or new implementations become available.

Even though the Java spec does not directly deal with plug-ins, Java has simplified many of the problems associated with developing plug-in-based applications found in other programming languages. Through the runtime interpretation of bytecodes, Java-based plug-ins are easy to manage, cross platform, and, straightforward to implement. In this article, I introduce design patterns and concepts to assist in the development of a Java plug-in framework, including discovery and initialization of extension modules and dynamically extending the application classpath. The complete source code for a framework is available electronically; see "Resource Center," page 5.

Giving Plug-Ins an Interface

In order to load plug-ins, applications need to initialize and talk to them. To keep things simple, I assume that the plug-in has already been loaded by the application and that the application has determined the name of the class to use as the entry point into the plug-in. The application needs to instantiate the plug-in class and initialize the plug-in. To accomplish this, the plug-in and application need to share a common interface. The plug-in interface defines an API that is implemented by a class in the plug-in module and used by the application. In its simplest form, the interface needs to define two methods—start and stop. The application can call start when the plug-in is loaded, and stop before the plug-in is unloaded. Java supports true interfaces, so creating a plug-in API requires defining the interface, and making it publicly available to all plug-in developers. Listing One shows the basic interface definition.

You may have noticed in the example plug-in interface that the start takes a context parameter. This parameter can be any data the application wants to pass to the plug-in. For the plug-in to be useful, it may need to access internal application data at runtime. One way to allow access is to provide the plug-in with hooks into the application through the context. When designing a plug-in-based application, the most difficult decision can be determining what data should be shared and how it should be shared with the plug-in. One good strategy is to create a context class that can be extended to contain more data as you find that the requirements of the application change. Another option is to create more interfaces that plug-ins can implement besides just the basic interface, which would let the application pass exact information to the plug-in based on the interfaces the plug-in implements. I don't present multiple interfaces here, but they are simple enough to implement once you understand the concepts.

Packing the Plug-In for Production

Once you have defined and implemented the API for a plug-in, the class files needs to be packaged for distribution to users. For any plug-in that provides real functionality, it requires more than the single class that implements the plug-in interface. To bundle all of these classes into a single file, you can use a JAR. The primary reason for choosing the JAR file format for plug-in packaging is that the Java Foundation Classes (JFC) contain classes to support processing JAR files in your application.

Again, the application had already determined the name of the plug-in class. There are a couple of simple ways for the application to do this using the JAR utility classes in the JFC. The method I use here is to include the name of the class in the manifest file of the JAR. According to the Java documentation, "The manifest is a special file that can contain information about the files packaged in a JAR file. By tailoring this metainformation that the manifest contains, you enable the JAR file to be used for a variety of purposes" (http://java.sun.com/docs/books/tutorial/jar/basics/ manifest.html). For a plug-in, the manifest needs to contain the class name of the class that implements the plug-in interface that you defined previously. To do this, add a line to the manifest, such as Plugin-Class:org.ddj.plugin.HelloWorldPlugin. Listing Two presents the simple implementation of HelloWorldPlugin, while Listing Three presents the manifest file for the JAR. The final JAR can be created using the jar command: jar -cfm helloworld-plugin.jar HelloWorldPlugin.mf org/ddj/plugin/*.class. By using the manifest file, you have a well-documented file format and utility classes in the JFC to process the file.

Locating and Loading

Turning attention back to the application, you need to add support for locating and loading the plug-in JAR files. The first step—locating the JAR files—is accomplished by using the standard java.io.File class to list all of the files in a directory. I assume that the directory is known beforehand and that all the files ending in the JAR extension (.jar) are plug-ins. Using a java.io.FileFilter object and the File.listFiles(FileFilter) method, obtaining a list of all of the plug-in JAR files in a directory is trivial. The first half of Listing Four does this. This listing assumes that all of the plug-ins will be stored in a directory passed into the method. Also notice that Java is hiding all of the cross-platform issues that may arise, letting a single implementation be used on any Java-supported operating system.

The next step is to determine the name of the plug-in class to instantiate from that JAR, which you placed in the manifest file. The standard JFC class java.util.jar.JarFile lets you examine the internals of the JAR file. To get the manifest file from the JarFile, use the JarFile.getManifest() method, which returns a java.util.jar.Manifest object. The Manifest object can then be queried for attributes using the Manifest.getMainAttributes() method. After a few method calls, you can ask the java.util.jar.Attributes object for the Plugin-Class attribute, which returns the name of the plug-in class. The second half of Listing Four presents an implementation of the attribute discovery algorithm. One thing to remember is that the JarFile class automatically opens the JAR upon construction, so be sure to call JarFile.close() after you get the attribute.

Choosing the Right Classpath

For classes to be loaded at runtime, Java defines the java.lang.ClassLoader class, which is responsible for loading classes for the application. In most situations, an instance of a class loader is created by the Java Virtual Machine when the application starts executing and this bootstrap class loader includes all of the classes in the application's classpath. The bootstrap class loader presents a problem when writing a plug-in-based application since the plug-ins are not discovered until after the application is executing and the classpath has been established. Java does not provide any direct way of modifying the original application classpath; however, it does allow for new class loaders to be created and used at runtime. Creating new class loaders is the approach you are going to take to actually create the classes in the plug-in JAR.

The JFC provides the java.net.URLClassLoader class that can be used to load classes from JAR files by providing the class loader with the name of the JAR file in the form of a URL. You use the File.toURL() method to create a java.net.URL to represent the plug-in JAR file. Java class loaders use a delegation model for class discovery; therefore, you want to be sure to give the JVM's class loader to the plug-in class loader as the parent. Using the JVM's class loader as the parent ensures that the plug-in will be able to find any application classes that it may need. Using the instance of the URLClassLoader, you can now instantiate the plug-in class using the ClassLoader.loadClass(String) method. Using the plug-in class object returned from loadClass(), you can instantiate an instance by calling java.lang.Class.newInstance(). Java defines that the class loader of the new instance will be the class loader that loaded the class. Therefore, the class loader for the plug-in object that you just instantiated will be the URLClassLoader, allowing the plug-in to create any other classes from the JAR that it may need by simply instantiating the objects as it normally would. Listing Five presents an implementation of creating a class loader and instantiating the plug-in.

Java class loaders are flexible, but that flexibility introduces complexity. The example I present here only touches on the possibilities of what can be done with class loaders and plug-ins. One important note: The application is only able to instantiate classes from the plug-in JAR using the explicit URLClassLoader, which loads from that JAR. For more information on class loaders, see the J2SDK API documentation.

Plugging the Plug-Ins In

To bring everything together, you implement a PluginService that is responsible for the plug-in lifecycle, including discovery, loading, initialization, and disposal. The PluginService is composed of the methods already discussed and written to this point. The application can now use the PluginService to simplify plug-in management. It is important to keep the plug-in framework generic so that it can be used in many different applications without having to change by making generous use of interfaces. I have already presented the plug-in interface used by the framework to instantiate, initialize, and dispose of the plug-in. Another interface to consider is the application context interface. Earlier, I mentioned that the application could use a context strategy to provide the plug-in access to application data. Because the data will most likely be application dependent, it is easy for the framework to accidentally become tied to a single application. To prevent this coupling, implement an interface for the context data that the application can realize. The context interface lets only the application and plug-in implementation be coupled to the concrete application context class, while the framework remains generic. Listing Six is an implementation of the PluginService, as well as a simple application that uses the plug-in framework. Figure 1 presents an overview of the dependencies of the plug-in framework with respect to the application and plug-in implementation.

Extending the Framework

Although useful in this simple form, a plug-in framework can provide much more. With any large application, the issue of dependency management is likely to arise. It would be possible to help document and maintain dependency management using the plug-in framework. The metainformation (the information about the plug-in that you placed in the manifest file) could be extended to include dependency information such as other required plug-ins or services that this plug-in can provide to the application.

Many applications also need to design methods for integrating the plug-ins into the application. In this framework, the application simply provides some form of a context, which presumably contains the necessary hooks for the plug-in to provide its functionality. The context design, however, may not be flexible enough once plug-ins start providing services that need to be used by other plug-ins or when plug-ins want to tie into a GUI. The plug-in framework could be extended to support all of these features with some clever interfaces.

It is possible to manage plug-ins by using metainformation, and to extend the framework to support multiple versions of the same plug-in. That effort makes upgrades and new features available to users who want them while ensuring backward compatibility at the same time. The plug-in approach also makes patching a single feature of an application easy. Simply replace the plug-in JAR file on the user's installation and the patch is complete.

Conclusion

One notable application that's based on a plug-in framework is the Eclipse Integrated Development Environment project (http://www.eclipse.org/), which implements the core application as just the plug-in loading framework. All of the application functionality is shipped via plug-ins. This lets vendors such as IBM repackage Eclipse with different plug-ins as their commercial WebSphere developer product. This application versatility is only possible using a well-designed plug-in architecture.

DDJ



Listing One

package org.ddj.framework;

/* The plugin interface will be implemented by all plugins and
 * server as the common interface between a plugin and the application. */
public interface Plugin
{
  /** Start method is called by the application when the plugin is loaded. */
  public void start(PluginContext context);
  /** Stop method is called by the application when the plugin is unloaded.*/
  public void stop();
}
Back to article


Listing Two
package org.ddj.plugin;
import org.ddj.app.SimpleContext;
import org.ddj.framework.*;
/* Simple plugin that will print to standard out when
 * started and stopped by the application. */
public class HelloWorldPlugin implements Plugin
{
    /** Called when application loads this plugin. Prints to stdout. */
    public void start(PluginContext context) {
        // At this point you can cast the context to something
        // that the application has defined and use data from the application. 
        SimpleContext simpleContext = (SimpleContext) context;
        System.out.println("Hello World: plugin started.");
        System.out.println("\tThe answer is " + simpleContext.getAnswer());
    }
    /** Called when application unloads this plugin. Prints to stdout. */
    public void stop() {
        System.out.println("Goodbye World: plugin stopped.");
    }
}
Back to article


Listing Three
Manifest-Version: 1.0
Plugin-Class: org.ddj.plugin.HelloWorldPlugin
Back to article


Listing Four
// List the directory using a filter to only accept the JAR files.
return dir.listFiles(new JarFilter());
/** File filter that accepts all files ending with .JAR. This filter
 * is case insensitive.
 */
private static class JarFilter implements FileFilter {
  /** The extension that this filter will search for. */
  private static final String JAR_EXTENSION = ".JAR";
  /** Accepts any file ending in .jar. The case of the filename is ignored. */
  public boolean accept(File f)
  {
    // Perform a case insensitive check.
    return f.toString().toUpperCase().endsWith(JAR_EXTENSION);
  }
}
Back to article


Listing Five
public static Plugin createPlugin(String className, File f)
        throws RuntimeException {
  try
  {
    // Create a URL class loader for the given JAR
    URL[] urls = new URL[] { f.toURL() };
    URLClassLoader pluginClassLoader = new URLClassLoader(urls);
    // Ask the class loader to load the class
    Class pluginClass = pluginClassLoader.loadClass(className);
    // Once we have the class, we can do some checks on it to ensure
    // that it is a valid implementation of a plugin.
    int modifiers = pluginClass.getModifiers();
    if (Modifier.isAbstract(modifiers) || Modifier.isInterface(modifiers) ||
       (!Plugin.class.isAssignableFrom(pluginClass))) {
      throw new RuntimeException("The plugin class is not compatible.");
    }
    // Now ask the class to create a new instance
    Object pluginInstance = pluginClass.newInstance();
    // Since the framework required the plugin to implement the plugin
    // interface, it can be safely cast
    Plugin plugin = (Plugin)pluginInstance;
    return plugin;
    }
  catch (MalformedURLException e)
  {
    throw new RuntimeException("Error in filename " + f.toString(), e);
  }
  catch (ClassNotFoundException e)
  {
    throw new RuntimeException("Class not found " + className, e);
  }
  catch (InstantiationException e)
  {
    throw new RuntimeException("Error instantiating " + className, e);
  }
  catch (IllegalAccessException e)
  {
    throw new RuntimeException("Illegal access to " + className, e);
  }
}
public static String extractPluginClassName(File f) throws IOException {    
  JarFile jarFile = new JarFile(f);     
  try 
  {
    // Extract the entire Manifest
    Manifest manifest = jarFile.getManifest();
    // Get the attributes from the Manifest
    Attributes attribs = manifest.getMainAttributes();
    // Get the class name
    return attribs.getValue(PLUGIN_CLASS_KEY);
  }
  finally
  {
    // Be sure that we always close the JAR file
    jarFile.close();
  }
}
Back to article


Listing Six
package org.ddj.framework;

import java.io.*;
import java.lang.reflect.Modifier;
import java.net.*;
import java.util.*;

/** Provides the functionality for plugin management such as
 * discovery, initialization, and destruction. */
public class PluginService {
  /** The list of plugins this service is responsible for. */
  private List mPlugins;
  
  /** Constructs PluginService which will immediately discover and start all
   * plugins in the given directory.
   * @param workingDir the directory to search for plugins
   * @param context the context to give to all plugins */
  public PluginService(File workingDir, PluginContext context) {
     // Discover all of the jar files
     File[] jars = discoverPlugins(workingDir);
     // Instantiate each plugin
     mPlugins = new LinkedList();
     for (int i = 0; i < jars.length; i++) {
       try {
         File file = jars[i];
     String className = extractPluginClassName(file);
     Plugin plugin = createPlugin(className, file);
     // Start the plugin
     plugin.start(context);
     // Add it to the list for future reference
     mPlugins.add(plugin);
      }
      catch (Exception ex) {
        System.err.println("Error loading plugin: " + ex.getMessage());
    ex.printStackTrace();
      }
    }
  }
  /* Disposes of this service and unloads all active plugins. */
  public void dispose() {
    // Iterate the plugins and stop them
    for (Iterator iter = mPlugins.iterator(); iter.hasNext();) {
      Plugin plugin = (Plugin) iter.next();
      plugin.stop();
    }
    mPlugins.clear();
    mPlugins = null;
  }
  /** Discovers all JAR files in the given directory.
   * @param dir the directory to search
   * @return an array of all jar files in the given directory */
  public static File[] discoverPlugins(File dir) {
      // Refer to Listing 4 for implementation.
  }
  /** The key of the plugin class name defined in the manifest file. */
  private static final String PLUGIN_CLASS_KEY = "Plugin-Class";
  /** Instantiates the plugin with the given class name found in the
   * jar referenced by the given file.
   * @param className the name of the plugin class to instantiate
   * @param f the JAR file containing the given class
   * @return the plugin instance
   * @throws RuntimeException if there is an exception while trying
   * to instantiate the requested class */
  public static Plugin createPlugin(String className, File f)
    throws RuntimeException {
    // Refer to Listing 5 for implementation.   
  }
  /** Extracts the plugin class name from the manifest file of the
   * JAR file referenced by the given file.
   * @param f the file reference to a JAR
   * @return name of the plugin class as it is defined in the manifest file
   * @throws IOException if there is an error reading from the JAR file */
  public static String extractPluginClassName(File f)
    throws IOException {    
    // Refer to Listing 5 for implementation
  }
}
package org.ddj.app;
import java.io.File;
import org.ddj.framework.*;

/* Main class of the application. This application simply loads all of 
 * plugins found in a given directory and then immediately shuts them
 * down. It is not very exciting, but it gives a good example of how
 * simple a plugin loading application can be when using a good framework. */
public class Main {
  /** Constructs the main class. Upon construction all plugins will be
   * loaded, started, then stopped.
   * @param workingDir the directory containing all plugins */
  public Main(File workingDir) {
    // The context to share with the plugin. This is a very simple context
    // that does not contain much data. 
    PluginContext context = new SimpleContext(42);
    // Construct the plugin service, which will load the plugins.
    PluginService plugService = new PluginService(workingDir, context);
    // All of the plugins are now loaded and started. At this point
    // the application would do whatever it normally does.
    System.out.println("Application: now doing some processing.");
    // Assuming that the application is finished, we now shut everything down.
    plugService.dispose();
  }
   /** Main method of the application. The first argument must be
   * the path to the plugins.
   * @param args the command-line arguments */
  public static void main(String[] args) {
    // Check for the path.
    if (args.length != 1) {
      System.out.println("Usage: ");
      System.out.println("\torg.ddj.Main <plugin_directory>");
      System.exit(1);
    }
    // Start the application.
    new Main(new File(args[0]));
  }
}
Back to article

December, 2004: Plug-Ins & Java

Figure 1: Dependencies of the plug-in framework.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.