调用Java编译器API编译Java

本文由 外刊IT评论   组织翻译


Core Java Technologies Tech Tips

Compiling with the Java Compiler API

调用Java编译器API编译Java

从第一天开始,标准Java平台就缺少能够被调用,去产生Java字节码的编译器接口. 使用Sun实现的平台,一个用户可以通过非标准的 com.sun.tools.javac   包中的Main class 去编译你的代码 (你可以在lib子目录下的 tools.jar 文件里找到它). 然而这个包并没有提供一个标准的公开的编程接口. 使用其它实现的用户必然不能访问这个类. 使用Java SE 6和在JSR-199中定义的它的新的Java编译器接口,你可以从你自己的应用程序里访问javac编译工具了.

有两种方式使用这种工具. 一种是简单的,一种是稍微复杂点但拥有更多选项的. 你首先将会用较简单的一种去编译 "Hello, World"程序,就是下面的这个:

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

要想从Java程序里调用Java编译器,你需要访问JavaCompiler 接口. 除此外,通过访问这个接口,你可以设置源代码的路径,classpath,和目标目录. 通过指定可编译的文件为 JavaFileObject instance ,你可以将它们全部编译. 然而,你并不需要对 JavaFileObject 了解多少.

可以使用 ToolProvider 类去请求 theJavaCompiler 接口的缺省实现. 这个 ToolProvider 类提供了一个 getSystemJavaCompiler() 方法, 它返回一个 JavaCompiler 接口的实例.

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
 

使用 JavaCompiler 运行编译最简单的方法是调用在这个接口工具里定义的 run() 方法,它的实现是:

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

分别为前三个缺省参数 System.in , System.out , and System.err 传入 null 值. 参数集 String 对象表示着传入编译器的文件的名称.

这样,你应该像下面这样去编译前面显示的 Hello 源程序:

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

假设没有编译错误,这样会在目标目录里产生一个 Hello.class文件. 如果这里有错误, run() 方法会把它输出到标准错误输出流里,也就是 run()  方法的的第三个参数. 当错误发生时这个方法返回一个非0的结果.

你可以使用下面的代码去编译 Hello.java 源文件:

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);
  }
}
 

一旦你编译了 CompileIt 一次 ,你就可以多次运行它,当你修改了 Hello.java 源程序时或者要重新编译它,你不需要重新编译 CompileIt . 如果没有错误,运行 CompileIt 会产生下面的输出:

> java CompileIt
Result code: 0
 

运行 CompileIt 同样也会在相同的目录下产生一个 Hello.class 文件:

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

你可以完事了,因为这样使用标准编译器已经足够了,可是这还有更有用的. 当你需要更好的处理这些结果时,你可以使用第二种方法来访问编译器. 更特别的是,这第二种方式允许开发者将编译输出结果用一种更有意义的方式表现出来,而不是简单的那种送往stdeer的错误文本. 利用 StandardJavaFileManager 类我们有这种更好的途径使用编译器. 这个文件管理器提供了一种方式,用来处理普通文件的输入输出操作. 它同时利用 DiagnosticListener 实例来报告调试信息. 你需要使用的 DiagnosticCollector 类其实是监听器的一种实现.

在搞清楚你需要编译什么之前,你需要一个文件管理器. 生成一个管理器基本上需要两步: 创建一个 DiagnosticCollector 和 使用 JavaCompilergetStandardFileManager() 方法获得一个文件管理器. 把 DiagnosticListener 对象传入 getStandardFileManager() 方法中. 这个监听器可以报告一些非致命的问题,到后来你可以选择性的通过把它传入 getTask() 方法来和编译器共享.

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

你也可以往这个调用里传入一个 null 值的诊断器,但这样也就等于用以前的编译器方法了.

在详细查看 StandardJavaFileManager 之前 ,编译过程涉及到 JavaCompiler 的一个方法叫做 getTask() . 它有六个参数,返回一个叫做 CompilationTask 内部类的实例:

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

缺省情况下,大部分它的参数可以是 null.
* 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

最后一个参数 compilationUnits 却是不能够为null ,因为它是你要去编译的东西. 它把我们又带回了 StandardJavaFileManager 类.注意这个参数类型: Iterable<? extends JavaFileObject> .   StandardJavaFileManager 有两个方法返回这样的结果. 你可以使用一个文件对象的List或者 String 对象的List,用它们来表示文件名:

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

并不仅仅 List ,实际上,任何一个能够标识需要编译的内容的集合的 Iterable 都可以.  List 出现在这里只是因为它容易生成:

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

 

现在你有了编译源文件的所有的必要的信息. 从 getTask( ) 返回的 JavaCompiler.CompilationTask  实现了 Callable .接口 这样,想让任务开始就去调用call()方法.

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

 

 

如果没有编译警告和错误,这个call() 方法会编译所有的 compilationUnits 变量指定的文件,以及有依赖关系的可编译的文件. 想要知道是否所有的都成功了,去查看一下返回的 Boolean 值. 只有当所有的编译单元都执行成功了,这个 call() 方法才返回 Boolean.TRUE  . 一旦有任何错误,这个方法就会返回 Boolean.FALSE .

在展示运行这个例子之前,让我们添加最后一个东西,DiagnosticListener , 或者更确切的说,  DiagnosticCollector .的实现类.把这个监听器当作getTask()的第三个参数传递进去,你就可以在编译之后进行一些调式信息的查询了.

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));
}
 

在最后,你应该调用管理器的close() 方法.

把所有的放在一起,就得到的了下面的程序,让我们重新编译Hello类.

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);
  }
}
 

 

编译和运行这个程序会输出成功的信息:

> javac BigCompile.java
> java BigCompile
Success: true
 

然而,如果你把 println  方法改成书写错误的 pritnln 方法,当你运行时你会得到下面的信息:

> 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
 

使用Compiler API,你可以实现比在这篇简要的提示介绍的更多的事情. 例如,你可以控制输入输出的目录或者在集成编译器里高亮一些编译错误. 现在,向 Java Compiler API表示感谢,你可以使用标准API了. For more information on the Java Compiler API and JSR 199, see the JSR 199 specification.

你可能感兴趣的:(java)