Java SE 6 新特性: 編譯器 API

來源:互聯網
上載者:User

新 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. 程式運行時編譯檔案

01 package compile;
02 import java.util.Date;
03 public class Target {
04 public void doSomething(){
05 Date date = new Date(10, 3, 3);
// 這個建構函式被標記為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 }

首 先運行 <JDK60_INSTALLATION_DIR>/bin/javac Compiler.java,然後運行 <JDK60_INSTALLATION_DIR>/jdk1.6.0/bin/java compile.Compiler。螢幕上將輸出 Done ,並會在目前的目錄產生一個 err.txt 檔案,檔案內容如下:

Note: compile/Target.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.

仔細觀察 run 方法,可以發現最後一個參數是 String...arguments,是一個變長的字串數組。它的實際作用是接受傳遞給 javac 的參數。假設要編譯 Target.java 檔案,並顯示編譯過程中的詳細資料。命令列為:javac Target.java -verbose。相應的可以將 17 句改為:

int compilationResult = 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 類來封裝一個編譯操作。這個類可以通過:

JavaCompiler.getTask (
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 進行編譯的過程:

圖 1. 使用 CompilationTask 進行編譯

下面的例子通過構造 CompilationTask 分多步編譯一組 Java 源檔案。

清單 2. 構造 CompilationTask 進行編譯

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 }

以上是第一步,通過構造一個 CompilationTask 編譯了一個 Java 檔案。14-17 行實現了主要邏輯。第 14 行,首先取得一個編譯器對象。由於僅僅需要編譯普通檔案,因此第 15 行中通過編譯器對象取得了一個標準檔案管理工具。16 行,將需要編譯的檔案構造成了一個 Iterable 對象。最後將檔案管理工具和 Iterable 對象傳遞給 JavaCompilergetTask 方法,取得了 JavaCompiler.CompilationTask 對象。

接下來第二步,開發人員希望產生 Calculator 的一個測試類別,而不是手工編寫。使用 compiler API,可以將記憶體中的一段字串,編譯成一個 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 }

SimpleJavaFileObjectJavaFileObject 的子類,它提供了預設的實現。繼承 SimpleJavaObject 之後,只需要實現 getCharContent 方法。如 清單 3 中的 9-11 行所示。接下來,在記憶體中構造 Calculator 的測試類別 CalculatorTest,並將代表該類的字串放置到 StringObject 中,傳遞給 JavaCompilergetTask 方法。清單 4 展現了這些步驟。

清單 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 }

實現邏輯和 清單 2 相似。不同的是在 20-30 行,程式在記憶體中構造了 CalculatorTest 類,並且通過 StringObject 的建構函式,將記憶體中的字串,轉換成了 JavaFileObject 對象。

採集編譯器的診斷資訊

第三個新增加的功能,是收集編譯過程中的診斷資訊。診斷資訊,通常指錯誤、警告或是編譯過程中的詳盡輸出。JDK 6 通過 Listener 機制,擷取這些資訊。如果要註冊一個 DiagnosticListener,必須使用 CompilationTask 來進行編譯,因為 Tool 的 run 方法沒有辦法註冊 Listener。步驟很簡單,先構造一個 Listener,然後傳遞給 JavaFileManager 的建構函式。清單 5 對 清單 2 進行了改動,展示了如何註冊一個 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 行,構造了一個 DiagnosticCollector 對象,這個對象由 JDK 提供,它實現了 DiagnosticListener 介面。18 行將它註冊到 CompilationTask 中去。一個編譯過程可能有多個診斷資訊。每一個診斷資訊,被抽象為一個 Diagnostic。20-26 行,將所有的診斷資訊逐個輸出。編譯並運行 Compiler,得到以下輸出:

清單 6. DiagnosticCollector 收集的編譯資訊

Line Number->5
Message->math/Calculator.java:5: ';' expected
Source->compiler.err.expected

實際上,也可以由使用者自己定製。清單 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 }

總結

JDK 6 的編譯器新特性,使得開發人員可以更自如的控制編譯的過程,這給了工具開發人員更加靈活的自由度。通過 API 的調用完成編譯操作的特性,使得開發人員可以更方便、高效地將編譯變為軟體系統運行時的服務。而編譯更廣泛形式的原始碼,則為整合更多的資料來源及功能提供了 強大的支援。相信隨著 JDK 的不斷完善,更多的工具將具有 API 支援,我們拭目以待。

參考資料

  • 閱讀 Java SE 6 新特性系列 文章的完整列表,瞭解 Java SE 6 其它重要的增強。

  • Java SE 6 文檔:Java SE 6 的規範文檔,可以找到絕大部分新特性的官方說明。
  • 參考 The Java Compiler API。

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.