Java SE 6 新特性: 编译器 API

新 API 功能简介

JDK 6 提供了在运行时调用编译器的 API,后面我们将假设把此 API 应用在 JSP 技术中。在传统的 JSP 技术中,服务器处理 JSP 通常需要进行下面 6 个步骤:

  1. 分析 JSP 代码;
  2. 生成 Java 代码;
  3. 将 Java 代码写入存储器;
  4. 启动另外一个进程并运行编译器编译 Java 代码;
  5. 将类文件写入存储器;
  6. 服务器读入类文件并运行;

但如果采用运行时编译,可以同时简化步骤 4 和 5,节约新进程的开销和写入存储器的输出开销,提高系统效率。实际上,在 JDK 5 中,Sun 也提供了调用编译器的编程接口。然而不同的是,老版本的编程接口并不是标准 API 的一部分,而是作为 Sun 的专有实现提供的,而新版则带来了标准化的优点。

新 API 的第二个新特性是可以编译抽象文件,理论上是任何形式的对象 —— 只要该对象实现了特定的接口。有了这个特性,上述例子中的步骤 3 也可以省略。整个 JSP 的编译运行在一个进程中完成,同时消除额外的输入输出操作。

第三个新特性是可以收集编译时的诊断信息。作为对前两个新特性的补充,它可以使开发人员轻松的输出必要的编译错误或者是警告信息,从而省去了很多重定向的麻烦。

运行时编译 Java 文件

在 JDK 6 中,类库通过 javax.tools 包提供了程序运行时调用编译器的 API。从这个包的名字 tools 可以看出,这个开发包提供的功能并不仅仅限于编译器。工具还包括 javah、jar、pack200 等,它们都是 JDK 提供的命令行工具。这个开发包希望通过实现一个统一的接口,可以在运行时调用这些工具。在 JDK 6 中,编译器被给予了特别的重视。针对编译器,JDK 设计了两个接口,分别是 JavaCompilerJavaCompiler.CompilationTask

下面给出一个例子,展示如何在运行时调用编译器。

  • 指定编译文件名称(该文件必须在 CLASSPATH 中可以找到):String fullQuanlifiedFileName = "compile" + java.io.File.separator +"Target.java";
  • 获得编译器对象: JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

通过调用 ToolProvidergetSystemJavaCompiler 方法,JDK 提供了将当前平台的编译器映射到内存中的一个对象。这样使用者可以在运行时操纵编译器。JavaCompiler 是一个接口,它继承了 javax.tools.Tool 接口。因此,第三方实现的编译器,只要符合规范就能通过统一的接口调用。同时,tools 开发包希望对所有的工具提供统一的运行时调用接口。相信将来,ToolProvider 类将会为更多地工具提供 getSystemXXXTool 方法。tools 开发包实际为多种不同工具、不同实现的共存提供了框架。

  • 编译文件:int result = compiler.run(null, null, null, fileToCompile);

获得编译器对象之后,可以调用 Tool.run 方法对源文件进行编译。Run 方法的前三个参数,分别可以用来重定向标准输入、标准输出和标准错误输出,null 值表示使用默认值。清单 1 给出了一个完整的例子:


清单 1. 程序运行时编译文件

Java SE 6 新特性: 编译器 API01packagecompile;
Java SE 6 新特性: 编译器 API
02importjava.util.Date;
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
03publicclassTarget...{
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
04publicvoiddoSomething()...{
Java SE 6 新特性: 编译器 API
05Datedate=newDate(10,3,3);
Java SE 6 新特性: 编译器 API
//这个构造函数被标记为deprecated,编译时会
Java SE 6 新特性: 编译器 API
//向错误输出输出信息。
Java SE 6 新特性: 编译器 API
06System.out.println("Doing...");
Java SE 6 新特性: 编译器 API
07}

Java SE 6 新特性: 编译器 API
08}

Java SE 6 新特性: 编译器 API
Java SE 6 新特性: 编译器 API
09packagecompile;
Java SE 6 新特性: 编译器 API
10importjavax.tools.*;
Java SE 6 新特性: 编译器 API
11importjava.io.FileOutputStream;
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
12publicclassCompiler...{
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
13publicstaticvoidmain(String[]args)throwsException...{
Java SE 6 新特性: 编译器 API
14StringfullQuanlifiedFileName="compile"+java.io.File.separator+
Java SE 6 新特性: 编译器 API
"Target.java";
Java SE 6 新特性: 编译器 API
15JavaCompilercompiler=ToolProvider.getSystemJavaCompiler();
Java SE 6 新特性: 编译器 API
Java SE 6 新特性: 编译器 API
16FileOutputStreamerr=newFileOutputStream("err.txt");
Java SE 6 新特性: 编译器 API
Java SE 6 新特性: 编译器 API
17intcompilationResult=compiler.run(null,null,err,fullQuanlifiedFileName);
Java SE 6 新特性: 编译器 API
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
18if(compilationResult==0)...{
Java SE 6 新特性: 编译器 API
19System.out.println("Done");
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
20}
else...{
Java SE 6 新特性: 编译器 API
21System.out.println("Fail");
Java SE 6 新特性: 编译器 API
22}

Java SE 6 新特性: 编译器 API
23}

Java SE 6 新特性: 编译器 API
24}

