8.2.2 Java编译器API

使用javac工具的一个问题是其API是Oracle的私有实现,这点从API的包名com.sun.*可以看出来。私有API的接口和实现可能发生变化,从而造成后期维护上的问题。从Java SE 6开始,Java编译器相关的API以JSR 199(JavaTMCompiler API)的形式规范下来,Java编译器API采用了Java平台标准的服务提供者接口的定义方式。编译器API中仅包含接口声明,对应的实现由平台实现者提供。使用者通过工厂方法或服务加载器来查找具体的实现。相关的方法调用都通过接口来完成。Java平台当前的编译器提供了编译器API的默认实现,可以直接使用。通过编译器API可以对编译过程进行更加精细的控制。

先通过与代码清单8-3和代码清单8-4两个示例相同的编译源文件的方式来说明编译器API的基本用法,如代码清单8-5所示。编译器API中的编译器由javax.tools.JavaCompiler接口表示。首先要得到JavaCompiler接口的实现。通过javax.tools.ToolProvider类的getSystemJavaCompiler方法可以得到当前Java平台上默认的编译器实现。一般来说,使用这个默认实现就足够了。在编译器API中对所操作的对象来源进行了抽象,用javax.tools.FileObject接口表示。虽然从名称上看,FileObject接口表示的是文件对象,但是实际上它是一个数据来源的抽象表示,不仅包括磁盘文件,也包括内存中的对象和数据库中的数据等。FileObject接口中的方法主要用来获取数据来源的信息和进行读写操作等。接口javax.tools.JavaFileObject继承自FileObject接口,用来表示Java的源代码和字节代码文件。在编译过程中对Java源代码和字节代码文件的管理是由javax.tools.JavaFileManager接口的实现对象来完成的。JavaFileManager接口提供的方法所操作的目标是抽象的路径。通过JavaFileManager接口不仅可以得到某个路径对应的FileObject接口和JavaFileObject接口的实现对象,还可以根据条件列出某个路径下包含的文件对应的JavaFileObject接口的实现对象。如果Java源代码保存在磁盘上,那么可以使用基于java.io.File接口实现的javax.tools.StandardJavaFileManager接口。通过StandardJavaFileManager接口可以从文件名或File类的对象中创建JavaFileObject接口的实现对象。在一般情况下,使用StandardJavaFileManager接口进行文件管理就足够了。通过JavaCompiler类的对象的getStandardFileManager方法可以得到一个StandardJavaFileManager接口的实现对象。

具体的编译过程是首先调用JavaCompiler类的对象的getTask方法得到一个表示编译任务的JavaCompiler.CompilationTask类的对象,再调用此对象的call方法来执行此任务。创建编译任务的getTask方法的参数比较多,首先是用来输出编译器信息的java.io.Writer类的对象;其次是管理源代码和字节代码文件的JavaFileManager接口的实现对象;接着是处理编译过程中诊断信息的javax.tools.DiagnosticListener接口的实现对象;最后3个参数都是java.lang.Iterable类型的对象,分别表示用来遍历编译时的选项、字节代码文件路径和源文件路径的迭代器。

代码清单8-5 Java编译器API的使用示例


public class JavaCompilerAPICompiler{

public void compile(Path src, Path output)throws IOException{

JavaCompiler compiler=ToolProvider.getSystemJavaCompiler();

try(Standard Java File Manager file Manager=compiler.

getStandardFileManager(null, null, null)){

Iterable<?extends JavaFileObject>compilationUnits=fileManager.getJavaFileObjects(src.toFile());

Iterable<String>options=Arrays.asList("-d",output.toString());

CompilationTask task=compiler.getTask(null, fileManager, null, options, null, compilationUnits);

boolean result=task.call();

}

}

}


编译器API相对于javac工具的一个重要优势在于Java源代码的存在不限于文件形式。JavaFileObject接口的实现对象可以作为源代码的来源。在实现JavaFileObject接口时,比较好的做法是继承已有的javax.tools.SimpleJavaFileObject类,从而减少实现的代价。最常见的需求是允许编译器使用字符串作为源代码的表现形式,免去了使用文件作为中间形式的麻烦。代码清单8-6给出的StringSourceJavaFileObject类表示从字符串创建出的JavaFileObject接口的实现对象,在创建时传入类名和内容即可。

代码清单8-6 字符串形式的Java源代码表示方式


public class StringSourceJavaFileObject extends SimpleJavaFileObject{

private String content;

public StringSourceJavaFileObject(String name, String content){

super(URI.create("string:///"+name.replace('.','/')+Kind.SOURCE.extension),Kind.SOURCE);

this.content=content;

}

public CharSequence getCharContent(boolean ignoreEncodingErrors)throws IOException{

return content;

}

}


使用StringSourceJavaFileObject类之后,可以对任意包含Java源代码的字符串进行编译。这为编译带来了很多灵活性。举例来说,编写一个Java程序来计算带括号的四则运算表达式的值,实现的方式可以有很多。比较传统的做法是对表达式进行语法解析,模拟计算过程来得到结果。如果考虑到括号的存在和操作符之间的优先级顺序等问题,那么采用这种做法的实现并不简单,而且容易出错。另外一种做法是使用第2章介绍的脚本语言支持API,把表达式当成一个JavaScript语句,通过eval方法得到结果。还有一种做法是使用Java编译器API,实现起来也会比较简单。基本的思路是把表达式作为一个Java方法的内容,得到一个Java源文件,编译此源文件之后得到字节代码,再使用类加载器加载字节代码到虚拟机中,最后通过反射API调用其中的方法,得到计算结果。代码清单8-7给出了这种计算方式的完整实现。

代码清单8-7 使用Java编译器API的表达式求值方式


public class Calculator extends ClassLoader{

public double calculate(String expr)throws Exception{

String className="CalculatorMain";

String methodName="calculate";

String source="public class"+className+"{public static double"+methodName+"(){return"+expr+";}}";

JavaCompiler compiler=ToolProvider.getSystemJavaCompiler();

StandardJavaFileManager fileManager=compiler.getStandardFileManager(null, null, null);

JavaFileObject sourceObject=new StringSourceJavaFileObject(className, source);

Iterable<?extends Java File Object>file Objects=Arrays.asList(sourceObject);

Path output=Files.createTempDirectory("calculator");

Iterable<String>options=Arrays.asList("-d",output.toString());

CompilationTask task=compiler.getTask(null, fileManager, null, options, null, fileObjects);

boolean result=task.call();

if(result){

byte[]classData=Files.readAllBytes(Paths.get(output.toString(),className+".class"));

Class<?>clazz=defineClass(className, classData,0,classData.length);

Method method=clazz.getMethod(methodName);

Object value=method.invoke(null);

return(Double)value;

}

else{

throw new Exception("无法识别的表达式。");

}

}

}


在代码清单8-7中,先根据传入的表达式的内容创建Java源代码。比如,表达式的内容是“(3+2)*5”,所得到的Java源代码的内容如代码清单8-8所示。只需要编译其中的Java类,并通过反射API调用其calculate方法,就可以得到表达式的计算结果。

代码清单8-8 动态生成的Java源代码的内容示例


public class CalculatorMain{

public static double calculate(){

return(3+2)*5;

}

}


调用编译器API的方式与代码清单8-5很相似,区别在于不是通过StandardJava-FileManager接口的实现对象从文件路径中得到JavaFileObject接口的实现对象,而是使用StringSourceJavaFileObject类的对象作为编译器的源文件来源。编译之后的字节代码保存在磁盘上的临时目录中。编译成功之后,从class文件中得到字节代码的内容,再使用类加载器从字节代码中定义出Java类。得到Java类之后,通过反射API来调用类中包含的calculate方法,得到表达式的计算结果。