Java Tip 131: Make a statement with javac!

Interactively explore short bits of Java

Often you may want to test a single piece of code. For example, say you forget how the % operator works with negative numbers, or you must determine how a certain API call operates. Writing, compiling, and running a small program repeatedly just to test small things can prove annoying.

With that in mind, in this Java Tip, I present a short program that compiles and runs Java code statements simply by using tools included in Sun’s JDK 1.2 and above.

Note: You can download this article’s source code from Resources.

Use javac inside your program

You’ll find the javac compiler in the tools.jar library found in the lib/ directory of your JDK 1.2 and higher installation.

Many developers do not realize that an application can access javac programmatically. A class called com.sun.tools.javac.Main acts as the main entry point. If you know how to use javac on the command line, you already know how to use this class: its compile() method takes the familiar command-line input arguments.

Compile a single statement

For javac to compile any statement, the statement must be contained within a complete class. Let’s define a minimal class right now:

    /**
     * Source created on <this date>
     */
    public class <Temporary Class Name> {
        public static void main(String[] args) throws Exception {
            <Your Statement>
        }
    }

Can you figure out why the main() method must throw an exception?

Your statement obviously goes inside the main() method, as shown, but what should you write for the class name? The class name must possess the same name as the file in which it is contained because we declared it as public.

Prepare a file for compilation

Two facilities included in the java.io.File class since JDK 1.2 will help. The first facility, creating temporary files, frees us from choosing some temporary name for our source file and class. It also guarantees the file name’s uniqueness. To perform this task, use the static createTempFile() method.

The second facility, automatically deleting a file when the VM exits, lets you avoid cluttering a directory or directories with temporary little test programs. You set a file for deletion by calling deleteOnExit().

Create the file

Choose the createTempFile() version with which you can specify the new file’s location, instead of relying on some default temporary directory.

Finally, specify that the extension must be .java and that the file prefix should be jav (the prefix choice is arbitrary):

    File file = File.createTempFile("jav", ".java",
            new File(System.getProperty("user.dir")));
    // Set the file to delete on exit
    file.deleteOnExit();
    // Get the file name and extract a class name from it
    String filename = file.getName();
    String classname = filename.substring(0, filename.length()-5);

Note that you extract the class name by removing the .java suffix.

Output the source with your short code segment

Next, write the source code to the file through a PrintWriter for convenience:

    PrintWriter out = new PrintWriter(new FileOutputStream(file));
    out.println("/**");
    out.println(" * Source created on " + new Date());
    out.println(" */");
    out.println("public class " + classname + " {");
    out.println("    public static void main(String[] args) throws Exception {");
    // Your short code segment
    out.print("        "); out.println(text.getText());
    out.println("    }");
    out.println("}");
    // Flush and close the stream
    out.flush();
    out.close();

The generated source code will look nice for later examination, with the added benefit that if the VM exits abnormally without deleting the temporary file, the file will not be a mystery if you stumble upon it later.

The short code segment, if you notice, is written with text.getText(). As you will see shortly, the program uses a small GUI (graphical user interface), and all your code will be typed into a TextArea called text.

Use javac to compile

To use the compiler, create a Main object instance, as mentioned above. Let’s use an instance field to hold this:

    private com.sun.tools.javac.Main javac = new com.sun.tools.javac.Main();

A call to compile() with some command-line arguments will compile the aforementioned file. It also returns a status code indicating either success or a problem with the compile:

    String[] args = new String[] {
        "-d", System.getProperty("user.dir"),
        filename
    };
    int status = javac.compile(args);

Run the freshly compiled program

Reflection nicely runs code inside an arbitrary class, so we’ll use it to locate and execute the main() method where we placed our short code segment. In addition, we process the returned status code by displaying an appropriate message to give users a clean and informative experience. We found the meaning of each status code by decompiling javac, hence we have those weird “Compile status” messages.

The actual class file will reside in the user’s current working directory, as already specified with the -d option to the javac instance.

