年底Sun 公司发布了 Java Standard Edition (Java SE )的最终正式版代号 Mustang(野马)跟 Tiger(Java SE )相比Mustang 在性能方面有了不错的提升与 Tiger 在 API 库方面的大幅度加强相比虽然 Mustang 在 API 库方面的新特性显得不太多但是也提供了许多实用和方便的功能在脚本WebServiceXML编译器 API数据库JMX网络和 Instrumentation 方面都有不错的新特性和功能加强 本系列 文章主要介绍 Java SE 在 API 库方面的部分新特性通过一些例子和讲解帮助开发者在编程实践当中更好的运用 Java SE 提高开发效率
本文是其中的第四篇介绍了 JDK 中为在运行时操纵编译器所增加的编译器 API(JSR )您将了解到利用此 API 开发人员可以在运行时调用 Java 编译器还可以编译非文本形式的 Java 源代码最后还能够采集编译器的诊断信息本文将展开描述这些功能并使用这些功能构造一个简单的应用 —— 在内存中直接为一个类生成测试用例
新API 功能简介
JDK 提供了在运行时调用编译器的 API后面我们将假设把此 API 应用在 JSP 技术中在传统的 JSP 技术中服务器处理 JSP 通常需要进行下面 个步骤
分析 JSP 代码
生成 Java 代码
将 Java 代码写入存储器
启动另外一个进程并运行编译器编译 Java 代码
将类文件写入存储器
服务器读入类文件并运行
但如果采用运行时编译可以同时简化步骤 和 节约新进程的开销和写入存储器的输出开销提高系统效率实际上在 JDK 中Sun 也提供了调用编译器的编程接口然而不同的是老版本的编程接口并不是标准 API 的一部分而是作为 Sun 的专有实现提供的而新版则带来了标准化的优点
新API 的第二个新特性是可以编译抽象文件理论上是任何形式的对象 —— 只要该对象实现了特定的接口有了这个特性上述例子中的步骤 也可以省略整个 JSP 的编译运行在一个进程中完成同时消除额外的输入输出操作
第三个新特性是可以收集编译时的诊断信息作为对前两个新特性的补充它可以使开发人员轻松的输出必要的编译错误或者是警告信息从而省去了很多重定向的麻烦
运行时编译 Java 文件
在 JDK 中类库通过 javaxtools 包提供了程序运行时调用编译器的 API从这个包的名字 tools 可以看出这个开发包提供的功能并不仅仅限于编译器工具还包括 javahjarpack 等它们都是 JDK 提供的命令行工具这个开发包希望通过实现一个统一的接口可以在运行时调用这些工具在 JDK 中编译器被给予了特别的重视针对编译器JDK 设计了两个接口分别是 JavaCompiler 和 JavaCompilerCompilationTask
下面给出一个例子展示如何在运行时调用编译器
指定编译文件名称(该文件必须在 CLASSPATH 中可以找到)String fullQuanlifiedFileName = compile + javaioFileseparator +Targetjava;
获得编译器对象 JavaCompiler compiler = ToolProvidergetSystemJavaCompiler();
通过调用 ToolProvider 的 getSystemJavaCompiler 方法JDK 提供了将当前平台的编译器映射到内存中的一个对象这样使用者可以在运行时操纵编译器JavaCompiler 是一个接口它继承了 javaxtoolsTool 接口因此第三方实现的编译器只要符合规范就能通过统一的接口调用同时tools 开发包希望对所有的工具提供统一的运行时调用接口相信将来ToolProvider 类将会为更多地工具提供 getSystemXXXTool 方法tools 开发包实际为多种不同工具不同实现的共存提供了框架
编译文件int result = compilerrun(null null null fileToCompile);
获得编译器对象之后可以调用 Toolrun 方法对源文件进行编译Run 方法的前三个参数分别可以用来重定向标准输入标准输出和标准错误输出null 值表示使用默认值清单 给出了一个完整的例子
清单 程序运行时编译文件
package compile;
import javautilDate;
public class Target {
public void doSomething(){
Date date = new Date( );
// 这个构造函数被标记为deprecated 编译时会
// 向错误输出输出信息
Systemoutprintln(Doing);
}
}
package compile;
import javaxtools*;
import javaioFileOutputStream;
public class Compiler {
public static void main(String[] args) throws Exception{
String fullQuanlifiedFileName = compile + javaioFileseparator +
Targetjava;
JavaCompiler compiler = ToolProvidergetSystemJavaCompiler();
FileOutputStream err = new FileOutputStream(errtxt);
int compilationResult = compilerrun(null null err fullQuanlifiedFileName);
if(compilationResult == ){
Systemoutprintln(Done);
} else {
Systemoutprintln(Fail);
}
}
}
首 先运行 <JDK_INSTALLATION_DIR>\bin\javac Compilerjava然后运行 <JDK_INSTALLATION_DIR>\jdk\bin\java compileCompiler屏幕上将输出 Done 并会在当前目录生成一个 errtxt 文件文件内容如下
Note: compile/Targetjava uses or overrides a deprecated API
Note: Recompile with Xlint:deprecation for details
仔细观察 run 方法可以发现最后一个参数是 Stringarguments是一个变长的字符串数组它的实际作用是接受传递给 javac 的参数假设要编译 Targetjava 文件并显示编译过程中的详细信息命令行为javac Targetjava verbose相应的可以将 句改为
int compilationResult = compilerrun(null null err verbosefullQuanlifiedFileName);
编译非文本形式的文件JDK 的编译器 API 的另外一个强大之处在于它可以编译的源文件的形式并不局限于文本文件JavaCompiler 类依靠文件管理服务可以编译多种形式的源文件比如直接由内存中的字符串构造的文件或者是从数据库中取出的文件这种服务是由 JavaFileManager 类提供的通常的编译过程分为以下几个步骤
解析 javac 的参数
在 source path 和/或 CLASSPATH 中查找源文件或者 jar 包
处理输入输出文件
在这个过程中JavaFileManager 类可以起到创建输出文件读入并缓存输出文件的作用由于它可以读入并缓存输入文件这就使得读入各种形式的输入文件成为可能JDK 提供的命令行工具处理机制也大致相似在未来的版本中其它的工具处理各种形式的源文件也成为可能为此新的 JDK 定义了 javaxtoolsFileObject 和 javaxtoolsJavaFileObject 接口任何类只要实现了这个接口就可以被 JavaFileManager 识别
如果要使用 JavaFileManager就必须构造 CompilationTaskJDK 提供了 JavaCompilerCompilationTask 类来封装一个编译操作这个类可以通过
JavaCompilergetTask (
Writer out
JavaFileManager fileManager
DiagnosticListener<? super JavaFileObject> diagnosticListener
Iterable<String> options
Iterable<String> classes
Iterable<? extends JavaFileObject> compilationUnits
)
方法得到关于每个参数的含义请参见 JDK 文档传递不同的参数会得到不同的 CompilationTask通过构造这个类一个编译过程可以被分成多步进一步CompilationTask 提供了 setProcessors(Iterable<? extends Processor>processors) 方法用户可以制定处理 annotation 的处理器图 展示了通过 CompilationTask 进行编译的过程
图 使用 CompilationTask 进行编译
下面的例子通过构造 CompilationTask 分多步编译一组 Java 源文件
清单 构造 CompilationTask 进行编译
package math;
public class Calculator {
public int multiply(int multiplicand int multiplier) {
return multiplicand * multiplier;
}
}
package compile;
import javaxtools*;
import javaioFileOutputStream;
import javautilArrays;
public class Compiler {
public static void main(String[] args) throws Exception{
String fullQuanlifiedFileName = math + javaioFileseparator +Calculatorjava;
JavaCompiler compiler = ToolProvidergetSystemJavaCompiler();
StandardJavaFileManager fileManager =
compilergetStandardFileManager(null null null);
Iterable<? extends JavaFileObject> files =
fileManagergetJavaFileObjectsFromStrings(
ArraysasList(fullQuanlifiedFileName));
JavaCompilerCompilationTask task = compilergetTask(
null fileManager null null null files);
Boolean result = taskcall();
if( result == true ) {
Systemoutprintln(Succeeded);
}
}
}
以上是第一步通过构造一个 CompilationTask 编译了一个 Java 文件 行实现了主要逻辑第 行首先取得一个编译器对象由于仅仅需要编译普通文件因此第 行中通过编译器对象取得了一个标准文件管理器 行将需要编译的文件构造成了一个 Iterable 对象最后将文件管理器和 Iterable 对象传递给 JavaCompiler 的 getTask 方法取得了 JavaCompilerCompilationTask 对象
接下来第二步开发者希望生成 Calculator 的一个测试类而不是手工编写使用 compiler API可以将内存中的一段字符串编译成一个 CLASS 文件
清单 定制 JavaFileObject 对象
package math;
import URI;
public class StringObject extends SimpleJavaFileObject{
private String contents = null;
public StringObject(String className String contents) throws Exception{
super(new URI(className) KindSOURCE);
ntents = contents;
}
public CharSequence getCharContent(boolean ignoreEncodingErrors)
throws IOException {
return contents;
}
}
SimpleJavaFileObject 是 JavaFileObject 的子类它提供了默认的实现继承 SimpleJavaObject 之后只需要实现 getCharContent 方法如 清单 中的 行所示接下来在内存中构造 Calculator 的测试类 CalculatorTest并将代表该类的字符串放置到 StringObject 中传递给 JavaCompiler 的 getTask 方法清单 展现了这些步骤
清单 编译非文本形式的源文件
package math;
import javaxtools*;
import javaioFileOutputStream;
import javautilArrays;
public class AdvancedCompiler {
public static void main(String[] args) throws Exception{
// Steps used to compile Calculator
// Steps used to compile StringObject
// construct CalculatorTest in memory
JavaCompiler compiler = ToolProvidergetSystemJavaCompiler();
StandardJavaFileManager fileManager =
compilergetStandardFileManager(null null null);
JavaFileObject file = constructTestor();
Iterable<? extends JavaFileObject> files = ArraysasList(file);
JavaCompilerCompilationTask task = compilergetTask (
null fileManager null null null files);
Boolean result = taskcall();
if( result == true ) {
Systemoutprintln(Succeeded);
}
}
private static SimpleJavaFileObject constructTestor() {
StringBuilder contents = new StringBuilder(
package math; +
class CalculatorTest {\n +
public void testMultiply() {\n +
Calculator c = new Calculator();\n +
Systemoutprintln(cmultiply( ));\n +
}\n +
public static void main(String[] args) {\n +
CalculatorTest ct = new CalculatorTest();\n +
cttestMultiply();\n +
}\n +
}\n);
StringObject so = null;
try {
so = new StringObject(mathCalculatorTest contentstoString());
} catch(Exception exception) {
exceptionprintStackTrace();
}
return so;
}
}
实现逻辑和 清单 相似不同的是在 行程序在内存中构造了 CalculatorTest 类并且通过 StringObject 的构造函数将内存中的字符串转换成了 JavaFileObject 对象
采集编译器的诊断信息
第三个新增加的功能是收集编译过程中的诊断信息诊断信息通常指错误警告或是编译过程中的详尽输出JDK 通过 Listener 机制获取这些信息如果要注册一个 DiagnosticListener必须使用 CompilationTask 来进行编译因为 Tool 的 run 方法没有办法注册 Listener步骤很简单先构造一个 Listener然后传递给 JavaFileManager 的构造函数清单 对 清单 进行了改动展示了如何注册一个 DiagnosticListener
清单 注册一个 DiagnosticListener 收集编译信息
package math;
public class Calculator {
public int multiply(int multiplicand int multiplier) {
return multiplicand * multiplier
// deliberately omit semicolon ADiagnosticListener
// will take effect
}
}
package compile;
import javaxtools*;
import javaioFileOutputStream;
import javautilArrays;
public class CompilerWithListener {
public static void main(String[] args) throws Exception{
String fullQuanlifiedFileName = math +
javaioFileseparator +Calculatorjava;
JavaCompiler compiler = ToolProvidergetSystemJavaCompiler();
StandardJavaFileManager fileManager =
compilergetStandardFileManager(null null null);
Iterable<? extends JavaFileObject> files =
fileManagergetJavaFileObjectsFromStrings(
ArraysasList(fullQuanlifiedFileName));
DiagnosticCollector<JavaFileObject> collector =
new DiagnosticCollector<JavaFileObject>();
JavaCompilerCompilationTask task =
compilergetTask(null fileManager collector null null files);
Boolean result = taskcall();
List<Diagnostic<? extends JavaFileObject>> diagnostics =
collectorgetDiagnostics();
for(Diagnostic<? extends JavaFileObject> d : diagnostics){
Systemoutprintln(Line Number> + dgetLineNumber());
Systemoutprintln(Message>+
dgetMessage(LocaleENGLISH));
Systemoutprintln(Source + dgetCode());
Systemoutprintln(\n);
}
if( result == true ) {
Systemoutprintln(Succeeded);
}
}
}
在 行构造了一个 DiagnosticCollector 对象这个对象由 JDK 提供它实现了 DiagnosticListener 接口 行将它注册到 CompilationTask 中去一个编译过程可能有多个诊断信息每一个诊断信息被抽象为一个 Diagnostic 行将所有的诊断信息逐个输出编译并运行 Compiler得到以下输出
清单 DiagnosticCollector 收集的编译信息
Line Number>
Message>math/Calculatorjava:: ; expected
Source>compilererrexpected
实际上也可以由用户自己定制清单 给出了一个定制的 Listener
清单 自定义的 DiagnosticListener
class ADiagnosticListener implements DiagnosticListener<JavaFileObject>{
public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
Systemoutprintln(Line Number> + diagnosticgetLineNumber());
Systemoutprintln(Message>+ diagnosticgetMessage(LocaleENGLISH));
Systemoutprintln(Source + diagnosticgetCode());
Systemoutprintln(\n);
}
}
总结
JDK 的编译器新特性使得开发者可以更自如的控制编译的过程这给了工具开发者更加灵活的自由度通过 API 的调用完成编译操作的特性使得开发者可以更方便高效地将编译变为软件系统运行时的服务而编译更广泛形式的源代码则为整合更多的数据源及功能提供了 强大的支持相信随着 JDK 的不断完善更多的工具将具有 API 支持我们拭目以待