首先运行 <JDK60_INSTALLATION_DIR>/bin/javac Compiler.java,然后运行 <JDK60_INSTALLATION_DIR>/jdk1.6.0/bin/java compile.Compiler。屏幕上将输出 Done ,并会在当前目录生成一个 err.txt 文件,文件内容如下:

Java SE 6 新特性: 编译器 APINote:compile/Target.javausesoroverridesadeprecatedAPI.
Java SE 6 新特性: 编译器 APINote:Recompilewith-Xlint
:deprecationfordetails.

仔细观察 run 方法,可以发现最后一个参数是 String...arguments,是一个变长的字符串数组。它的实际作用是接受传递给 javac 的参数。假设要编译 Target.java 文件,并显示编译过程中的详细信息。命令行为:javac Target.java -verbose。相应的可以将 17 句改为:

Java SE 6 新特性: 编译器 APIintcompilationResult=compiler.run(null,null,err,“-verbose”,fullQuanlifiedFileName);

编译非文本形式的文件

JDK 6 的编译器 API 的另外一个强大之处在于,它可以编译的源文件的形式并不局限于文本文件。JavaCompiler 类依靠文件管理服务可以编译多种形式的源文件。比如直接由内存中的字符串构造的文件,或者是从数据库中取出的文件。这种服务是由 JavaFileManager 类提供的。通常的编译过程分为以下几个步骤:

  1. 解析 javac 的参数;
  2. 在 source path 和/或 CLASSPATH 中查找源文件或者 jar 包;
  3. 处理输入,输出文件;

在这个过程中,JavaFileManager 类可以起到创建输出文件,读入并缓存输出文件的作用。由于它可以读入并缓存输入文件,这就使得读入各种形式的输入文件成为可能。JDK 提供的命令行工具,处理机制也大致相似,在未来的版本中,其它的工具处理各种形式的源文件也成为可能。为此,新的 JDK 定义了 javax.tools.FileObjectjavax.tools.JavaFileObject 接口。任何类,只要实现了这个接口,就可以被 JavaFileManager 识别。

如果要使用 JavaFileManager,就必须构造 CompilationTask。JDK 6 提供了 JavaCompiler.CompilationTask 类来封装一个编译操作。这个类可以通过:

Java SE 6 新特性: 编译器 APIJavaCompiler.getTask(
Java SE 6 新特性: 编译器 APIWriterout,
Java SE 6 新特性: 编译器 APIJavaFileManagerfileManager,
Java SE 6 新特性: 编译器 APIDiagnosticListener
<?superJavaFileObject>diagnosticListener,
Java SE 6 新特性: 编译器 APIIterable
<String>options,
Java SE 6 新特性: 编译器 APIIterable
<String>classes,
Java SE 6 新特性: 编译器 APIIterable
<?extendsJavaFileObject>compilationUnits
Java SE 6 新特性: 编译器 API)

方法得到。关于每个参数的含义,请参见 JDK 文档。传递不同的参数,会得到不同的 CompilationTask。通过构造这个类,一个编译过程可以被分成多步。进一步,CompilationTask 提供了 setProcessors(Iterable<? extends Processor>processors) 方法,用户可以制定处理 annotation 的处理器。图 1 展示了通过 CompilationTask 进行编译的过程:


图 1. 使用 CompilationTask 进行编译
Java SE 6 新特性: 编译器 API

下面的例子通过构造 CompilationTask 分多步编译一组 Java 源文件。


清单 2. 构造 CompilationTask 进行编译

Java SE 6 新特性: 编译器 API01packagemath;
Java SE 6 新特性: 编译器 API
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
02publicclassCalculator...{
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
03publicintmultiply(intmultiplicand,intmultiplier)...{
Java SE 6 新特性: 编译器 API
04returnmultiplicand*multiplier;
Java SE 6 新特性: 编译器 API
05}

Java SE 6 新特性: 编译器 API
06}

