Department of Engineering

IT Services

Dynamic Class Loading in Java

This article aims to explain how to dynamically load classes at runtime with Java. In doing so it will illustrate the use of package, interface, Exceptions, abstract classes, etc, with Vectors and Hashtables thrown in. Some knowledge of Java is assumed. Most of the code originates from http://www.pramodx.20m.com/dynamic_class_loading_in_java.htm for which much thanks.

Introduction

Plug-in architectures offer flexibility for both the programmer and the user - extra functionality can be added to a program without the program having to be changed. Java has mechanisms to support this approach. This example is based on a situation where a text file will be read in and analysed in various ways, some of which may not be foreseeable by the programmer. When the program is run, it will load in the available plug-ins. The plug-ins need to conform to rules laid down by the main program but within those limits there's still some flexibility.

The type of arguments these detectors need may depend on what the detectors are looking for. In the example provided the plug-in is looking for a certain poetry form where the syllable-per-line information is needed. The main program can ask plug-ins what type of arguments are required.

Concepts

Before we look at the code, some terms and concepts are worth previewing

  • package - Rather like a library in other languages, or a namespace. All the methods here are put into a TextPatterns package. This helps avoid potential name-clashes, especially in big programs. To use a package, you need to import it.
  • interface - Unlike C++, Java doesn't support multiple inheritance - objects can be derived from only one parent. However, Java has interfaces so that otherwise unrelated objects can show that they support certain method calls. An interface declares a list of publically available methods and constants, but doesn't state where (in what class) the implementation of the methods are. In this example, an interface is defined in Detector.java
    package TextPatterns;
    import java.util.*; 
    public interface Detector
    {
      void description();
      String argtype_required();
      void processing(Vector v);
    }
    

    interfaces are created and used rather in the same way as classes are except that whereas a class can only extend one class it can implement several interfaces. Note that implementing an interface has some similarities with extending a class with abstract methods - in both situations the new class has to provide an implementation. However, extending a class implies inheriting its data structures and non-abstract methods too.

    The plug-ins here all have to implement this interface. For example, they all have a description method which should display a description of the plug-in.

  • exception - When something goes wrong, a java program throws a bundle of information called an exception that another piece of code should catch. There are many types of exceptions to cope informatively with different situations. Here we'll invent our own exception type. DetectorNotCreatedException.java contains
    package TextPatterns;
    
    public class DetectorNotCreatedException extends Exception
    {
       DetectorNotCreatedException()
       {
          super("Detector could not be created!");
       }
    }
    

  • abstract class - Objects of an abstract class can't be created. Abstract classes are designed to be inherited.
  • factory - a design pattern describing a situation where a piece of code's purpose is to create objects. It's more flexible than directly calling constructors. A factory returns another class depending on the context. To create the instance of an object, the task is delegated to something which has better access to the object. See Pattern: Abstract Factory
  • Loading classes - In java.lang.Class the static Class Class.forName(String name) method returns the class object associated with the class name. If the class has not been loaded, most implementations load the class as well. It throws the ClassNotFoundException if the class could not be found for some reason. This is what we'll use for dynamic loading. When a class is loaded in this way, any static method that doesn't have a name is run. We'll make use of this idea to add information about the loaded class to a table.

The Code