A 0 status code indicates that the compile succeeded:

    switch (status) {
        case 0:  // OK
            // Make the class file temporary as well
            new File(file.getParent(), classname + ".class").deleteOnExit();
            try {
                // Try to access the class and run its main method
                Class clazz = Class.forName(classname);
                Method main = clazz.getMethod("main", new Class[] { String[].class });
                main.invoke(null, new Object[] { new String[0] });
            } catch (InvocationTargetException ex) {
                // Exception in the main method we just tried to run
                showMsg("Exception in main: " + ex.getTargetException());
                ex.getTargetException().printStackTrace();
            } catch (Exception ex) {
                showMsg(ex.toString());
            }
            break;
        case 1: showMsg("Compile status: ERROR"); break;
        case 2: showMsg("Compile status: CMDERR"); break;
        case 3: showMsg("Compile status: SYSERR"); break;
        case 4: showMsg("Compile status: ABNORMAL"); break;
        default:
            showMsg("Compile status: Unknown exit status");
    }

An InvocationTargetException throws when code executes through reflection and the code itself throws some exception. If that happens, the InvocationTargetException is caught and the underlying exception’s stack trace prints to the console. All other important messages are sent to a showMsg() method that simply relays the text to System.err.

The non-OK status codes (codes other than zero) cause a short error message to display to inform the user what’s happening if a compile problem results.

How to use the program

Yep, that’s it! Other than a nice user interface and a catchy name, the program’s core is complete. The program, featuring a small AWT (Abstract Windowing Toolkit) interface for input, sends all output to System.err on the console (or wherever you decide to send it by changing the showMsg() method).

So, what about a catchy name for the program? How about JavaStatement? It’s concise, to the point, and so boring nobody would think it was chosen this way on purpose. Henceforth, all references to “the program” or “the application” will be replaced by “JavaStatement.”

Write statements

Statements sometimes must be written a certain way, and you must take special care to run the JVM with the proper classpath. I elaborate on these issues below:

  • If you use packages other than java.lang, you may notice the absence of import statements at the top of the generated source code. You may wish to add a few convenient imports such as java.io or java.util to save some typing.
  • If you do not add any imports, then for any class outside java.lang, you must prepend the full package name. For example, to create a new java.util.StringTokenizer, use new java.util.StringTokenizer(...) instead of just new StringTokenizer(...).

Screenshot

The figure below shows JavaStatement’s GUI, with its text area for typing statements and a Run button for executing the code. All output goes to System.err, so watch the window from where the program runs.

JavaStatement screenshot

Run the program

JavaStatement references two classes that may not otherwise be included in the JVM’s classpath: the com.sun.tools.javac.Main class from tools.jar and the temporary compiled classes situated in the current working directory.

Thus, to run the program properly, use a command line such as:

    java -cp <JDK location>/lib/tools.jar;. JavaStatement

where <JDK location> represents your JDK’s installed location.

For example, I installed my JDK to C:Javajdk1.3.1_03. Hence, my command line would be:

    java -cp C:javajdk1.3.1_03libtools.jar;. JavaStatement

Note: You must also include the tools.jar library in the classpath when compiling JavaStatement.java.

If you forget to add the tools.jar file to your classpath, you’ll find complaints either about a NoClassDefFoundError for the JVM or an unresolved symbol for the compiler.

Finally, compile the JavaStatement.java with the same compiler version that runs the code.

Test short bits of code? No problem!

Java developers frequently test short bits of code. JavaStatement lets you efficiently test such code by freeing you from tediously going through the cycle of writing, compiling, and running many small programs.

Beyond JavaStatement, perhaps challenge yourself by exploring how to use javac in your own programs. Off the top of my head, I can think of two javac uses:

  1. A rudimentary form of scripting: Coupled with using your own classloader, you could make the compiled classes reference objects from within a running program
  2. A programming language translator: It is simpler to translate a program (say written in Pascal) into Java, and compile it into classfiles, than to compile it yourself

Remember, javac does the hard work of translating a high-level language into low-level instructions—freeing you to do your own thing with Java!

Shawn
Silverman is a graduate student in the Department of Electrical
and Computer Engineering at the University of Manitoba, Canada. He
began working with Java in mid-1996 and has been using it almost
exclusively ever since. His current interests include the
simulation of electric fields and fluids, embedded Java, and the
implementation of nifty GUI tricks. Shawn also teaches a third-year
software design course in the computer engineering department at
his university.

Source: www.infoworld.com