Java SE 6 新特性: 编译器 API
Java SE 6 新特性: 编译器 API
07packagecompile;
Java SE 6 新特性: 编译器 API
08importjavax.tools.*;
Java SE 6 新特性: 编译器 API
09importjava.io.FileOutputStream;
Java SE 6 新特性: 编译器 API
10importjava.util.Arrays;
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
11publicclassCompiler...{
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
12publicstaticvoidmain(String[]args)throwsException...{
Java SE 6 新特性: 编译器 API
13StringfullQuanlifiedFileName="math"+java.io.File.separator+"Calculator.java";
Java SE 6 新特性: 编译器 API
14JavaCompilercompiler=ToolProvider.getSystemJavaCompiler();
Java SE 6 新特性: 编译器 API
15StandardJavaFileManagerfileManager=
Java SE 6 新特性: 编译器 APIcompiler.getStandardFileManager(
null,null,null);
Java SE 6 新特性: 编译器 API
Java SE 6 新特性: 编译器 API
16Iterable<?extendsJavaFileObject>files=
Java SE 6 新特性: 编译器 APIfileManager.getJavaFileObjectsFromStrings(
Java SE 6 新特性: 编译器 APIArrays.asList(fullQuanlifiedFileName));
Java SE 6 新特性: 编译器 API
17JavaCompiler.CompilationTasktask=compiler.getTask(
Java SE 6 新特性: 编译器 API
null,fileManager,null,null,null,files);
Java SE 6 新特性: 编译器 API
Java SE 6 新特性: 编译器 API
18Booleanresult=task.call();
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
19if(result==true)...{
Java SE 6 新特性: 编译器 API
20System.out.println("Succeeded");
Java SE 6 新特性: 编译器 API
21}

Java SE 6 新特性: 编译器 API
22}

Java SE 6 新特性: 编译器 API
23}

以上是第一步,通过构造一个 CompilationTask 编译了一个 Java 文件。14-17 行实现了主要逻辑。第 14 行,首先取得一个编译器对象。由于仅仅需要编译普通文件,因此第 15 行中通过编译器对象取得了一个标准文件管理器。16 行,将需要编译的文件构造成了一个 Iterable 对象。最后将文件管理器和 Iterable 对象传递给 JavaCompilergetTask 方法,取得了 JavaCompiler.CompilationTask 对象。

接下来第二步,开发者希望生成 Calculator 的一个测试类,而不是手工编写。使用 compiler API,可以将内存中的一段字符串,编译成一个 CLASS 文件。


清单 3. 定制 JavaFileObject 对象

Java SE 6 新特性: 编译器 API01packagemath;
Java SE 6 新特性: 编译器 API
02importjava.net.URI;
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
03publicclassStringObjectextendsSimpleJavaFileObject...{
Java SE 6 新特性: 编译器 API
04privateStringcontents=null;
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
05publicStringObject(StringclassName,Stringcontents)throwsException...{
Java SE 6 新特性: 编译器 API
06super(newURI(className),Kind.SOURCE);
Java SE 6 新特性: 编译器 API
07this.contents=contents;
Java SE 6 新特性: 编译器 API
08}

Java SE 6 新特性: 编译器 API
Java SE 6 新特性: 编译器 API
09publicCharSequencegetCharContent(booleanignoreEncodingErrors)
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
throwsIOException...{
Java SE 6 新特性: 编译器 API
10returncontents;
Java SE 6 新特性: 编译器 API
11}

Java SE 6 新特性: 编译器 API
12}

SimpleJavaFileObjectJavaFileObject 的子类,它提供了默认的实现。继承 SimpleJavaObject 之后,只需要实现 getCharContent 方法。如 清单 3 中的 9-11 行所示。接下来,在内存中构造 Calculator 的测试类 CalculatorTest,并将代表该类的字符串放置到 StringObject 中,传递给 JavaCompilergetTask 方法。清单 4 展现了这些步骤。


清单 4. 编译非文本形式的源文件

