本文永久地址:https://my.oschina.net/bysu/blog/1552935java
javax.tools
包是一種添加到 Java SE 6 的標準 API,能夠實現 Java 源代碼編譯,使您可以添加動態功能來擴展靜態應用程序。本文將探查 javax.tools 包中提供的主要類,並演示如何使用它們建立一個 façade,以從 Java String
、StringBuffer
或其餘 CharSequence
中編譯 Java 源代碼,而不是從文件中編譯。以後,使用這個 façade 構建一個交互式繪圖應用程序,經過該應用程序,用戶可使用任何有效的數值 Java 表達式表示一個數值函數 y = f(x)。最後,本文將討論與動態源代碼編譯相關的安全風險以及應對方法。git
經過編譯和加載 Java 擴展來對應用程序進行擴展,這種想法並不新鮮,而且一些現有框架也支持這一功能。Java Platform, Enterprise Edition (Java EE) 中的 JavaServer Pages (JSP) 技術就是一種廣爲人知的動態框架,可以生成並編譯 Java 類。JSP 轉換器經過中間產物即源代碼文件將 .jsp 文件轉換爲 Java servlet,JSP 引擎隨後將源代碼文件編譯並加載到 Java EE servlet 容器中。編譯過程一般是經過直接調用 javac
編譯器完成的,這須要安裝 Java Development Kit (JDK) 或者調用 com.sun.tools.javac.Main
(可經過 Sun 的 tools.jar 得到)。Sun 的許可證容許跟隨完整的 Java 運行時環境(Java Runtime Environment,JRE)一塊兒從新分發 tools.jar。其餘實現動態功能的方法包括使用可與應用程序實現語言(參見 參考資料)集成的現有動態腳本編制語言(例如 JavaScript 或 Groovy),或者編寫特定於域的語言和相關的語言解釋器和編譯器。express
其餘框架(例如 NetBeans 和 Eclipse)支持開發人員使用 Java 語言直接編寫擴展,可是這些框架須要外部靜態編譯,並須要管理 Java 代碼及其工件的源代碼和二進制文件。Apache Commons JCI 提供了一種機制能夠將 Java 類編譯並加載到運行的應用程序中。Janino 和 Javassist 也提供了相似的動態功能,可是 Janino 只限於 Java 1.4 以前的語言,而 Javassist 只能工做在 Java 類抽象級別,而不能在源代碼級別工做(參見 參考資料 中有關這些項目的連接)。然而,Java 開發人員已經熟悉如何使用 Java 語言編寫程序,若是一種系統可以動態生成 Java 源代碼並進行編譯和加載,那麼它能夠保證最短的學習曲線並提供最大程度的靈活性。安全
javax.tools
的優勢使用 javax.tools
具備如下好處:服務器
com.sun.tools.javac.Main
API 不屬於 通過文件歸檔的 Java 平臺 API,所以沒有必要在其餘供應商的 JDK 中提供或保證在將來版本的 Sun JDK 中提供該 API。要理解 javax.tools
包,回顧 Java 編譯概念以及如何使用包實現編譯將很是有幫助。javax.tools
包以一種通用的方式對這些概念進行了抽象化,使您可以從備用的源代碼對象提供源代碼,而不要求源代碼必須位於文件系統中。app
編譯 Java 源代碼須要用到如下組件:框架
JavaFileManager
實現,後者管理多個源代碼和類 JavaFileObject
實例以及傳遞給 JavaFileManager
構造函數的 ClassLoader
。JavaFileObject
是一個 FileObject
,專門處理如下任一種由編譯器使用的 JavaFileObject.Kind
枚舉類型:
SOURCE
CLASS
HTML
OTHER
openInputStream()
方法,能夠做爲 InputStream
訪問源代碼。javac
選項,以 Iterable<String>
的形式傳遞JavaFileManager
提供了一個抽象的文件系統,能夠將源文件和輸出文件的文件名映射到 JavaFileObject
實例(其中,文件 表示一個唯一名稱和一串字節之間的關聯。客戶機不須要使用實際的文件系統)。在本文的示例中,JavaFileManager
管理類名與 CharSequence
實例之間的映射,後者包含待編譯的 Java 源代碼。JavaFileManager.Location
包含一個文件名和一個標記,該標記能夠代表該位置是源代碼仍是一個輸出位置。ForwardingJavaFileManager
實現 Chain of Responsibility 模式(參見 參考資料),容許將文件管理器連接在一塊兒,就像類路徑和源路徑將 JAR 和目錄連接起來同樣。若是在這條鏈的第一個元素中沒有發現 Java 類,那麼將對鏈中的其餘元素進行查找。JavaFileManager
也保存表示編譯過的 CLASS
文件的 JavaFileObject
實例。JavaCompiler
建立 JavaCompiler.CompilationTask
對象,後者從 JavaFileManager
中的 JavaFileObject
SOURCE
對象編譯源代碼,建立新的輸出 JavaFileObject
CLASS
文件和 Diagnostic
(警告和錯誤)。靜態 ToolProvider.getSystemJavaCompiler()
方法返回編譯器實例。Diagnostic
和 DiagnosticListener
實現。Diagnostic
是編譯器發出的警告或編譯錯誤。Diagnostic
指定如下內容:
Kind
(ERROR
、WARNING
、MANDATORY_WARNING
、NOTE
或 OTHER
)DiagnosticListener
,編譯器可經過它向客戶機發回診斷信息。DiagnosticCollector
是一個簡單的 DiagnosticListener
實現。圖 1 展現了 javax.tools
中的 javac
概念與其實現之間的映射:jsp
圖 1. javac
概念如何映射到 javax.tools
接口ide
瞭解了這些概念,咱們如今看一下如何實現一個 façade 來編譯 CharSequence
。函數
CharSequence
實例中的 Java 源代碼在本節中,我將爲 javax.tools.JavaCompiler
構造一個 façade。javaxtools.compiler.CharSequenceCompiler
類(參見 下載)能夠編譯保存在任何 java.lang.CharSequence
對象(例如 String
、StringBuffer
和 StringBuilder
)中的 Java 源代碼,並返回一個 Class
。CharSequenceCompiler
提供瞭如下 API:
public CharSequenceCompiler(ClassLoader loader, Iterable<String> options)
:該構造函數接收傳遞給 Java 編譯器的 ClassLoader
,容許它解析相關類。Iterable
options
容許客戶機傳遞額外的編譯器選項,這些選項均對應於 javac
選項。public Map<String, Class<T>> compile(Map<String, CharSequence> classes, final DiagnosticCollector<JavaFileObject> diagnostics) throws CharSequenceCompilerException, ClassCastException
:這是經常使用的編譯方法,支持同時編譯多個源代碼。注意,Java 編譯器必須處理類的循環依賴性,例如 A.java 依賴 B.java,B.java 依賴 C.java,而 C.java 又依賴 A.java。該方法的第一個參數是 Map
,它的鍵爲徹底限定類名,而相應的值爲包含該類源代碼的 CharSequence
。例如:
"mypackage.A"
⇒ "package mypackage; public class A { ... }";
"mypackage.B"
⇒ "package mypackage; class B extends A implements C { ... }";
"mypackage.C"
⇒ "package mypackage; interface C { ... }"
Diagnostic
添加到 DiagnosticCollector
。您但願對類進行強制轉換的主要類型是泛型類型參數 T
。compile()
被另外一個方法重載,其參數爲一個類名和待編譯的 CharSequence
。public ClassLoader getClassLoader()
:該方法返回編譯器在生成 .class 文件時組裝的類加載器,所以,能夠從其中加載其餘類或資源。public Class<T> loadClass(final String qualifiedClassName) throws ClassNotFoundException
:因爲 compile()
方法能夠定義多個類(包括公共的嵌套類),所以容許加載輔助類。爲了支持 CharSequenceCompiler
API,我使用 JavaFileObjectImpl
類和 JavaFileManagerImpl
實現了 javax.tools
接口,其中,JavaFileObjectImpl
類用於保存 CharSequence
源代碼和編譯器產生的 CLASS
輸出,而 JavaFileManagerImpl
用於將名稱映射到 JavaFileObjectImpl
實例,從而管理源代碼和編譯器產生的字節碼。
JavaFileObjectImpl
清單 1 中的 JavaFileObjectImpl
實現 JavaFileObject
並保存 CharSequence source
(用於 SOURCE
)或一個 ByteArrayOutputStream byteCode
(用於 CLASS
文件)。關鍵方法是 CharSequence getCharContent(final boolean ignoreEncodingErrors)
,編譯器經過它得到源代碼文本。參見 下載,獲取全部代碼示例的源代碼。
清單 1. JavaFileObjectImpl
(只顯示部分源代碼)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
FileManagerImpl
FileManagerImpl
(參見清單 2)對 ForwardingJavaFileManager
進行了擴展,將限定的類名映射到 JavaFileObjectImpl
實例:
清單 2. FileManagerImpl
(只顯示部分源代碼)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
CharSequenceCompiler
若是 ToolProvider.getSystemJavaCompiler() 不能建立 JavaCompiler
若是 tools.jar 不在應用程序類路徑中,ToolProvider.getSystemJavaCompiler()
方法能夠返回 null
。CharStringCompiler
類檢測到這一配置問題後將拋出一個異常,並給出修復建議。注意,Sun 許可證容許跟隨 JRE 一塊兒從新分發 tools.jar。
經過這些支持類,如今能夠定義 CharSequenceCompiler
,可以使用運行時 ClassLoader
和編譯器選項構建。它使用 ToolProvider.getSystemJavaCompiler()
得到 JavaCompiler
實例,而後對轉發給編譯器標準文件管理器的 JavaFileManagerImpl
進行實例化。
compile()
方法對輸入映射進行迭代,從每一個名稱/CharSequence
中構建一個 JavaFileObjectImpl
,並將其添加到 JavaFileManager
中,所以,在調用文件管理器的 getFileForInput()
方法時,JavaCompiler
將找到它們。compile()
方法隨後將建立一個 JavaCompiler.Task
實例並運行該實例。故障被做爲 CharSequenceCompilerException
拋出。而後,對於傳遞給 compile()
方法的全部源代碼,加載產生的 Class
類並放在結果 Map
中。
與 CharSequenceCompiler
(參見清單 3)相關的類加載器是一個 ClassLoaderImpl
實例,它在 JavaFileManagerImpl
實例中查找類的字節碼,返回編譯器建立的 .class 文件:
清單 3. CharSequenceCompiler
(只顯示部分源代碼)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
|
如今,我有了一個能夠編譯源代碼的簡單 API,我將經過建立函數繪製應用程序(使用 Swing 編寫)來發揮其功用。圖 2 展現了該應用程序使用圖形表示 x * sin(x) * cos(x)
函數:
圖 2. 使用 javaxtools.compiler 包的動態應用程序
該應用程序使用清單 4 中定義的 Function
接口:
清單 4. Function
接口
1 2 3 4 |
|
應用程序提供了一個文本字段,用戶能夠向其中輸入一個 Java 表達式,後者根據隱式聲明的 double x
輸入參數返回一個 double
值。在清單 5 所示的代碼模板中,應用程序將表達式文本插入到以 $expression
標記的位置。而且每次生成一個唯一的類名,替代模板中的 $className
。包名也是一個模板變量。
清單 5. Function
模板
1 2 3 4 5 6 7 8 |
|
應用程序使用 fillTemplate(packageName, className, expr)
函數填充模板,它返回一個 String
對象,而後應用程序使用 CharSequenceCompiler
進行編譯。異常或編譯器診斷信息被傳遞給 log()
方法或直接寫入到應用程序中可滾動的 errors
組件。
清單 6 顯示的 newFunction()
方法將返回一個對象,它將實現 Function
接口(參見 清單 5 中的源代碼模板):
清單 6. Plotter
的 Function newFunction(String expr)
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
您一般會生成一些源類,使用它們擴展已有的基類或實現特定接口,從而能夠將實例轉換爲已知的類型並經過一個類型安全 API 調用其方法。注意,在實例化 CharSequenceCompiler<T>
時,Function
類被做爲泛型類型參數 T
使用。所以,也能夠將 compiledFunction
輸入做爲 Class<Function>
和 compiledFunction.newInstance()
,以返回 Function
實例,而不須要進行強制轉換。
動態生成一個 Function
實例後,應用程序使用它針對一系列 x 值生成 y 值,而後使用開源的 JFreeChart API(參見 參考資料)描繪(x,y)值。Swing 應用程序的完整源代碼能夠經過 javaxtools.compiler.examples.plotter
包的 可下載源代碼 部分中得到。
這個應用程序的源代碼生成需求很是普通。更爲複雜的源代碼模板工具能夠更好地知足其餘應用程序的需求,例如 Apache Velocity (參見 參考資料)。
若是應用程序容許用戶隨意輸入 Java 源代碼,那麼會存在一些內在的安全風險。相似 SQL 注入(參見 參考資料),若是系統容許用戶或其餘代理提供原始的 Java 源代碼來生成代碼,那麼惡意用戶可能會利用這一點。例如,在本文的 Plotter
應用程序中,一個有效的 Java 表達式可能包含匿名的嵌套類,它能夠訪問系統資源、在受到拒絕服務攻擊時產生大量線程或者執行其餘行爲。這些行爲被稱爲 Java 注入。這種應用程序不該該部署在非信任用戶能夠隨意訪問的不可靠位置,例如做爲 servlet 或 applet 的 Java EE 服務器。相反,javax.tools
的大多數客戶機應該限制用戶輸入並將用戶請求轉換爲安全的源代碼。
使用這種包時能夠採用的安全策略包括:
SecurityManager
或 ClassLoader
阻止加載匿名類或其餘沒法直接控制的類。Plotter
可使用 java.io.StreamTokenizer
並刪除含有 {
(左側大括號)字符的輸入,從而有效阻止了匿名或嵌套類的聲明。javax.tools
API,JavaFileManager
能夠刪除任何預料以外的 CLASS
文件的寫入。例如,當編譯某個特定類時,對於要求保存預料以外的類文件的任何調用,JavaFileManager
將拋出一個 SecurityExeception
異常,並只容許生成用戶沒法猜想或欺騙的包名或類名。Plotter
的 newFunction
方法使用的就是這種策略。在本文中,我解釋了 javax.tools
包的概念和重要接口,並展現了一個 façade,使用它編譯保存在 String
或其餘 CharSequence
中的 Java 源代碼,而後使用這個庫類開發能夠描繪任意 f(x) 函數的樣例應用程序。可使用這種技術建立其餘有用的應用程序:
下一次開發應用程序時,若是須要使用動態行爲,請嘗試 javax.tools
提供的多樣性和靈活性。