婁不夜 首都經濟貿易大學信息學院 100070
Web環境下Java表達式的動態編譯與計算
婁不夜 首都經濟貿易大學信息學院 100070
針對Java Web應用程序需要在運行時從外部讀得Java表達式并進行計算的要求,利用編譯器API對源代碼進行編譯、采用自定義類裝載器裝載字節代碼、基于反射機制執行字節代碼,從而實現Java表達式的動態編譯與計算。該方法不需要產生Java源文件或class文件。實際系統的具體應用驗證了該方法的有效性。
Java表達式;編譯器API;類裝載器;反射機制
有些Java Web應用程序在運行時需要從外部(如數據庫、XML文件)讀入某些數據,以完成相應的處理功能。這些數據可能是一些簡單的字符串、數值,也可能是一些需要計算才能得到結果的表達式。
大多數解釋型語言都有諸如eval的函數,可以將一個字符串作為表達式進行計算求值,而作為半編譯半解釋的Java語言并不提供類似的功能。本文介紹一種利用Java SE6提供的編譯器API實現類似功能的方法,可以在程序運行過程中完成Java表達式的編譯和計算。
該方法涉及動態編譯、自定義類裝載器、反射機制等技術,其基本過程如下:
(1) 基于要計算的表達式字符串構建一個Java類,類的源代碼存儲在普通的字符串中。


(2) 利用編譯器API實現對源代碼的動態編譯。其中源代碼直接讀自上述字符串,編譯產生的字節代碼保存在字節數組中。整個過程不涉及源代碼文件和字節代碼文件的創建。
(3) 使用自定義類裝載器裝入字節代碼、產生Class對象。這里,類裝載器直接從字節數組讀取字節代碼。由于Java不支持類的重新裝載,也不允許已裝載類的卸載,所以每次計算表達式都需要新建一個類裝載器實例。
(4) 利用反射機制調用Temp類的m方法,完成表達式的最后計算。
其中,字符串exp是要計算的表達式,靜態方法m的功能是計算并返回表達式的值。這里表達式的類型可以是任意引用類型或基本類型。若是基本類型,計算結果會自動轉換成相應包裝類對象返回。
(1) 調用javax.tools.ToolProvider類的getSystemJavaCompiler方法獲得編譯器對象。
(2) 調用編譯器對象的getTask方法創建編譯作業對象。getTask方法有六個參數,其中第2個參數需指定一個Java文件管理器,在執行編譯作業時,系統會自動調用該文件管理器的相關方法獲取用于保存編譯產生的字節代碼的Java文件對象。第4個參數指定編譯所需的相關參數。第6個參數需指定包含待編譯源代碼的Java文件對象。其他三個參數可置為null。
(3) 調用編譯作業對象的call方法,執行編譯作業。若方法返回true,表示編譯成功;否則表示編譯失敗。
(4) 調用Java文件管理器的相關方法獲取保存有字節代碼的Java文件對象,然后調用Java文件對象的相關方法獲取字節代碼。
Java文件對象是對Java源代碼或Java字節代碼的抽象表示。默認情況下,一個Java文件對象表示一個Java源文件或一個class文件。為能表示保存在內存(如字符串、字節數組)的Java源代碼或字節代碼,需要自定義Java文件對象類。
為清晰之見,對兩種Java文件對象類分別進行定義。用于表示源代碼的Java文件對象類的代碼如下:


在執行編譯作業時,系統會自動調用getCharContent方法獲得源代碼。
用于表示字節代碼的Java文件對象類的部分代碼如下:

當執行編譯作業時,系統會自動調用openOutputStream方法獲得一個字節輸出流,并通過該字節輸出流寫出編譯產生的字節代碼。編譯結束后,可以調用getByteCode方法獲取字節代碼。
Java文件管理器用于管理Java文件對象。在創建編譯作業對象時,若第2個參數設置為null,系統將采用一個標準Java文件管理器。在這種情況下,編譯產生的字節代碼將被保存到相應的class文件中。為實現對特殊Java文件對象的管理,需要自定義Java文件管理器類(省略了構造方法):