Java SE 6 新特性: 编译器 API01packagemath;
Java SE 6 新特性: 编译器 API
02importjavax.tools.*;
Java SE 6 新特性: 编译器 API
03importjava.io.FileOutputStream;
Java SE 6 新特性: 编译器 API
04importjava.util.Arrays;
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
05publicclassAdvancedCompiler...{
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
06publicstaticvoidmain(String[]args)throwsException...{
Java SE 6 新特性: 编译器 API
Java SE 6 新特性: 编译器 API
07//StepsusedtocompileCalculator
Java SE 6 新特性: 编译器 API
08//StepsusedtocompileStringObject
Java SE 6 新特性: 编译器 API

Java SE 6 新特性: 编译器 API
09//constructCalculatorTestinmemory
Java SE 6 新特性: 编译器 API
10JavaCompilercompiler=ToolProvider.getSystemJavaCompiler();
Java SE 6 新特性: 编译器 API
11StandardJavaFileManagerfileManager=
Java SE 6 新特性: 编译器 APIcompiler.getStandardFileManager(
null,null,null);
Java SE 6 新特性: 编译器 API
12JavaFileObjectfile=constructTestor();
Java SE 6 新特性: 编译器 API
13Iterable<?extendsJavaFileObject>files=Arrays.asList(file);
Java SE 6 新特性: 编译器 API
14JavaCompiler.CompilationTasktask=compiler.getTask(
Java SE 6 新特性: 编译器 API
null,fileManager,null,null,null,files);
Java SE 6 新特性: 编译器 API
Java SE 6 新特性: 编译器 API
15Booleanresult=task.call();
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
16if(result==true)...{
Java SE 6 新特性: 编译器 API
17System.out.println("Succeeded");
Java SE 6 新特性: 编译器 API
18}

Java SE 6 新特性: 编译器 API
19}

Java SE 6 新特性: 编译器 API
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
20privatestaticSimpleJavaFileObjectconstructTestor()...{
Java SE 6 新特性: 编译器 API
21StringBuildercontents=newStringBuilder(
Java SE 6 新特性: 编译器 API
"packagemath;"+
Java SE 6 新特性: 编译器 API
"classCalculatorTest{ "+
Java SE 6 新特性: 编译器 API
"publicvoidtestMultiply(){ "+
Java SE 6 新特性: 编译器 API
"Calculatorc=newCalculator(); "+
Java SE 6 新特性: 编译器 API
"System.out.println(c.multiply(2,4)); "+
Java SE 6 新特性: 编译器 API
"} "+
Java SE 6 新特性: 编译器 API
"publicstaticvoidmain(String[]args){ "+
Java SE 6 新特性: 编译器 API
"CalculatorTestct=newCalculatorTest(); "+
Java SE 6 新特性: 编译器 API
"ct.testMultiply(); "+
Java SE 6 新特性: 编译器 API
"} "+
Java SE 6 新特性: 编译器 API
"} ");
Java SE 6 新特性: 编译器 API
22StringObjectso=null;
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
23try...{
Java SE 6 新特性: 编译器 API
24so=newStringObject("math.CalculatorTest",contents.toString());
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
25}
catch(Exceptionexception)...{
Java SE 6 新特性: 编译器 API
26exception.printStackTrace();
Java SE 6 新特性: 编译器 API
27}

Java SE 6 新特性: 编译器 API
28returnso;
Java SE 6 新特性: 编译器 API
29}

Java SE 6 新特性: 编译器 API
30}

实现逻辑和 清单 2 相似。不同的是在 20-30 行,程序在内存中构造了 CalculatorTest 类,并且通过 StringObject 的构造函数,将内存中的字符串,转换成了 JavaFileObject 对象。

采集编译器的诊断信息

第三个新增加的功能,是收集编译过程中的诊断信息。诊断信息,通常指错误、警告或是编译过程中的详尽输出。JDK 6 通过 Listener 机制,获取这些信息。如果要注册一个 DiagnosticListener,必须使用 CompilationTask 来进行编译,因为 Tool 的 run 方法没有办法注册 Listener。步骤很简单,先构造一个 Listener,然后传递给 JavaFileManager 的构造函数。清单 5清单 2 进行了改动,展示了如何注册一个 DiagnosticListener


清单 5. 注册一个 DiagnosticListener 收集编译信息

Java SE 6 新特性: 编译器 API01packagemath;
Java SE 6 新特性: 编译器 API
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
02publicclassCalculator...{
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
03publicintmultiply(intmultiplicand,intmultiplier)...{
Java SE 6 新特性: 编译器 API
04returnmultiplicand*multiplier
Java SE 6 新特性: 编译器 API
//deliberatelyomitsemicolon,ADiagnosticListener
Java SE 6 新特性: 编译器 API
//willtakeeffect
Java SE 6 新特性: 编译器 API
05}

Java SE 6 新特性: 编译器 API
06}

