Friday, April 6, 2007

[completeJava] Java Compiler API

Compiling with the Java Compiler API  

From day one, the standard Java platform has been lacking standard interfaces to call and generate Java byte codes using its compiler. Using Sun's implementation of the platform, a user could access the non-standard Main class of the com.sun.tools.javac package to compile your code (found in the tools.jar file in the lib subdirectory). However, that package doesn't provide standard, public programming interfaces. Users of other implementations don't necessarily have access to the class. With Java SE 6 and its new Java Compiler API defined by JSR-199, you can access the javac compiler tool from your own applications.

There are two ways to use the tool. One way is simple, and the other is more complicated but allows you to manipulate more options. You'll first use the simpler way first to compile the "Hello, World" program, shown here:

public class Hello {
  public static void main(String args[]) {
    System.out.println("Hello, World");
  }
}

To invoke the Java compiler from your Java programs, you need to access the JavaCompiler interface. Among other things, accessing the interface allows you to set the source path, the classpath, and the destination directory. Specifying each of the compilable files as a JavaFileObject instance allows you to compile each of them. However, you don't quite need to know about JavaFileObject just yet.

Use the ToolProvider class to request the default implementation of the JavaCompiler interface. The ToolProvider class provides a getSystemJavaCompiler() method, which returns an instance of the JavaCompiler interface.

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

The simplest way to compile with the JavaCompiler is to use the run() method, which is defined in the Tool interface, which it implements:

int run(InputStream in, 
    OutputStream out, 
    OutputStream err, 
    String... arguments)

Pass null stream arguments to use the defaults of System.in, System.out, and System.err respectively for the first three arguments. The varargs set of String objects represents the filenames to pass into the compiler.

Thus, to compile the Hello class source previously shown, you need the following:

int results = tool.run(null, null, null, "Hello.java");

Assuming no compilation errors, this will generate the Hello.class file in the destination directory. Had there been an error, the run() method sends output to the standard error stream, which is the third argument of the run() method. The method returns a non-zero result when errors occur.

You can use the following code to compile the Hello.java source file:

import java.io.*;
import javax.tools.*;

public class CompileIt {
  public static void main(String args[]) throws IOException {
    JavaCompiler compiler =
        ToolProvider.getSystemJavaCompiler();
    int results = compiler.run(
        null, null, null, "Hello.java");
    System.out.println("Result code: " + results);
  }
}

After you compile the CompileIt program once, you can run it multiple times without recompilation when you need to change or recompile the Hello.java source. Assuming no errors, running CompileIt will produce the following output:

> java CompileIt
Result code: 0

Running CompileIt also produces a Hello.class file in the same directory:

> ls
CompileIt.class
CompileIt.java
Hello.class
Hello.java

You could stop there since that is sufficient to use the now standard compiler, but there is more. You have a second way to access the compiler for when you want better access to the results. More specifically, this second way allows developers to present the compilation results in a more meaningful manner, rather than just pass along the error text that went to stderr. The better approach to using the compiler takes advantage of the StandardJavaFileManager class. The file manager provides a way to work with regular files for both input and output operations. It also reports diagnostic messages with the help of a DiagnosticListener instance. The DiagnosticCollector class you will be using is just one such implementation of that listener.

Before identifying what needs to be compiled, you need a file manager. Create a file manager in two basic steps: create a DiagnosticCollector and then ask the JavaCompiler for the file manager with its getStandardFileManager() method. Pass the DiagnosticListener object to the getStandardFileManager() method. This listener reports non-fatal problems and you can optionally share it with the compiler by passing it into the getTask() method later.

DiagnosticCollector<JavaFileObject> diagnostics =
    new DiagnosticCollector<JavaFileObject>();
StandardJavaFileManager fileManager =
    compiler.getStandardFileManager(diagnostics, aLocale, aCharset);

You could provide a null diagnostics listener to the call, but that is just about the same as using the earlier compilation method.

Before looking at the details of StandardJavaFileManager, the compilation process involves a single method of JavaCompiler called getTask(). It takes six arguments and returns an instance of an inner class called CompilationTask :

JavaCompiler.CompilationTask getTask(
    Writer out,
    JavaFileManager fileManager,
    DiagnosticListener<? super JavaFileObject> diagnosticListener,
    Iterable<String> options,
    Iterable<String> classes,
    Iterable<? extends JavaFileObject> compilationUnits)

Most of these arguments can be null, with logical defaults.
* out: System.err
* fileManager: compiler's standard file manager
* diagnosticListener: compiler's default behavior
* options: no command-line options to compiler
* classes: no class names for annotation processing

