This chapter is from the book

This chapter is from the book

10.2. The Compiler API

In the preceding sections, you saw how to interact with code in a scripting language. Now we turn to a different scenario: Java programs that compile Java code. There are quite a few tools that need to invoke the Java compiler, such as:

Development environments

Java teaching and tutoring programs

Build and test automation tools

Templating tools that process snippets of Java code, such as JavaServer Pages (JSP)

In the past, applications invoked the Java compiler by calling undocumented classes in the jdk/lib/tools.jar library. As of Java SE 6, a public API for compilation is a part of the Java platform, and it is no longer necessary to use tools.jar. This section explains the compiler API.

The compiler sends output and error messages to the provided streams. You can set these parameters to null, in which case System.out and System.err are used. The first parameter of the run method is an input stream. As the compiler takes no console input, you can always leave it as null. (The run method is inherited from a generic Tool interface, which allows for tools that read input.)

The remaining parameters of the run method are simply the arguments that you would pass to javac if you invoked it on the command line. These can be options or file names.

10.2.2. Using Compilation Tasks

You can have even more control over the compilation process with a CompilationTask object. In particular, you can

Control the source of program code—for example, by providing code in a string builder instead of a file.

Control the placement of class files—for example, by storing them in a database.

Listen to error and warning messages as they occur during compilation.

Run the compiler in the background.

The location of source and class files is controlled by a JavaFileManager. It is responsible for determining JavaFileObject instances for source and class files. A JavaFileObject can correspond to a disk file, or it can provide another mechanism for reading and writing its contents.

To listen to error messages, install a DiagnosticListener. The listener receives a Diagnostic object whenever the compiler reports a warning or error message. The DiagnosticCollector class implements this interface. It simply collects all diagnostics so that you can iterate through them after the compilation is complete.

A Diagnostic object contains information about the problem location (including file name, line number, and column number) as well as a human-readable description.

To obtain a CompilationTask object, call the getTask method of the JavaCompiler class. You need to specify:

A Writer for any compiler output that is not reported as a Diagnostic, or null to use System.err

A JavaFileManager, or null to use the compiler’s standard file manager

A DiagnosticListener

Option strings, or null for no options

Class names for annotation processing, or null if none are specified (we’ll discuss annotation processing later in this chapter)

JavaFileObject instances for source files

You need to provide the last three arguments as Iterable objects. For example, a sequence of options might be specified as

Iterable<String> options = Arrays.asList("-g", "-d", "classes");

Alternatively, you can use any collection class.

If you want the compiler to read source files from disk, you can ask the StandardJavaFileManager to translate the file name strings or File objects to JavaFileObject instances. For example,

However, if you want the compiler to read source code from somewhere other than a disk file, you need to supply your own JavaFileObject subclass. Listing 10.3 shows the code for a source file object with data contained in a StringBuilder. The class extends the SimpleJavaFileObject convenience class and overrides the getCharContent method to return the content of the string builder. We’ll use this class in our example program in which we dynamically produce the code for a Java class and then compile it.

The CompilationTask interface extends the Callable<Boolean> interface. You can pass it to an Executor for execution in another thread, or you can simply invoke the call method. A return value of Boolean.FALSE indicates failure.

If you simply want the compiler to produce class files on disk, you need not customize the JavaFileManager. However, our sample application will generate class files in byte arrays and later read them from memory, using a special class loader. Listing 10.4 defines a class that implements the JavaFileObject interface. Its openOutputStream method returns the ByteArrayOutputStream into which the compiler will deposit the bytecodes.

It turns out a bit tricky to tell the compiler’s file manager to use these file objects. The library doesn’t supply a class that implements the StandardJavaFileManager interface. Instead, you subclass the ForwardingJavaFileManager class that delegates all calls to a given file manager. In our situation, we only want to change the getJavaFileForOutput method. We achieve this with the following outline:

In summary, call the run method of the JavaCompiler task if you simply want to invoke the compiler in the usual way, reading and writing disk files. You can capture the output and error messages, but you need to parse them yourself.

If you want more control over file handling or error reporting, use the CompilationTask interface instead. Its API is quite complex, but you can control every aspect of the compilation process.

intercept this call if you want to substitute a file object for writing class files; kind is one of SOURCE, CLASS, HTML, or OTHER.

10.2.3. An Example: Dynamic Java Code Generation

In the JSP technology for dynamic web pages, you can mix HTML with snippets of Java code, such as

<p>The current date and time is <b><%= new java.util.Date() %></b>.</p>

The JSP engine dynamically compiles the Java code into a servlet. In our sample application, we use a simpler example and generate dynamic Swing code instead. The idea is that you use a GUI builder to lay out the components in a frame and specify the behavior of the components in an external file. Listing 10.5 shows a very simple example of a frame class, and Listing 10.6 shows the code for the button actions. Note that the constructor of the frame class calls an abstract method addEventHandlers. Our code generator will produce a subclass that implements the addEventHandlers method, adding an action listener for each line in the action.properties file. (We leave it as the proverbial exercise to the reader to extend the code generation to other event types.)

We place the subclass into a package with the name x, which we hope is not used anywhere else in the program. The generated code has the form

The buildSource method in the program of Listing 10.7 builds up this code and places it into a StringBuilderJavaSource object. That object is passed to the Java compiler.

We use a ForwardingJavaFileManager with a getJavaFileForOutput method that constructs a ByteArrayJavaClass object for every class in the x package. These objects capture the class files generated when the x.Frame class is compiled. The method adds each file object to a list before returning it so that we can locate the bytecodes later. Note that compiling the x.Frame class produces a class file for the main class and one class file per listener class.

After compilation, we build a map that associates class names with bytecode arrays. A simple class loader (shown in Listing 10.8) loads the classes stored in this map.

We ask the class loader to load the class that we just compiled, and then we construct and display the application’s frame class.

Run the program again. Now the Yellow button is disabled after you click it. Also have a look at the code directories. You will not find any source or class files for the classes in the x package. This example demonstrates how you can use dynamic compilation with in-memory source and class files.