The code is divided into a number of files.

  • Detector.java - the interface definition shown already
  • DetectorNotCreatedException.java - the exception definition shown already
  • DetectorCreator.java - an abstract class. It has a table to store information about loaded plugins, and a static method createDetector that dynamically uses the Class.forName method to load a class in if it's not already loaded. It then uses the factory to create an object. In the case of any failure, a DetectorNotCreatedException is thrown.
    package TextPatterns;
    
    import java.util.*;
    
    public abstract class DetectorCreator
    {
       // This is the list of loaded plug-ins
       public static Hashtable detectorFactories = new Hashtable();
    
       protected static Detector createDetector(String name, String type)
          throws DetectorNotCreatedException
       {
          DetectorFactory s = (DetectorFactory) detectorFactories.get(name);
          if(s == null) // detector not found
          {
             try
             {
                Class.forName("TextPatterns.Detectors."+name);
                // Loading the class should add it to the detectorFactories
                // table.
    
                s = (DetectorFactory) detectorFactories.get(name);
                if (s == null) 
                {
                   throw (new DetectorNotCreatedException());
                }
             }
             catch(ClassNotFoundException e)
             {
                // We'll throw an exception to indicate that
                // the detector could not be created
                throw(new DetectorNotCreatedException());
             }
          }
          return(s.create(type));
       }
    }
    

  • DetectorFactory.java - the factory - another abstract class.
    package TextPatterns;
    
    // a detector is expected to provide sub-class of this  factory which 
    // will create the required detector.
    // The static block without a function name in the the detector class 
    // should create a new instance of its factory and add it to the
    // hash-table
    public abstract class DetectorFactory
    {
    	public abstract Detector create(String type);
    }
    

  • PatternDetector - the top level. It loads in the available plug-ins, lists their features then calls them. It extends DetectorCreator. If an attempt is made to load in something that isn't a Detector, ClassCastException is thrown. ClassCastException is a subclass of RuntimeException and Sun doesn't recommend catching these - but this is only a demo ...
    package TextPatterns;
    // To run this, set the CLASSPATH to the parent directory,
    // go to that directory, and type
    //    java TextPatterns.PatternDetector
     import java.io.*;  
     import java.util.*; 
    public class PatternDetector extends DetectorCreator
    {
       public static void main(String args[])
       {
        // Look for *.class files in a particular directory
        // and load them in using createDetector
        File dir = new File("TextPatterns/Detectors");
        if (!dir.exists()) {
            // File or directory does not exist
            System.out.println("No plugin directory");
            return;
        }
    
        String[] children = dir.list();
        if (children == null) {
            System.out.println("No plugin directory - it's a file");
            return;
        } 
        
        FilenameFilter filter = new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.endsWith(".class");
            }
        };
        children = dir.list(filter);
         // Let's specify 5 types of plug-in
         Hashtable plugin_families = new Hashtable();
         plugin_families.put("syllabic", "Vector of ints (syllables/line)");
         plugin_families.put("textual","Vector of strings (the lines of the poem)");
         plugin_families.put("stress", "Vector of strings (stress patterns)");
         plugin_families.put("phoneme", "Vector of strings (phonemes)");
         plugin_families.put("rhyme", "Vector of ints (rhyming lines)");
    
        for (int i=0; i<children.length; i++) {
            // Get filename of file or directory
            String filename = children[i].substring(0, children[i].length()-6);
            if (filename.indexOf('$')==-1) { 
                try
                {
                  Detector s = createDetector(filename,"syllabic");
                  System.out.println(filename + " plug-in loaded");
                  System.out.print("   Description: ");
                  s.description();
                            
                  System.out.print("   Processing: ");
                  String plugin_type=s.argtype_required();
                  String argtype_description = (String) plugin_families.get(plugin_type);
                  if (argtype_description == null)
                     System.out.println(plugin_type + "isn't a recognised type");
                  else 
                    if (plugin_type.equals("syllabic")){
                        // In the real program we'd process a text file to
                        // provide the arguments. Here we'll fabricate
                        Vector vec = new Vector();
                        vec.addElement(new Integer(5));
                        vec.addElement(new Integer(7));
                        vec.addElement(new Integer(5));
                        s.processing(vec);
                    }
                }
                catch(DetectorNotCreatedException e)
                {
                  System.out.println(e.getMessage());
                }
                catch(ClassCastException e)
                {
                  System.out.println("Tried to load a non-detector as a detector!!");
                }
            }
        }
    
        // Now print some info about the loaded plug-ins
        System.out.println(DetectorCreator.detectorFactories.size() + " plug-in(s) loaded. Names are:");
        for ( Enumeration e = DetectorCreator.detectorFactories.keys() ; e.hasMoreElements() ; )
                System.out.println(e.nextElement());
       }
    }
    

  • SyllableFormDetection.java - the plug-in. It subclasses DetectorFactory and provides its own Factory. For this it needs to implement the create method in DetectorFactory.
    package TextPatterns.Detectors;
    
    import TextPatterns.*;
    import java.util.*; 
    import java.io.*;  
    public class SyllableFormDetection implements Detector
    {
       String type;
       
       public SyllableFormDetection()
       {
          type = new String("syllabic");
       }
    
       public SyllableFormDetection(String t)
       {
          type = t;
       }
       
       public void description()
       {
          System.out.print("This is a description of the SyllableFormDetection plug in.");
          System.out.println(" It is a " + type + " detector");
       }
    
       public String argtype_required() 
       {
          return type; 
       }
    
       public void processing(Vector vec) {
       // Haiku check
          if (vec.size() == 3) {
            System.out.print("Three lines ...");
            if ( ((Integer)vec.elementAt(0)).intValue()==5 &&
                 ((Integer)vec.elementAt(1)).intValue()==7 &&
                 ((Integer)vec.elementAt(2)).intValue()==5)
                    System.out.println("... and a haiku!");
            else
                 System.out.println("... but no haiku");
            }
        }
    
       // This class has to be static (i.e. a class method rather than
       // an object one) because it's called from the static block below
       static class SyllableFormDetectionFactory extends DetectorFactory
       {
          public Detector create(String type)
          {
             return(new SyllableFormDetection(type));
          }
       }
    
       // Note that the following routine is static and has no name, which
       // means it will only be run when the class is loaded
       static
       {
          // put factory in the hashtable for detector factories.
          DetectorCreator.detectorFactories.put("SyllableFormDetection", new SyllableFormDetectionFactory());
       }
    }
    

Plugin requirements

Further plugins can be added. They need to

  • go in the specified directory
  • implement the Detector interface
  • have a type set so that the processing method can receive the appropriate arguments
  • have a factory method, and a static block to register it in the table

Compiling

If you're going to try this example out, you'll have to create a directory named DetectorUser. In that create a directory named Detectors. The DetectorUser directory will store the main sources and the Detectors directory will store the plug-ins. Set your CLASSPATH environment variable to the parent directory of DetectorUser. Then compile the files in the order they're presented here. Run by going to the parent directory and typing java TextPatterns.PatternDetector

The top-level PatternDetector extends DetectorCreator so that it can load plug-ins. SyllableFormDetection is the sample plug-in provided. Loading it calls the plug-in's nameless block which in turn calls the plug-in's factory (which extends DetectorFactory).