這種類型的Java文件管理器可以對JavaFileObjectByteCode型文件對象進行管理。當執行編譯作業時,系統會自動調用getJavaFileForOutput方法獲得用于保存相應字節代碼的文件對象。編譯結束后,用戶也可以調用該方法獲得相同的文件對象。
要執行Java字節代碼,首先需要由類裝載器將其裝入JVM。在JVM中,通常存在多個類裝載器,每個類裝載器負責裝載特定位置的class文件。最基本的類裝載器包括:
(1) 引導(Bootstrap)類裝載器。該裝載器屬于JVM的一部分,其本身在JVM啟動時自行裝入。作用有兩個:一是裝載并創建擴展類裝載器和應用程序類裝載器;二是需要時裝載Java核心類庫中的class文件。
(2) 擴展(Extension)類裝載器。用于裝載java.ext.dirs屬性指定位置處的class文件。
(3) 應用程序(Application)類裝載器。用于裝載java.class.path屬性指定位置處的class文件。
其中擴展類裝載器代碼和應用程序類裝載器代碼都是Java類,它們都是ClassLoader類子類。
每個類裝載器在創建時可以指定一個父類裝載器,如擴展類裝載器就被指定為應用程序類裝載器的父類裝載器。裝載一個類通常由系統隱含調用類裝載器的loadClass(String)方法完成,其過程采用委托模型:首先檢查當前類裝載器是否已裝載該類;若沒有,就委托其父類裝載器進行裝載;若沒有父類裝載器,則委托引導類裝載器進行裝載;若上述所有情況都無法定位或裝載該類,就調用當前類裝載器的findClass(String)方法自行裝載。
Web應用環境一般會有更多的類裝載器,這些類裝載器通常把應用程序類裝載器作為父類裝載器,它們同樣采用上述委托模型進行類的裝載。
為了能將由動態編譯產生的、保存在字節數組中的Temp類的字節代碼裝入JVM,需要自定義類裝載器類:

這里,loadClass(byte[])方法特用于裝載Temp類的字節代碼,而Temp類引用的其他類型仍由系統隱含調用定義在超類中的loadClass(String)方法裝載,實質上就是委托父類裝載器或引導類裝載器裝載。
只要父類裝載器和編譯參數(getTask方法的第4個參數)設置得當,Temp類和需要計算的表達式就可以引用任何應用程序能夠引用的類型。
裝入Temp類的字節代碼后,就可以利用反射機制執行其中的靜態方法,完成表達式的計算求值:

這里為基于JSF框架的Web應用設計一個名為Calculator的應用程序范圍的托管Bean,其他托管Bean可以調用其calculate(String)方法計算指定的Java表達式:

其中,createSource和execute方法的代碼已在前面給出,calculate(String)方法除需調用上面兩個方法外,主要是要實現代碼的動態編譯,其處理過程與相關技術在前面已進行了詳細介紹,這里不再贅述。
該托管Bean類還定義了若干有名常量。這些常量在每次計算表達式時都是通用的,可以在構造方法中進行設置。這些常量的作用如下:
compiler:編譯器對象。用于創建編譯作業對象以及標準Java文件管理器。
sm:標準Java文件管理器。在創建自定義的FileManagerImpl型文件管理器時,應將該標準文件管理器作為參數傳遞給構造方法。
options:作為編譯器對象的getTask方法的第4個參數,可包含一些編譯參數。如將classpath參數設為Web應用中WEB-INFclasses目錄的實際路徑。
parent:裝載當前Bean類的類裝載器。可作為自定義類裝載器的父類裝載器。
本文采用編譯器API、自定義類裝載器、反射機制等技術實現了Java表達式的動態編譯和計算。該方法已在實際系統中得到了驗證和應用,具有通用、便捷、性能好等特點,達到了滿意的效果。
[1]David Biesack. 使用javax.tools創建動態應用程序[EB/OL]. http://www.ibm.com/ developerworks/cn/java/j-jcomp/#download, 2007-12-24.
[2]James Gosling, Bill Joy. The Java Language Specification Third Edition[M]. Addison Wesley,2005:308-331.
[3]藏旭毅. 淺析J2EE應用服務器的JAVA類裝載器[J]. 電腦知識與技術.2007, 3(18):1609-1610.
[4]陳燁,張蓓. JDK1.5類庫大全[M]. 北京:清華大學出版社.2005:403-419.
10.3969/j.issn.1001-8972.2010.16.063
首都經濟貿易大學教改項目(00790954210333)
婁不夜(1965-),男,副教授,碩士,研究方向為信息技術與信息系統、Web應用。