Java SE 6 新特性: 编译器 API
Java SE 6 新特性: 编译器 API
07packagecompile;
Java SE 6 新特性: 编译器 API
08importjavax.tools.*;
Java SE 6 新特性: 编译器 API
09importjava.io.FileOutputStream;
Java SE 6 新特性: 编译器 API
10importjava.util.Arrays;
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
11publicclassCompilerWithListener...{
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
12publicstaticvoidmain(String[]args)throwsException...{
Java SE 6 新特性: 编译器 API
13StringfullQuanlifiedFileName="math"+
Java SE 6 新特性: 编译器 APIjava.io.File.separator
+"Calculator.java";
Java SE 6 新特性: 编译器 API
14JavaCompilercompiler=ToolProvider.getSystemJavaCompiler();
Java SE 6 新特性: 编译器 API
15StandardJavaFileManagerfileManager=
Java SE 6 新特性: 编译器 APIcompiler.getStandardFileManager(
null,null,null);
Java SE 6 新特性: 编译器 API
Java SE 6 新特性: 编译器 API
16Iterable<?extendsJavaFileObject>files=
Java SE 6 新特性: 编译器 APIfileManager.getJavaFileObjectsFromStrings(
Java SE 6 新特性: 编译器 APIArrays.asList(fullQuanlifiedFileName));
Java SE 6 新特性: 编译器 API
17DiagnosticCollector<JavaFileObject>collector=
Java SE 6 新特性: 编译器 API
newDiagnosticCollector<JavaFileObject>();
Java SE 6 新特性: 编译器 API
18JavaCompiler.CompilationTasktask=
Java SE 6 新特性: 编译器 APIcompiler.getTask(
null,fileManager,collector,null,null,files);
Java SE 6 新特性: 编译器 API
Java SE 6 新特性: 编译器 API
19Booleanresult=task.call();
Java SE 6 新特性: 编译器 API
20List<Diagnostic<?extendsJavaFileObject>>diagnostics=
Java SE 6 新特性: 编译器 APIcollector.getDiagnostics();
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
21for(Diagnostic<?extendsJavaFileObject>d:diagnostics)...{
Java SE 6 新特性: 编译器 API
22System.out.println("LineNumber->"+d.getLineNumber());
Java SE 6 新特性: 编译器 API
23System.out.println("Message->"+
Java SE 6 新特性: 编译器 APId.getMessage(Locale.ENGLISH));
Java SE 6 新特性: 编译器 API
24System.out.println("Source"+d.getCode());
Java SE 6 新特性: 编译器 API
25System.out.println(" ");
Java SE 6 新特性: 编译器 API
26}

Java SE 6 新特性: 编译器 API
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
27if(result==true)...{
Java SE 6 新特性: 编译器 API
28System.out.println("Succeeded");
Java SE 6 新特性: 编译器 API
29}

Java SE 6 新特性: 编译器 API
30}

Java SE 6 新特性: 编译器 API
31}

在 17 行,构造了一个 DiagnosticCollector 对象,这个对象由 JDK 提供,它实现了 DiagnosticListener 接口。18 行将它注册到 CompilationTask 中去。一个编译过程可能有多个诊断信息。每一个诊断信息,被抽象为一个 Diagnostic。20-26 行,将所有的诊断信息逐个输出。编译并运行 Compiler,得到以下输出:


清单 6. DiagnosticCollector 收集的编译信息

Java SE 6 新特性: 编译器 APILineNumber->5
Java SE 6 新特性: 编译器 APIMessage-
>math/Calculator.java:5:';'expected
Java SE 6 新特性: 编译器 APISource-
>compiler.err.expected

实际上,也可以由用户自己定制。清单 7 给出了一个定制的 Listener
清单 7. 自定义的 DiagnosticListener

Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API01classADiagnosticListenerimplementsDiagnosticListener<JavaFileObject>...{
Java SE 6 新特性: 编译器 APIJava SE 6 新特性: 编译器 API
02publicvoidreport(Diagnostic<?extendsJavaFileObject>diagnostic)...{
Java SE 6 新特性: 编译器 API
03System.out.println("LineNumber->"+diagnostic.getLineNumber());
Java SE 6 新特性: 编译器 API
04System.out.println("Message->"+diagnostic.getMessage(Locale.ENGLISH));
Java SE 6 新特性: 编译器 API
05System.out.println("Source"+diagnostic.getCode());
Java SE 6 新特性: 编译器 API
06System.out.println(" ");
Java SE 6 新特性: 编译器 API
07}

Java SE 6 新特性: 编译器 API
08}

总结

JDK 6 的编译器新特性,使得开发者可以更自如的控制编译的过程,这给了工具开发者更加灵活的自由度。通过 API 的调用完成编译操作的特性,使得开发者可以更方便、高效地将编译变为软件系统运行时的服务。而编译更广泛形式的源代码,则为整合更多的数据源及功能提供了强大的支持。相信随着 JDK 的不断完善,更多的工具将具有 API 支持,我们拭目以待。



参考资料