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.
10.2.1. Compiling the Easy Way
It is very easy to invoke the compiler. Here is a sample call:
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); OutputStream outStream = ...; OutputStream errStream = ...; int result = compiler.run(null, outStream, errStream, "-sourcepath", "src", "Test.java");
A result value of 0 indicates successful compilation.
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,
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); Iterable<JavaFileObject> fileObjects = fileManager.getJavaFileObjectsFromStrings(fileNames);
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.
Callable<Boolean> task = new JavaCompiler.CompilationTask(null, fileManager, diagnostics, options, null, fileObjects); if (!task.call()) System.out.println("Compilation failed");
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:
JavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null); fileManager = new ForwardingJavaFileManager<JavaFileManager>(fileManager) { public JavaFileObject getJavaFileForOutput(Location location, final String className, Kind kind, FileObject sibling) throws IOException { return custom file object } };
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.
Listing 10.3. compiler/StringBuilderJavaSource.java
1 package compiler; 2 3 import java.net.*; 4 import javax.tools.*; 5 6 /** 7 * A Java source that holds the code in a string builder. 8 * @version 1.00 2007-11-02 9 * @author Cay Horstmann 10 */ 11 public class StringBuilderJavaSource extends SimpleJavaFileObject 12 { 13 private StringBuilder code; 14 15 /** 16 * Constructs a new StringBuilderJavaSource. 17 * @param name the name of the source file represented by this file object 18 */ 19 public StringBuilderJavaSource(String name) 20 { 21 super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), 22 Kind.SOURCE); 23 code = new StringBuilder(); 24 } 25 26 public CharSequence getCharContent(boolean ignoreEncodingErrors) 27 { 28 return code; 29 } 30 31 public void append(String str) 32 { 33 code.append(str); 34 code.append('\n'); 35 } 36 }
Listing 10.4. compiler/ByteArrayJavaClass.java
1 package compiler; 2 3 import java.io.*; 4 import java.net.*; 5 import javax.tools.*; 6 /** 7 * A Java class that holds the bytecodes in a byte array. 8 * @version 1.00 2007-11-02 9 * @author Cay Horstmann 10 */ 11 public class ByteArrayJavaClass extends SimpleJavaFileObject 12 { 13 private ByteArrayOutputStream stream; 14 15 /** 16 * Constructs a new ByteArrayJavaClass. 17 * @param name the name of the class file represented by this file object 18 */ 19 public ByteArrayJavaClass(String name) 20 { 21 super(URI.create("bytes:///" + name), Kind.CLASS); 22 stream = new ByteArrayOutputStream(); 23 } 24 25 public OutputStream openOutputStream() throws IOException 26 { 27 return stream; 28 } 29 30 public byte[] getBytes() 31 { 32 return stream.toByteArray(); 33 } 34 }
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
package x; public class Frame extends SuperclassName { protected void addEventHandlers() { componentName1.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent) { code for event handler1 } } ); // repeat for the other event handlers ... } }
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.
ClassLoader loader = new MapClassLoader(byteCodeMap); Class<?> cl = loader.loadClass("x.Frame"); Frame frame = (JFrame) cl.newInstance(); frame.setVisible(true);
When you click the buttons, the background color changes in the usual way. To see that the actions are dynamically compiled, change one of the lines in action.properties, for example, like this:
yellowButton=panel.setBackground(java.awt.Color.YELLOW); yellowButton.setEnabled(false);
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.
Listing 10.5. buttons2/ButtonFrame.java
1 package buttons2; 2 import javax.swing.*; 3 4 /** 5 * @version 1.00 2007-11-02 6 * @author Cay Horstmann 7 */ 8 public abstract class ButtonFrame extends JFrame 9 { 10 public static final int DEFAULT_WIDTH = 300; 11 public static final int DEFAULT_HEIGHT = 200; 12 13 protected JPanel panel; 14 protected JButton yellowButton; 15 protected JButton blueButton; 16 protected JButton redButton; 17 18 protected abstract void addEventHandlers(); 19 20 public ButtonFrame() 21 { 22 setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); 23 24 panel = new JPanel(); 25 add(panel); 26 27 yellowButton = new JButton("Yellow"); 28 blueButton = new JButton("Blue"); 29 redButton = new JButton("Red"); 30 31 panel.add(yellowButton); 32 panel.add(blueButton); 33 panel.add(redButton); 34 35 addEventHandlers(); 36 } 37 }
Listing 10.6. buttons2/action.properties
1 yellowButton=panel.setBackground(java.awt.Color.YELLOW); 2 blueButton=panel.setBackground(java.awt.Color.BLUE);
Listing 10.7. compiler/CompilerTest.java
1 package compiler; 2 3 import java.awt.*; 4 import java.io.*; 5 import java.util.*; 6 import java.util.List; 7 import javax.swing.*; 8 import javax.tools.*; 9 import javax.tools.JavaFileObject.*; 10 11 /** 12 * @version 1.00 2007-10-28 13 * @author Cay Horstmann 14 */ 15 public class CompilerTest 16 { 17 public static void main(final String[] args) throws IOException, ClassNotFoundException 18 { 19 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); 20 21 final List<ByteArrayJavaClass> classFileObjects = new ArrayList<>(); 22 23 DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>(); 24 25 JavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null); 26 fileManager = new ForwardingJavaFileManager<JavaFileManager>(fileManager) 27 { 28 public JavaFileObject getJavaFileForOutput(Location location, final String className, 29 Kind kind, FileObject sibling) throws IOException 30 { 31 if (className.startsWith("x.")) 32 { 33 ByteArrayJavaClass fileObject = new ByteArrayJavaClass(className); 34 classFileObjects.add(fileObject); 35 return fileObject; 36 } 37 else return super.getJavaFileForOutput(location, className, kind, sibling); 38 } 39 }; 40 41 42 String frameClassName = args.length == 0 ? "buttons2.ButtonFrame" : args[0]; 43 JavaFileObject source = buildSource(frameClassName); 44 JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, null, 45 null, Arrays.asList(source)); 46 Boolean result = task.call(); 47 48 for (Diagnostic<? extends JavaFileObject> d : diagnostics.getDiagnostics()) 49 System.out.println(d.getKind() + ": " + d.getMessage(null)); 50 fileManager.close(); 51 if (!result) 52 { 53 System.out.println("Compilation failed."); 54 System.exit(1); 55 } 56 57 EventQueue.invokeLater(new Runnable() 58 { 59 public void run() 60 { 61 try 62 { 63 Map<String, byte[]> byteCodeMap = new HashMap<>(); 64 for (ByteArrayJavaClass cl : classFileObjects) 65 byteCodeMap.put(cl.getName().substring(1), cl.getBytes()); 66 ClassLoader loader = new MapClassLoader(byteCodeMap); 67 JFrame frame = (JFrame) loader.loadClass("x.Frame").newInstance(); 68 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 69 frame.setTitle("CompilerTest"); 70 frame.setVisible(true); 71 } 72 catch (Exception ex) 73 { 74 ex.printStackTrace(); 75 } 76 } 77 }); 78 } 79 80 /* 81 * Builds the source for the subclass that implements the addEventHandlers method. 82 * @return a file object containing the source in a string builder 83 */ 84 static JavaFileObject buildSource(String superclassName) 85 throws IOException, ClassNotFoundException 86 { 87 StringBuilderJavaSource source = new StringBuilderJavaSource("x.Frame"); 88 source.append("package x;\n"); 89 source.append("public class Frame extends " + superclassName + " {"); 90 source.append("protected void addEventHandlers() {"); 91 final Properties props = new Properties(); 92 props.load(Class.forName(superclassName).getResourceAsStream("action.properties")); 93 for (Map.Entry<Object, Object> e : props.entrySet()) 94 { 95 String beanName = (String) e.getKey(); 96 String eventCode = (String) e.getValue(); 97 source.append(beanName + ".addActionListener(new java.awt.event.ActionListener() {"); 98 source.append("public void actionPerformed(java.awt.event.ActionEvent event) {"); 99 source.append(eventCode); 100 source.append("} } );"); 101 } 102 source.append("} }"); 103 return source; 104 } 105 }
Listing 10.8. compiler/MapClassLoader.java
1 package compiler; 2 3 import java.util.*; 4 5 /** 6 * A class loader that loads classes from a map whose keys are class names and whose values are 7 * byte code arrays. 8 * @version 1.00 2007-11-02 9 * @author Cay Horstmann 10 */ 11 public class MapClassLoader extends ClassLoader 12 { 13 private Map<String, byte[]> classes; 14 15 public MapClassLoader(Map<String, byte[]> classes) 16 { 17 this.classes = classes; 18 } 19 20 protected Class<?> findClass(String name) throws ClassNotFoundException 21 { 22 byte[] classBytes = classes.get(name); 23 if (classBytes == null) throw new ClassNotFoundException(name); 24 Class<?> cl = defineClass(name, classBytes, 0, classBytes.length); 25 if (cl == null) throw new ClassNotFoundException(name); 26 return cl; 27 } 28 }