Build a Reflection-based Interpreter in Java

cripting languages exist because some programming tasks are both simple and important, and carrying them out with a language that doesn’t tax the mind as much as a system programming language sometimes will is beneficial. (See Sidebar: Scripting vs. Programming for a full explanation of the differences between scripting languages and programming languages.) Although Java is most decidedly a programming language (it is typed, highly structured, and compiled), don’t you sometimes wish you could do scripting in Java, or something close to Java?

Thanks to the Reflection API, you can. Using the Reflection API, you can run methods dynamically. Java’s support of reflection enables you to create an interpreter that executes commands interactively. This article describes a system you can use to build an interpreter into your application. It demonstrates how to build a reflection-based system that allows simple scripting of Java programs without having to install a special-purpose scripting language. It also provides a downloadable sample program. This solution can allow you to create simple control scripts for your Java programs.

The Scripting “Language”
Before I get to the implementation, let’s take a look at the “language.” I put the word language in quotes because this language is so crude it hardly merits the term. It has no control structures, no types, no variables, no keywords, and no expressions. It simply allows you to call a Java method. Nevertheless, it constitutes a language, which makes our implementation an interpreter.

Here’s an example of a command in this language:

TinyChatServer startServer 5000

This command starts up a chat server listening on port 5000. The chat server is implemented by a class called TinyChatServer, which has a method called startServer:

static public String startServer( int port );

In general, each command has the format shown in Figure 1.

Figure 1: A Scripting Command

Each command consists of a class name, a method name, and one or more arguments. Here’s another example of a command:

Calculator add 10.0 20.0

Our interpreter interprets this command by calling the add method of the Calculator class, and passing it two arguments: 10.0 and 20.0.Every command specifies a class as the first element of the interpreter. In fact, the first word of every command in the scripting language must be a valid Java class. Additionally, the second word of every method must be a valid static, public method of that class.

These classes and methods are accessed from the CommandLine interpreter class, which is the main class of your scripting language. Figure 2 shows the relationships between the parts of a scripting command and the classes and methods running inside the CommandLine object.

Figure 2: The CommandLine Object

The following is a piece of the Calculator class:

static public double add( double a, double b ) {  return a + b;}

This method must be static. As I mentioned previously, the scripting language has no variables. If you create an instance of an object, you have no place to put it and no way to reference it from the interpreter. By simply making sure that your methods are static, you bypass the need for instances.

Note also that your method does have a return value. This return value is printed after the method is executed.

Test: The Sample Program
Test is the name of the sample program provided with this article. Here’s an example of a simple session within it:

% java Test$ Calculator add 10.0 20.030.0$ Calculator divide 100.0 4.025.0$ Calculator divide 100.0 0.0Infinity$ Calculator divide 100.0 helloError: CommandLineException: Can't find method divide in Calculator$ TinyChatServer startServer 5000Server started on port 5000Listening on ServerSocket[addr=0.0.0.0/0.0.0.0,port=0,localport=5000]$ TinyChatServer startServer 5555Listening on ServerSocket[addr=0.0.0.0/0.0.0.0,port=0,localport=5555]$

Don’t let the prompts confuse you. The first prompt, %, is the actual operating system command prompt, at which you type the java command. The second prompt, $, is the interpreter prompt.

This interpreter session does a little arithmetic and then starts up two chat servers (running on different ports). (You can try out the chat server by running the command java TinyChatServer hostname port at another command prompt.) Notice that your interpreter catches errors: when you tried to divide 100.0 by “hello” it reported that it couldn’t find a method that matched the arguments. I’ll discuss this more later. First, take a look at the code to see how CommandLine works.

CommandLine reads commands from an InputStream and writes its output to an OutputStream. This function allows CommandLine to read commands from files, standard input, or any other data source that can be presented as a string. It also allows CommandLine to redirect the output of a command anywhere.

The Test program creates a CommandLine object that reads from System.in and writes to System.out as follows:

static public void main( String args[] ) throws Exception {  new CommandLine( System.in, System.out );}

CommandLine has a background thread that reads from the input stream and writes to the output stream. Here’s an abbreviated version of the interpreter (download the full source for a complete look):

String line = in.readLine();String result = execute( line );out.write( result );

Whatever the command returns is printed to the output stream.The program parses each command using java.util.StringTokenizer. This breaks the command up into words by splitting it at whitespace as follows:

