Java Reflection APIs and Java interfaces provide great tools for writing reusable code. Take, for example, the case of a generic command launcher: Suppose you had a set of classes performing various tasks -- such as switching on or off the light; opening, closing, or locking the door; and so forth. The name of those classes would be LightOn, LightOff, DoorOpen, DoorClose, and DoorLock, respectively. All these classes conveniently implement the Command interface defined below:
public interface Command {
public void process();
}
You can write a simple generic launcher in the following way:
public class Launcher{
public static void main(String[] args){
if (args.length>0) {
try {
Command command = (Command)Class.forName(args[0]).newInstance();
command.process();
} catch (Exception ex) {
System.out.println("Invalid command");
}
} else {
System.out.println("Usage: Launcher
}
}
} // Launcher
You use the Class.forName() method to get a Class object for the class given in a parameter. Then you create an instance of this class using the newInstance() method. Since the class is expected to implement the Command interface, you cast the object into Command, and then call the process() method that will perform the task. If an exception is launched -- due to a misspelling of the class, a wrong name, or a security exception -- you display an Invalid command message.
You can use the command launcher in the following way:
%java Launcher LightOn
If new tasks are implemented, you don't need to change anything to the launcher. From a programmer point of view, that's great. But what about the user? Suppose a user enters:
%java Launcher OpenDoor
Invalid command
Does the Invalid command message mean the user cannot open the door? No, it is simply a name problem (OpenDoor instead of DoorOpen). The user should be able to see a list of available commands. To keep a generic launcher, he or she must be able to find those commands at runtime.
Java Reflection provides a lot of information about a given class at runtime: you can easily know all its super classes, implemented interfaces, methods, constructors, fields, and so on. But in this case, we are interested in all the classes that implement a given interface. No such information is available via Java Reflection. Before reading Mike Clark's "Java Tip 105: Mastering the Classpath with JWhich,"
A user-friendly generic command launcher
JWhich provides a way to obtain a File object from a package name. Since packages in Java are directories, it is easy to retrieve all the classes contained in a package using the File object's list() method.
The idea is to check, for each class file in the package, whether or not the corresponding class implements the Command interface using the instanceof statement. This means you can only check each class file's public class, and that the interface and its implementations must be located in a package.
Here is the code:
public static void find(String pckgname) {
// Code from JWhich
// ======
// Translate the package name into an absolute path
String name = new String(pckgname);
if (!name.startsWith("/")) {
name = "/" + name;
}
name = name.replace('.','/');
// Get a File object for the package
URL url = Launcher.class.getResource(name);
File directory = new File(url.getFile());
// New code
// ======
if (directory.exists()) {
// Get the list of the files contained in the package
String [] files = directory.list();
for (int i=0;I
// we are only interested in .class files
if (files[i].endsWith(".class")) {
// removes the .class extension
String classname = files[i].substring(0,files[i].length()-6);
try {
// Try to create an instance of the object
Object o = Class.forName(pckgname+"."+classname).newInstance();
if (o instanceof Command) {
System.out.println(classname);
}
} catch (ClassNotFoundException cnfex) {
System.err.println(cnfex);
} catch (InstantiationException iex) {
// We try to instantiate an interface
// or an object that does not have a
// default constructor
} catch (IllegalAccessException iaex) {
// The class is not public
}
}
}
}
}
To perform the task at hand, you just need to modify the original launcher a bit. You can assume now that the interface and its implementations are in the package commands:
public static void main(String[] args){
if (args.length>0) {
try {
Command command = (Command)Class.forName("commands."+args[0]).newInstance();
command.process();
} catch (Exception ex) {
System.out.println("Invalid command");
System.out.println("Available commands:");
find("commands");
}
} else {
System.out.println("Usage: Launcher
}
}
Here is the new result of a wrong command:
%java Launcher OpenDoor
Invalid command
Available commands:
LightOn
LightOff
DoorOpen
DoorClose
DoorLock
Runtime subclass identification
You can refine the find() method to find any subclass of a given class. To do so, you use the dynamic version of instanceof, isInstance(). You replace the (o instanceof Command) with (tosubclass.isInstance(o)), where tosubclass is the class given in the parameter of the find() method.
Now you have a method that can find any subclass of a given class in a given package. You can improve the method by letting it look for the subclasses in the currently loaded packages. To do that, you use the Package.getPackages() method, which returns the exact packages loaded by the current class loader. Then you just need to call the find() method for each package:
public static void find(String tosubclassname) {
try {
Class tosubclass = Class.forName(tosubclassname);
Package [] pcks = Package.getPackages();
for (int i=0;I
}
} catch (ClassNotFoundException ex) {
System.err.println("Class "+tosubclassname+" not found!");
}
}
The result of this method mainly depends on when it is called. In the case of the generic command launcher, few packages will be loaded when the find() method is called. For instance, here are the packages loaded on my NT box before calling the find() method:
Since, in the command launcher, the interface and all its implementations are in the same package, you get the loaded packages after loading the class, thus allowing the search for subclasses in that package. This is the only way to find a relevant package. The complete code for the RTSI class can be found in Resources. Once you unpack the zip file, you can test the code with the following:
% java -cp classes RTSI commands.Command
Working with jar files
The code I have described works fine when the class files are present on the file system, but no longer works when the class files are in a (or several) jar file(s). In the source code, you will find a solution to that problem. You can test that capability with the following:
% java -jar RTSI.jar commands.Command