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 ofpackage
,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 toimport
it.interface
- Unlike C++, Java doesn't support multiple inheritance - objects can be derived from only one parent. However, Java hasinterface
s 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, aninterface
is defined in Detector.java
package TextPatterns; import java.util.*; public interface Detector { void description(); String argtype_required(); void processing(Vector v); }
interface
s are created and used rather in the same way as classes are except that whereas a class can onlyextend
one class it canimplement
severalinterface
s. 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 adescription
method which should display a description of the plug-in.exception
- When something goes wrong, a java programthrow
s a bundle of information called anexception
that another piece of code shouldcatch
. 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
thestatic 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 theClassNotFoundException
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
).