2006 年底,Sun 公司發(fā)布了 Java Standard Edition 6(Java SE 6)的最終正式版,代號 Mustang(野馬)。跟 Tiger(Java SE 5)相比,Mustang 在性能方面有了不錯的提升。與 Tiger 在 API 庫方面的大幅度加強(qiáng)相比,雖然 Mustang 在 API 庫方面的新特性顯得不太多,但是也提供了許多實(shí)用和方便的功能:在腳本,WebService,XML,編譯器 API,數(shù)據(jù)庫,JMX,網(wǎng)絡(luò)和 Instrumentation 方面都有不錯的新特性和功能加強(qiáng)。 本系列 文章主要介紹 Java SE 6 在 API 庫方面的部分新特性,通過一些例子和講解,幫助開發(fā)者在編程實(shí)踐當(dāng)中更好的運(yùn)用 Java SE 6,提高開發(fā)效率。 本文是其中的第四篇,介紹了 JDK 6 中為在運(yùn)行時操縱編譯器所增加的編譯器 API(JSR 199)。您將了解到,利用此 API 開發(fā)人員可以在運(yùn)行時調(diào)用 Java 編譯器,還可以編譯非文本形式的 Java 源代碼,最后還能夠采集編譯器的診斷信息。本文將展開描述這些功能,并使用這些功能構(gòu)造一個簡單的應(yīng)用 —— 在內(nèi)存中,直接為一個類生成測試用例。 新 API 功能簡介 JDK 6 提供了在運(yùn)行時調(diào)用編譯器的 API,后面我們將假設(shè)把此 API 應(yīng)用在 JSP 技術(shù)中。在傳統(tǒng)的 JSP 技術(shù)中,服務(wù)器處理 JSP 通常需要進(jìn)行下面 6 個步驟: - 分析 JSP 代碼;
- 生成 Java 代碼;
- 將 Java 代碼寫入存儲器;
- 啟動另外一個進(jìn)程并運(yùn)行編譯器編譯 Java 代碼;
- 將類文件寫入存儲器;
- 服務(wù)器讀入類文件并運(yùn)行;
但如果采用運(yùn)行時編譯,可以同時簡化步驟 4 和 5,節(jié)約新進(jìn)程的開銷和寫入存儲器的輸出開銷,提高系統(tǒng)效率。實(shí)際上,在 JDK 5 中,Sun 也提供了調(diào)用編譯器的編程接口。然而不同的是,老版本的編程接口并不是標(biāo)準(zhǔn) API 的一部分,而是作為 Sun 的專有實(shí)現(xiàn)提供的,而新版則帶來了標(biāo)準(zhǔn)化的優(yōu)點(diǎn)。 新 API 的第二個新特性是可以編譯抽象文件,理論上是任何形式的對象 —— 只要該對象實(shí)現(xiàn)了特定的接口。有了這個特性,上述例子中的步驟 3 也可以省略。整個 JSP 的編譯運(yùn)行在一個進(jìn)程中完成,同時消除額外的輸入輸出操作。 第三個新特性是可以收集編譯時的診斷信息。作為對前兩個新特性的補(bǔ)充,它可以使開發(fā)人員輕松的輸出必要的編譯錯誤或者是警告信息,從而省去了很多重定向的麻煩。
運(yùn)行時編譯 Java 文件 在 JDK 6 中,類庫通過 javax.tools 包提供了程序運(yùn)行時調(diào)用編譯器的 API。從這個包的名字 tools 可以看出,這個開發(fā)包提供的功能并不僅僅限于編譯器。工具還包括 javah、jar、pack200 等,它們都是 JDK 提供的命令行工具。這個開發(fā)包希望通過實(shí)現(xiàn)一個統(tǒng)一的接口,可以在運(yùn)行時調(diào)用這些工具。在 JDK 6 中,編譯器被給予了特別的重視。針對編譯器,JDK 設(shè)計(jì)了兩個接口,分別是 JavaCompiler 和 JavaCompiler.CompilationTask 。 下面給出一個例子,展示如何在運(yùn)行時調(diào)用編譯器。 - 指定編譯文件名稱(該文件必須在 CLASSPATH 中可以找到):
String fullQuanlifiedFileName = "compile" + java.io.File.separator +"Target.java"; - 獲得編譯器對象:
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); 通過調(diào)用 ToolProvider 的 getSystemJavaCompiler 方法,JDK 提供了將當(dāng)前平臺的編譯器映射到內(nèi)存中的一個對象。這樣使用者可以在運(yùn)行時操縱編譯器。JavaCompiler 是一個接口,它繼承了 javax.tools.Tool 接口。因此,第三方實(shí)現(xiàn)的編譯器,只要符合規(guī)范就能通過統(tǒng)一的接口調(diào)用。同時,tools 開發(fā)包希望對所有的工具提供統(tǒng)一的運(yùn)行時調(diào)用接口。相信將來,ToolProvider 類將會為更多地工具提供 getSystemXXXTool 方法。tools 開發(fā)包實(shí)際為多種不同工具、不同實(shí)現(xiàn)的共存提供了框架。 - 編譯文件:
int result = compiler.run(null, null, null, fileToCompile); 獲得編譯器對象之后,可以調(diào)用 Tool.run 方法對源文件進(jìn)行編譯。Run 方法的前三個參數(shù),分別可以用來重定向標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯誤輸出,null 值表示使用默認(rèn)值。清單 1 給出了一個完整的例子: 清單 1. 程序運(yùn)行時編譯文件 01 package compile; 02 import java.util.Date; 03 public class Target { 04 public void doSomething(){ 05 Date date = new Date(10, 3, 3); // 這個構(gòu)造函數(shù)被標(biāo)記為deprecated, 編譯時會 // 向錯誤輸出輸出信息。 06 System.out.println("Doing..."); 07 } 08 } 09 package compile; 10 import javax.tools.*; 11 import java.io.FileOutputStream; 12 public class Compiler { 13 public static void main(String[] args) throws Exception{ 14 String fullQuanlifiedFileName = "compile" + java.io.File.separator + "Target.java"; 15 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); 16 FileOutputStream err = new FileOutputStream("err.txt"); 17 int compilationResult = compiler.run(null, null, err, fullQuanlifiedFileName); 18 if(compilationResult == 0){ 19 System.out.println("Done"); 20 } else { 21 System.out.println("Fail"); 22 } 23 } 24 } | 首先運(yùn)行 <JDK60_INSTALLATION_DIR>\bin\javac Compiler.java,然后運(yùn)行 <JDK60_INSTALLATION_DIR>\jdk1.6.0\bin\java compile.Compiler。屏幕上將輸出 Done ,并會在當(dāng)前目錄生成一個 err.txt 文件,文件內(nèi)容如下: Note: compile/Target.java uses or overrides a deprecated API. Note: Recompile with -Xlint:deprecation for details. | 仔細(xì)觀察 run 方法,可以發(fā)現(xiàn)最后一個參數(shù)是 String...arguments ,是一個變長的字符串?dāng)?shù)組。它的實(shí)際作用是接受傳遞給 javac 的參數(shù)。假設(shè)要編譯 Target.java 文件,并顯示編譯過程中的詳細(xì)信息。命令行為:javac Target.java -verbose 。相應(yīng)的可以將 17 句改為: int compilationResult = compiler.run(null, null, err, “-verbose”,fullQuanlifiedFileName); |
編譯非文本形式的文件 JDK 6 的編譯器 API 的另外一個強(qiáng)大之處在于,它可以編譯的源文件的形式并不局限于文本文件。JavaCompiler 類依靠文件管理服務(wù)可以編譯多種形式的源文件。比如直接由內(nèi)存中的字符串構(gòu)造的文件,或者是從數(shù)據(jù)庫中取出的文件。這種服務(wù)是由 JavaFileManager 類提供的。通常的編譯過程分為以下幾個步驟: - 解析 javac 的參數(shù);
- 在 source path 和/或 CLASSPATH 中查找源文件或者 jar 包;
- 處理輸入,輸出文件;
在這個過程中,JavaFileManager 類可以起到創(chuàng)建輸出文件,讀入并緩存輸出文件的作用。由于它可以讀入并緩存輸入文件,這就使得讀入各種形式的輸入文件成為可能。JDK 提供的命令行工具,處理機(jī)制也大致相似,在未來的版本中,其它的工具處理各種形式的源文件也成為可能。為此,新的 JDK 定義了 javax.tools.FileObject 和 javax.tools.JavaFileObject 接口。任何類,只要實(shí)現(xiàn)了這個接口,就可以被 JavaFileManager 識別。 如果要使用 JavaFileManager ,就必須構(gòu)造 CompilationTask 。JDK 6 提供了 JavaCompiler.CompilationTask 類來封裝一個編譯操作。這個類可以通過: JavaCompiler.getTask ( Writer out, JavaFileManager fileManager, DiagnosticListener<? super JavaFileObject> diagnosticListener, Iterable<String> options, Iterable<String> classes, Iterable<? extends JavaFileObject> compilationUnits ) | 方法得到。關(guān)于每個參數(shù)的含義,請參見 JDK 文檔。傳遞不同的參數(shù),會得到不同的 CompilationTask 。通過構(gòu)造這個類,一個編譯過程可以被分成多步。進(jìn)一步,CompilationTask 提供了 setProcessors(Iterable<? extends Processor>processors) 方法,用戶可以制定處理 annotation 的處理器。圖 1 展示了通過 CompilationTask 進(jìn)行編譯的過程: 圖 1. 使用 CompilationTask 進(jìn)行編譯 下面的例子通過構(gòu)造 CompilationTask 分多步編譯一組 Java 源文件。 清單 2. 構(gòu)造 CompilationTask 進(jìn)行編譯 01 package math; 02 public class Calculator { 03 public int multiply(int multiplicand, int multiplier) { 04 return multiplicand * multiplier; 05 } 06 } 07 package compile; 08 import javax.tools.*; 09 import java.io.FileOutputStream; 10 import java.util.Arrays; 11 public class Compiler { 12 public static void main(String[] args) throws Exception{ 13 String fullQuanlifiedFileName = "math" + java.io.File.separator +"Calculator.java"; 14 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); 15 StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); 16 Iterable<? extends JavaFileObject> files = fileManager.getJavaFileObjectsFromStrings( Arrays.asList(fullQuanlifiedFileName)); 17 JavaCompiler.CompilationTask task = compiler.getTask( null, fileManager, null, null, null, files); 18 Boolean result = task.call(); 19 if( result == true ) { 20 System.out.println("Succeeded"); 21 } 22 } 23 } | 以上是第一步,通過構(gòu)造一個 CompilationTask 編譯了一個 Java 文件。14-17 行實(shí)現(xiàn)了主要邏輯。第 14 行,首先取得一個編譯器對象。由于僅僅需要編譯普通文件,因此第 15 行中通過編譯器對象取得了一個標(biāo)準(zhǔn)文件管理器。16 行,將需要編譯的文件構(gòu)造成了一個 Iterable 對象。最后將文件管理器和 Iterable 對象傳遞給 JavaCompiler 的 getTask 方法,取得了 JavaCompiler.CompilationTask 對象。 接下來第二步,開發(fā)者希望生成 Calculator 的一個測試類,而不是手工編寫。使用 compiler API,可以將內(nèi)存中的一段字符串,編譯成一個 CLASS 文件。 清單 3. 定制 JavaFileObject 對象 01 package math; 02 import java.net.URI; 03 public class StringObject extends SimpleJavaFileObject{ 04 private String contents = null; 05 public StringObject(String className, String contents) throws Exception{ 06 super(new URI(className), Kind.SOURCE); 07 this.contents = contents; 08 } 09 public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { 10 return contents; 11 } 12 } | SimpleJavaFileObject 是 JavaFileObject 的子類,它提供了默認(rèn)的實(shí)現(xiàn)。繼承 SimpleJavaObject 之后,只需要實(shí)現(xiàn) getCharContent 方法。如 清單 3 中的 9-11 行所示。接下來,在內(nèi)存中構(gòu)造 Calculator 的測試類 CalculatorTest ,并將代表該類的字符串放置到 StringObject 中,傳遞給 JavaCompiler 的 getTask 方法。清單 4 展現(xiàn)了這些步驟。 清單 4. 編譯非文本形式的源文件 01 package math; 02 import javax.tools.*; 03 import java.io.FileOutputStream; 04 import java.util.Arrays; 05 public class AdvancedCompiler { 06 public static void main(String[] args) throws Exception{ 07 // Steps used to compile Calculator 08 // Steps used to compile StringObject 09 // construct CalculatorTest in memory 10 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); 11 StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); 12 JavaFileObject file = constructTestor(); 13 Iterable<? extends JavaFileObject> files = Arrays.asList(file); 14 JavaCompiler.CompilationTask task = compiler.getTask ( null, fileManager, null, null, null, files); 15 Boolean result = task.call(); 16 if( result == true ) { 17 System.out.println("Succeeded"); 18 } 19 } 20 private static SimpleJavaFileObject constructTestor() { 21 StringBuilder contents = new StringBuilder( "package math;" + "class CalculatorTest {\n" + " public void testMultiply() {\n" + " Calculator c = new Calculator();\n" + " System.out.println(c.multiply(2, 4));\n" + " }\n" + " public static void main(String[] args) {\n" + " CalculatorTest ct = new CalculatorTest();\n" + " ct.testMultiply();\n" + " }\n" + "}\n"); 22 StringObject so = null; 23 try { 24 so = new StringObject("math.CalculatorTest", contents.toString()); 25 } catch(Exception exception) { 26 exception.printStackTrace(); 27 } 28 return so; 29 } 30 } | 實(shí)現(xiàn)邏輯和 清單 2 相似。不同的是在 20-30 行,程序在內(nèi)存中構(gòu)造了 CalculatorTest 類,并且通過 StringObject 的構(gòu)造函數(shù),將內(nèi)存中的字符串,轉(zhuǎn)換成了 JavaFileObject 對象。
采集編譯器的診斷信息 第三個新增加的功能,是收集編譯過程中的診斷信息。診斷信息,通常指錯誤、警告或是編譯過程中的詳盡輸出。JDK 6 通過 Listener 機(jī)制,獲取這些信息。如果要注冊一個 DiagnosticListener ,必須使用 CompilationTask 來進(jìn)行編譯,因?yàn)?Tool 的 run 方法沒有辦法注冊 Listener 。步驟很簡單,先構(gòu)造一個 Listener ,然后傳遞給 JavaFileManager 的構(gòu)造函數(shù)。清單 5 對 清單 2 進(jìn)行了改動,展示了如何注冊一個 DiagnosticListener 。 清單 5. 注冊一個 DiagnosticListener 收集編譯信息 01 package math; 02 public class Calculator { 03 public int multiply(int multiplicand, int multiplier) { 04 return multiplicand * multiplier // deliberately omit semicolon, ADiagnosticListener // will take effect 05 } 06 } 07 package compile; 08 import javax.tools.*; 09 import java.io.FileOutputStream; 10 import java.util.Arrays; 11 public class CompilerWithListener { 12 public static void main(String[] args) throws Exception{ 13 String fullQuanlifiedFileName = "math" + java.io.File.separator +"Calculator.java"; 14 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); 15 StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); 16 Iterable<? extends JavaFileObject> files = fileManager.getJavaFileObjectsFromStrings( Arrays.asList(fullQuanlifiedFileName)); 17 DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<JavaFileObject>(); 18 JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, collector, null, null, files); 19 Boolean result = task.call(); 20 List<Diagnostic<? extends JavaFileObject>> diagnostics = collector.getDiagnostics(); 21 for(Diagnostic<? extends JavaFileObject> d : diagnostics){ 22 System.out.println("Line Number->" + d.getLineNumber()); 23 System.out.println("Message->"+ d.getMessage(Locale.ENGLISH)); 24 System.out.println("Source" + d.getCode()); 25 System.out.println("\n"); 26 } 27 if( result == true ) { 28 System.out.println("Succeeded"); 29 } 30 } 31 } | 在 17 行,構(gòu)造了一個 DiagnosticCollector 對象,這個對象由 JDK 提供,它實(shí)現(xiàn)了 DiagnosticListener 接口。18 行將它注冊到 CompilationTask 中去。一個編譯過程可能有多個診斷信息。每一個診斷信息,被抽象為一個 Diagnostic 。20-26 行,將所有的診斷信息逐個輸出。編譯并運(yùn)行 Compiler,得到以下輸出: 清單 6. DiagnosticCollector 收集的編譯信息 Line Number->5 Message->math/Calculator.java:5: ‘;‘ expected Source->compiler.err.expected | 實(shí)際上,也可以由用戶自己定制。清單 7 給出了一個定制的 Listener 。 清單 7. 自定義的 DiagnosticListener 01 class ADiagnosticListener implements DiagnosticListener<JavaFileObject>{ 02 public void report(Diagnostic<? extends JavaFileObject> diagnostic) { 03 System.out.println("Line Number->" + diagnostic.getLineNumber()); 04 System.out.println("Message->"+ diagnostic.getMessage(Locale.ENGLISH)); 05 System.out.println("Source" + diagnostic.getCode()); 06 System.out.println("\n"); 07 } 08 } |
總結(jié) JDK 6 的編譯器新特性,使得開發(fā)者可以更自如的控制編譯的過程,這給了工具開發(fā)者更加靈活的自由度。通過 API 的調(diào)用完成編譯操作的特性,使得開發(fā)者可以更方便、高效地將編譯變?yōu)檐浖到y(tǒng)運(yùn)行時的服務(wù)。而編譯更廣泛形式的源代碼,則為整合更多的數(shù)據(jù)源及功能提供了強(qiáng)大的支持。相信隨著 JDK 的不斷完善,更多的工具將具有 API 支持,我們拭目以待。
參考資料 |