The last argument, compilationUnits, really shouldn't be null as that is what you want to compile. That brings us back to StandardJavaFileManager. Notice the argument type: Iterable<? extends JavaFileObject>. Two methods of StandardJavaFileManager give this result. You can either start with a List of File objects, or a List of String objects, representing the file names:

Iterable<? extends JavaFileObject> getJavaFileObjectsFromFiles(
    Iterable<? extends File> files)
Iterable<? extends JavaFileObject> getJavaFileObjectsFromStrings(
    Iterable<String> names)

Actually, anything that is Iterable can be used to identify the collection of items to compile here, not just a List. A List just happens to be the easiest to create:

String[] filenames = ...;
Iterable<? extends JavaFileObject> compilationUnits =
    fileManager.getJavaFileObjectsFromFiles(Arrays.asList(filenames));

You now have all the necessary information to compile your source files. The JavaCompiler.CompilationTask returned from getTask( ) implements Callable. So, to start the task, invoke the call() method:

JavaCompiler.CompilationTask task =
    compiler.getTask(null, fileManager, null, null, null, compilationUnits);
Boolean success = task.call();

Assuming no compilation warnings or errors, the call() method will compile all the files identified by the compilationUnits variable, including all compilable dependencies. To find out if everything succeeded, check the Boolean return value for success. The call() method returns Boolean.TRUE only if all the compilation units compile. On any error, the method returns Boolean.FALSE.

Before showing the working example, let us add one last thing, the DiagnosticListener, or more specifically, its implementer DiagnosticCollector. Passing the listener as the third argument to getTask() allows you to ask after compilation for the diagnostics:

for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
  System.console().printf(
      "Code: %s%n" +
      "Kind: %s%n" +
      "Position: %s%n" +
      "Start Position: %s%n" +
      "End Position: %s%n" +
      "Source: %s%n" +
      "Message:  %s%n",
      diagnostic.getCode(), diagnostic.getKind(),
      diagnostic.getPosition(), diagnostic.getStartPosition(),
      diagnostic.getEndPosition(), diagnostic.getSource(),
      diagnostic.getMessage(null));
}

And lastly, you should call the file manager's close() method.

Putting all that together gives us the following program, to again just compile the Hello class:

import java.io.*;
import java.util.*;
import javax.tools.*;

public class BigCompile {
  public static void main(String args[]) throws IOException {
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    DiagnosticCollector<JavaFileObject> diagnostics =
        new DiagnosticCollector<JavaFileObject>();
    StandardJavaFileManager fileManager =
        compiler.getStandardFileManager(diagnostics, null, null);
    Iterable<? extends JavaFileObject> compilationUnits =
        fileManager.getJavaFileObjectsFromStrings(Arrays.asList("Hello.java"));
    JavaCompiler.CompilationTask task = compiler.getTask(
        null, fileManager, diagnostics, null, null, compilationUnits);
    Boolean success = task.call();
    for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
      System.console().printf(
          "Code: %s%n" +
          "Kind: %s%n" +
          "Position: %s%n" +
          "Start Position: %s%n" +
          "End Position: %s%n" +
          "Source: %s%n" +
          "Message:  %s%n",
          diagnostic.getCode(), diagnostic.getKind(),
          diagnostic.getPosition(), diagnostic.getStartPosition(),
          diagnostic.getEndPosition(), diagnostic.getSource(),
          diagnostic.getMessage(null));
    }
    fileManager.close();
    System.out.println("Success: " + success);
  }
}

Compiling and running this program will just print out the success message:

> javac BigCompile.java
> java BigCompile
Success: true

However, if you change the println method to the mistyped pritnln method, you instead get the following when run:

> java BigCompile
Code: compiler.err.cant.resolve.location
Kind: ERROR
Position: 80
Start Position: 70
End Position: 88
Source: Hello.java
Message:  Hello.java:3: cannot find symbol
symbol  : method pritnln(java.lang.String)
location: class java.io.PrintStream
Success: false

Using the Compiler API, you can do much more than what has been presented in this brief tip. For example, you can control the input and output directories or highlight the compilation errors in an integrated editor environment. Now, thanks to the Java Compiler API, you can do all that with standard API calls. For more information on the Java Compiler API and JSR 199, see the JSR 199 specification.

__._,_.___
Recent Activity
Visit Your Group
SPONSORED LINKS
Yahoo! Finance

It's Now Personal

Guides, news,

advice & more.

Need traffic?

Drive customers

With search ads

on Yahoo!

Yahoo! Groups

Start a group

in 3 easy steps.

Connect with others.

.

__,_._,___

No comments: