一.運行時編譯java文件
在 JDK 6 中,類庫通過 javax.tools
包提供了程序運行時調用編譯器的 API。從這個包的名字 tools 可以看出,這個開發包提供的功能並不僅僅限於編譯器。工具還包括 javah、jar、pack200 等,它們都是 JDK 提供的命令行工具。這個開發包希望通過實現一個統一的接口,可以在運行時調用這些工具。在 JDK 6 中,編譯器被給予了特別的重視。針對編譯器,JDK 設計了兩個接口,分別是 JavaCompiler
和 JavaCompiler.CompilationTask
。
下面給出一個例子,展示如何在運行時調用編譯器。
- 指定編譯文件名稱(該文件必須在 CLASSPATH 中可以找到):
String fullQuanlifiedFileName = "compile" + java.io.File.separator +"Target.java";
- 獲得編譯器對象:
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
通過調用 ToolProvider
的 getSystemJavaCompiler
方法,JDK 提供了將當前平臺的編譯器映射到內存中的一個對象。這樣使用者可以在運行時操縱編譯器。JavaCompiler
是一個接口,它繼承了 javax.tools.Tool
接口。因此,第三方實現的編譯器,只要符合規範就能通過統一的接口調用。同時,tools 開發包希望對所有的工具提供統一的運行時調用接口。相信將來,ToolProvider
類將會爲更多地工具提供 getSystemXXXTool
方法。tools 開發包實際爲多種不同工具、不同實現的共存提供了框架。
- 編譯文件:
int result = compiler.run(null, null, null, fileToCompile);
獲得編譯器對象之後,可以調用 Tool.run
方法對源文件進行編譯。Run
方法的前三個參數,分別可以用來重定向標準輸入、標準輸出和標準錯誤輸出,null
值表示使用默認值。
參考:
public class Target {
public void doSomething(){
Date date = new Date(10, 3, 3);
// 這個構造函數被標記爲deprecated, 編譯時向錯誤輸出輸出信息。
System.out.println("Doing");
}
}
public class Compiler {
public static void main(String[] args) throws Exception {
String fullQuanlifiedFileName = "Target.java";
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
FileOutputStream err = new FileOutputStream("err.txt");
int compilationResult = compiler.run(null, null, err, fullQuanlifiedFileName);
if (compilationResult == 0) {
System.out.println("Done");
} else {
System.out.println("Fail");
}
}
}
JDK 6 的編譯器 API 的另外一個強大之處在於,它可以編譯的源文件的形式並不侷限於文本文件。JavaCompiler
類依靠文件管理服務可以編譯多種形式的源文件。比如直接由內存中的字符串構造的文件,或者是從數據庫中取出的文件。這種服務是由 JavaFileManager
類提供的。通常的編譯過程分爲以下幾個步驟:
- 解析 javac 的參數;
- 在 source path 和/或 CLASSPATH 中查找源文件或者 jar 包;
- 處理輸入,輸出文件;
在這個過程中,JavaFileManager
類可以起到創建輸出文件,讀入並緩存輸出文件的作用。由於它可以讀入並緩存輸入文件,這就使得讀入各種形式的輸入文件成爲可能。JDK 提供的命令行工具,處理機制也大致相似,在未來的版本中,其它的工具處理各種形式的源文件也成爲可能。爲此,新的 JDK 定義了 javax.tools.FileObject
和javax.tools.JavaFileObject
接口。任何類,只要實現了這個接口,就可以被 JavaFileManager
識別。
如果要使用 JavaFileManager
,就必須構造 CompilationTask
。JDK 6 提供了 JavaCompiler.CompilationTask
類來封裝一個編譯操作。這個類可以通過:
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 的處理器。圖 1 展示了通過 CompilationTask
進行編譯的過程:
public class Compiler {
public static void main(String[] args) throws Exception {
String fullQuanlifiedFileName = "Target.java";
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
Iterable<?extendsJavaFileObject>files= fileManager.getJavaFileObjectsFromStrings(Arrays.asList(fullQuanlifiedFileName));
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, files);
Boolean result = task.call();
if (result == true) {
System.out.println("Succeeded");
}
}
}
以上是第一步,通過構造一個 CompilationTask
編譯了一個 Java 文件。首先取得一個編譯器對象。由於僅僅需要編譯普通文件,通過編譯器對象取得了一個標準文件管理器。將需要編譯的文件構造成了一個 Iterable
對象。最後將文件管理器和 Iterable
對象傳遞給 JavaCompiler
的 getTask
方法,取得了 JavaCompiler.CompilationTask
對象。
接下來第二步,開發者希望生成 Calculator
的一個測試類,而不是手工編寫。使用 compiler API,可以將內存中的一段字符串,編譯成一個 CLASS 文件。
public class StringObject extends SimpleJavaFileObject {
private String contents = null;
public Compiler(String className, String contents) throws Exception {super(new URI(className), Kind.SOURCE);
this.contents = contents;
}
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return contents;
}
}
SimpleJavaFileObject
是 JavaFileObject
的子類,它提供了默認的實現。繼承 SimpleJavaObject
之後,只需要實現 getCharContent
方法。接下來,在內存中構造 Calculator
的測試類 CalculatorTest
,並將代表該類的字符串放置到 StringObject
中,傳遞給 JavaCompiler
的 getTask
方法。
public class AdvancedCompiler {
// Steps used to compile Calculator
// Steps used to compile StringObject
// construct CalculatorTest in memory
public static void main(String[] args) throws Exception{
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
String contents="public class Target {\r\n" +
" public void doSomething(){\r\n" +
" Date date = new Date(10, 3, 3); \r\n" +
" // 這個構造函數被標記爲deprecated, 編譯時向錯誤輸出輸出信息。\r\n" +
" System.out.println(\"Doing\");\r\n" +
" }\r\n" +
"}";
JavaFileObject file = new StringObject("Target", contents.toString());
Iterable<? extends JavaFileObject> files = Arrays.asList(file);
JavaCompiler.CompilationTask task = compiler.getTask ( null, fileManager, null, null, null, files);
Boolean result = task.call();
if( result == true ) {
System.out.println("Succeeded");
}
}
}
程序在內存中構造了 Target類,並且通過
StringObject
的構造函數,將內存中的字符串,轉換成了 JavaFileObject
對象。
通過fileManager 獲取class文件內容
JavaClassObject jco = fileManager.getMainJavaClassObject();
List<JavaClassObject> innerClassJcos = fileManager.getInnerClassJavaClassObject();