<!----><!----><!---->2006 年底,Sun 公司發布了 Java Standard Edition 6(Java SE 6)的最終正式版,代號 Mustang(野馬)。跟 Tiger(Java SE 5)相比,Mustang 在性能方面有了不錯的提升。與 Tiger 在 API 庫方面的大幅度加強相比,雖然 Mustang 在 API 庫方面的新特性顯得不太多,但是也提供了許多實用和方便的功能:在腳本,WebService,XML,編譯器 API,數據庫,JMX,網絡和 Instrumentation 方面都有不錯的新特性和功能加強。 本系列 文章主要介紹 Java SE 6 在 API 庫方面的部分新特性,通過一些例子和講解,幫助開發者在編程實踐當中更好的運用 Java SE 6,提高開發效率。
本文是其中的第四篇,介紹了 JDK 6 中為在運行時操縱編譯器所增加的編譯器 API(JSR 199)。您將了解到,利用此 API 開發人員可以在運行時調用 Java 編譯器,還可以編譯非文本形式的 Java 源代碼,最后還能夠采集編譯器的診斷信息。本文將展開描述這些功能,并使用這些功能構造一個簡單的應用 —— 在內存中,直接為一個類生成測試用例。
JDK 6 提供了在運行時調用編譯器的 API,后面我們將假設把此 API 應用在 JSP 技術中。在傳統的 JSP 技術中,服務器處理 JSP 通常需要進行下面 6 個步驟:
- 分析 JSP 代碼;
- 生成 Java 代碼;
- 將 Java 代碼寫入存儲器;
- 啟動另外一個進程并運行編譯器編譯 Java 代碼;
- 將類文件寫入存儲器;
- 服務器讀入類文件并運行;
但如果采用運行時編譯,可以同時簡化步驟 4 和 5,節約新進程的開銷和寫入存儲器的輸出開銷,提高系統效率。實際上,在 JDK 5 中,Sun 也提供了調用編譯器的編程接口。然而不同的是,老版本的編程接口并不是標準 API 的一部分,而是作為 Sun 的專有實現提供的,而新版則帶來了標準化的優點。
新 API 的第二個新特性是可以編譯抽象文件,理論上是任何形式的對象 —— 只要該對象實現了特定的接口。有了這個特性,上述例子中的步驟 3 也可以省略。整個 JSP 的編譯運行在一個進程中完成,同時消除額外的輸入輸出操作。
第三個新特性是可以收集編譯時的診斷信息。作為對前兩個新特性的補充,它可以使開發人員輕松的輸出必要的編譯錯誤或者是警告信息,從而省去了很多重定向的麻煩。
![]() ![]() |
![]()
|
在 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
值表示使用默認值。
清單 1
給出了一個完整的例子:
清單 1. 程序運行時編譯文件
01 package compile; |
首先運行 <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. |
仔細觀察
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
類提供的。通常的編譯過程分為以下幾個步驟:
- 解析 javac 的參數;
- 在 source path 和/或 CLASSPATH 中查找源文件或者 jar 包;
- 處理輸入,輸出文件;
在這個過程中,
JavaFileManager
類可以起到創建輸出文件,讀入并緩存輸出文件的作用。由于它可以讀入并緩存輸入文件,這就使得讀入各種形式的輸入文件成為可能。JDK 提供的命令行工具,處理機制也大致相似,在未來的版本中,其它的工具處理各種形式的源文件也成為可能。為此,新的 JDK 定義了
javax.tools.FileObject
和
javax.tools.JavaFileObject
接口。任何類,只要實現了這個接口,就可以被
JavaFileManager
識別。
如果要使用
JavaFileManager
,就必須構造
CompilationTask
。JDK 6 提供了
JavaCompiler.CompilationTask
類來封裝一個編譯操作。這個類可以通過:
JavaCompiler.getTask ( |
方法得到。關于每個參數的含義,請參見 JDK 文檔。傳遞不同的參數,會得到不同的
CompilationTask
。通過構造這個類,一個編譯過程可以被分成多步。進一步,
CompilationTask
提供了
setProcessors(Iterable<? extends Processor>processors)
方法,用戶可以制定處理 annotation 的處理器。圖 1 展示了通過
CompilationTask
進行編譯的過程:
圖 1. 使用 CompilationTask 進行編譯
下面的例子通過構造
CompilationTask
分多步編譯一組 Java 源文件。
清單 2. 構造 CompilationTask 進行編譯
01 package math; |
以上是第一步,通過構造一個
CompilationTask
編譯了一個 Java 文件。14-17 行實現了主要邏輯。第 14 行,首先取得一個編譯器對象。由于僅僅需要編譯普通文件,因此第 15 行中通過編譯器對象取得了一個標準文件管理器。16 行,將需要編譯的文件構造成了一個
Iterable
對象。最后將文件管理器和
Iterable
對象傳遞給
JavaCompiler
的
getTask
方法,取得了
JavaCompiler.CompilationTask
對象。
接下來第二步,開發者希望生成
Calculator
的一個測試類,而不是手工編寫。使用 compiler API,可以將內存中的一段字符串,編譯成一個 CLASS 文件。
清單 3. 定制 JavaFileObject 對象
01 package math; |
SimpleJavaFileObject
是
JavaFileObject
的子類,它提供了默認的實現。繼承
SimpleJavaObject
之后,只需要實現
getCharContent
方法。如
清單 3
中的 9-11 行所示。接下來,在內存中構造
Calculator
的測試類
CalculatorTest
,并將代表該類的字符串放置到
StringObject
中,傳遞給
JavaCompiler
的
getTask
方法。
清單 4
展現了這些步驟。
清單 4. 編譯非文本形式的源文件
01 package math; |
實現邏輯和
清單 2
相似。不同的是在 20-30 行,程序在內存中構造了
CalculatorTest
類,并且通過
StringObject
的構造函數,將內存中的字符串,轉換成了
JavaFileObject
對象。
![]() ![]() |
![]()
|
第三個新增加的功能,是收集編譯過程中的診斷信息。診斷信息,通常指錯誤、警告或是編譯過程中的詳盡輸出。JDK 6 通過
Listener
機制,獲取這些信息。如果要注冊一個
DiagnosticListener
,必須使用
CompilationTask
來進行編譯,因為 Tool 的
run
方法沒有辦法注冊
Listener
。步驟很簡單,先構造一個
Listener
,然后傳遞給
JavaFileManager
的構造函數。
清單 5
對
清單 2
進行了改動,展示了如何注冊一個
DiagnosticListener
。
清單 5. 注冊一個 DiagnosticListener 收集編譯信息
01 package math; |
在 17 行,構造了一個
DiagnosticCollector
對象,這個對象由 JDK 提供,它實現了
DiagnosticListener
接口。18 行將它注冊到
CompilationTask
中去。一個編譯過程可能有多個診斷信息。每一個診斷信息,被抽象為一個
Diagnostic
。20-26 行,將所有的診斷信息逐個輸出。編譯并運行 Compiler,得到以下輸出:
清單 6. DiagnosticCollector 收集的編譯信息
Line Number->5 |
實際上,也可以由用戶自己定制。
清單 7
給出了一個定制的
Listener
。
清單 7. 自定義的 DiagnosticListener
01 class ADiagnosticListener implements DiagnosticListener<JavaFileObject>{ |
![]() ![]() |
![]()
|
JDK 6 的編譯器新特性,使得開發者可以更自如的控制編譯的過程,這給了工具開發者更加靈活的自由度。通過 API 的調用完成編譯操作的特性,使得開發者可以更方便、高效地將編譯變為軟件系統運行時的服務。而編譯更廣泛形式的源代碼,則為整合更多的數據源及功能提供了強大的支持。相信隨著 JDK 的不斷完善,更多的工具將具有 API 支持,我們拭目以待。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