StringTokenizer st = new StringTokenizer( line );List tokens = new ArrayList();while (st.hasMoreTokens()) {  String token = st.nextToken();  tokens.add( token );}

This is a relatively simple parsing method, but it’s sufficient for your purposes.

Finding the Class
Once we’ve got the command line broken up into words, you have access to the first word, which is the class name:

String className = line[0];try {  Class clas = Class.forName( className );} catch( ClassNotFoundException cnfe ) {  throw new CommandLineException(    "Can't find class "+className );}

Loading the class is as easy as calling Class.forName(), which looks for the specified class in the classpath. This means you can also use a fully qualified class name (FQCN) such as mypackage.mysubpackage.MyClass, and the CommandLine will find that class.

Finding the Method
Now that you have the correct class, you have to find the correct method, which is specified as the second word in the command. However, finding the method isn’t quite as simple as finding the class. Because Java allows method overloading, a number of different methods can have the same name. The method name isn’t enough.

To find the correct method, you need to look at the types of the arguments as well. You can do this with reflection. CommandLine has a method called getTypes that scans a list of arguments contained in an object array and returns an array of classes:

Object args[] = narrow( line, 2 );Class types[] = getTypes( args );

Wondering what the narrow method is? Narrow takes an array of objects and narrows each one. More specifically, type narrowing is the process of finding the most specific class for an object. When you first get the arguments to the methods, they are string objects. However, some of these strings actually contain integers or floating-point values. Narrow checks each object in the argument array. If it can be parsed correctly as an integer, narrow turns it into an integer. If not, narrow tries to parse it as a floating-point value and, if successful, turns it into a double. Otherwise, it is left as a string.

This allows you to find the most type-appropriate method to call. For example, Calculator.add takes two doubles as arguments. If the user has in fact supplied two doubles on the command line, then you’ll be able to find the method called add that takes two doubles as arguments.

Use Class.getDeclaredMethod to find the proper method. This class is part of the Reflection API, and it takes both a method name and an array of method types. If there is an appropriate method, the following will find it:

String methodName = line[1];try {  Method method =    class.getDeclaredMethod( methodName, types );} catch( NoSuchMethodException nsme ) {  throw new CommandLineException(    "Can't find method "+methodName+" in "+className );}

Method.invoke actually calls the method. This method takes two arguments: the object instance and the arguments. Since you are calling a static method, you don’t have an object instance. So use null as the instance:

try {  Object retval = method.invoke( null, args );} catch( IllegalAccessException iae ) {  throw new CommandLineException(    "Not allowed to call method "+      methodName+" in "+className );} catch( InvocationTargetException ite ) {  throw    (CommandLineException)    new CommandLineException(      "Exception while executing command" )        .initCause( ite );}

This code throws a couple of exceptions. IllegalAccessException is thrown if you don’t have privileges to call the method, such as when it’s a private method. InvocationTargetException is thrown if the method itself throws an exception. The actual method exception is wrapped in an instance of InvocationTargetException, and you wrap this in a CommandLineException and then throw it again.

Returning a Value
As mentioned earlier, any return value is printed to the CommandLine‘s output stream. When you called Method.invoke, you got an Object as a return value. You can simply convert this to a String, and print that string to the output stream. CommandLine.execute converts the object to a string, which is printed to the output stream:

String result = execute( line );out.write( result );

Write Scripts
Since you have a scripting language, why not write scripts? You can supply the following script to the interpreter (under Unix or a Windows command prompt):

TinyChatServer startServer 5000TinyChatServer startServer 5555Calculator add 10.0 20.0Calculator subtract 10.0 20.0Calculator multiply 10.0 20.0Calculator divide 10.0 20.0

When you supply this file as the input to the Test program, it runs each of the commands in sequence. The following is the output:

% java Test < test.input.txt$ Server started on port 5000$ Server started on port 5555$ 30.0$ -10.0$ 200.0$ 0.5$ Command line exiting.Listening on ServerSocket[addr=0.0.0.0/0.0.0.0,port=0,localport=5555]Listening on ServerSocket[addr=0.0.0.0/0.0.0.0,port=0,localport=5000]

The Power of Reflection
The Reflection API is powerful enough that one actually could write a full-fledged Java interpreter--and in fact developers have already done it (see the BeanShell site). Obviously, the program provided here is suited for running more simple commands. However, you could easily use the solution described in this article to add administrative commands to an existing server. Simply connect a CommandLine to a network port and you'd enable users to connect to a running server and issue commands to it.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

Recent Articles: