我們不需要將動態語言編譯為 Java字節碼就可以在 Java 應用程序中使用它們。使用 Java Platform, Standard Edition 6 (Java SE)中添加的腳本包(並且向後兼容 Java SE 5),Java 代碼可以在運行時以一種簡單的、統一的方式調用多種動態語言。本系列文章共分兩個部分,第 1 部分將介紹 Java 腳本 API 的各種特性。文章將使用一個簡單的 Hello World 應用程序展示 Java 代碼如何執行腳本代碼以及腳本如何反過來執行 Java 代碼。第 2 部分將深入研究 Java 腳本 API 的強大功能。
Java 開發人員清楚 Java 並不是在任何情況下都是最佳的語言。今年,1.0 版本的 JRuby 和 Groovy 的發行引領了一場熱潮,促使人們紛紛在自己的 Java 應用程序中添加動態語言。Groovy、JRuby、Rhino、Jython 和一些其他的開源項目使在所謂的腳本語言中編寫代碼並在 JVM 中運行成為了可能(請參閱 參考資料)。通常,在 Java 代碼中集成這些語言需要對各種解釋器所特有的 API 和特性有所了解。
Java SE 6 中添加的 javax.script 包使集成動態語言更加輕易。通過使用一小組接口和具體類,這個包使我們能夠簡單地調用多種腳本語言。但是,Java 腳本 API 的功能不只是在應用程序中編寫腳本;這個腳本包使我們能夠在運行時讀取和調用外部腳本,這意味著我們可以動態地修改這些腳本從而更改運行應用程序的行為。
Java 腳本 API
腳本與動態的對比
術語腳本 通常表示在解釋器 shell 中運行的語言,它們往往沒有單獨的編譯步驟。術語動態 通常表示等到運行時判定變量類型或對象行為的語言,往往具有閉包和連續特性。一些通用的編程語言同時具有這兩種特性。此處首選腳本語言 是因為本文的著重點是 Java 腳本 API,而不是因為提及的語言缺少動態特性。
2006 年 10 月,Java 語言添加了腳本包,從而提供了一種統一的方式將腳本語言集成到 Java 應用程序中去。對於語言開發人員,他們可以使用這個包編寫粘連代碼(glue code),從而使人們能夠在 Java 應用程序中調用他們的語言。對於 Java 開發人員,腳本包提供了一組類和接口,答應使用一個公共 API 調用多種語言編寫的腳本。因此,腳本包類似於不同語言(比如說不同的數據庫)中的 Java Database Connectivity (JDBC) 包,可以使用一致的接口集成到 Java 平台中去。
以前,在 Java 代碼中,動態調用腳本語言涉及到使用各種語言發行版所提供的獨特類或使用 Apache 的 Jakarta Bean Scripting Framework (BSF)。BSF 在一個 API 內部統一了一組腳本語言(請參閱 參考資料)。使用 Java SE 6 腳本 API,二十余種腳本語言(AppleScript、Groovy、javascript、Jelly、PHP、Python、Ruby 和 Velocity)都可以集成到 Java 代碼中,這在很大程序上依靠的是 BSF。
腳本 API 在 Java 應用程序和外部腳本之間提供了雙向可見性。Java 代碼不僅可以調用外部腳本,而且還答應那些腳本訪問選定的 Java 對象。比如說,外部 Ruby 腳本可以對 Java 對象調用方法,並訪問對象的屬性,從而使腳本能夠將行為添加到運行中的應用程序中(假如在開發時無法預計應用程序的行為)。
調用外部腳本可用於運行時應用程序增強、配置、監控或一些其他的運行時操作,比如說在不停止應用程序的情況下修改業務規則。腳本包可能的作用包括:
·在比 Java 語言更簡單的語言中編寫業務規則,而不用借助成熟的規則引擎。
·創建插件架構,使用戶能夠動態地定制應用程序。
·將已有腳本集成到 Java 應用程序中,比如說處理或轉換文件文章的腳本。
·使用成熟的編程語言(而不是屬性文件)從外部配置應用程序的運行時行為。
·在 Java 應用程序中添加一門特定於域的語言(domain-specific language)。
·在開發 Java 應用程序原型的過程中使用腳本語言。
·在腳本語言中編寫應用程序測試代碼。
你好,腳本世界
HelloScriptingWorld 類(本文中的相關代碼均可從 下載部分 獲得)演示了 Java 腳本包的一些要害特性。它使用硬編碼的 JavaScript 作為示例腳本語言。此類的 main() 方法(如清單 1 所示)將創建一個 JavaScript 腳本引擎,然後分別調用五個方法(在下文的清單中有顯示)用於突出顯示腳本包的特性。
清單 1. HelloScriptingWorld main 方法
public static void main(String[] args) throws ScriptException, NoSUChMethodException {
ScriptEngineManager scriptEngineMgr = new ScriptEngineManager();
ScriptEngine jsEngine = scriptEngineMgr.getEngineByName("JavaScript");
if (jsEngine == null) {
System.err.println("No script engine found for JavaScript");
System.exit(1);
}
System.out.println("Calling invokeHelloScript...");
invokeHelloScript(jsEngine);
System.out.println("
Calling defineScriptFunction...");
defineScriptFunction(jsEngine);
System.out.println("
Calling invokeScriptFunctionFromEngine...");
invokeScriptFunctionFromEngine(jsEngine);
System.out.println("
Calling invokeScriptFunctionFromJava...");
invokeScriptFunctionFromJava(jsEngine);
System.out.println("
Calling invokeJavaFromScriptFunction...");
invokeJavaFromScriptFunction(jsEngine);
}
main() 方法的主要功能是獲取一個 javax.script.ScriptEngine 實例(清單 1 中的前兩行代碼)。腳本引擎可以在特定的語言中加載並執行腳本。它是 Java 腳本包中使用最為頻繁、作用最為重要的類。我們從 javax.script.ScriptEngineManager 獲取一個腳本引擎(第一行代碼)。通常,程序只需要獲取一個腳本引擎實例,除非使用了很多種腳本語言。
ScriptEngineManager 類
ScriptEngineManager 可能是腳本包中惟一一個經常使用的具體類;其他大多數都是接口。它或許是腳本包中惟一的一個要直接或間接地(通過 Spring Framework 之類的依靠性注入機制)實例化的類。ScriptEngineManager 可以使用以下三種方式返回腳本引擎:
·通過引擎或語言的名稱,比如說 清單 1 請求 JavaScript 引擎。
·通過該語言腳本共同使用的文件擴展名,比如說 Ruby 腳本的 .rb。
·通過腳本引擎聲明的、知道如何處理的 MIME 類型。
本文示例為什麼要使用 JavaScript?
本文中的 Hello World 示例使用了部分 JavaScript 腳本,這是因為 JavaScript 代碼易於理解,不過主要還是因為 Sun Microsystems 和 BEA Systems 所提供的 Java 6 運行時環境附帶有基於 Mozilla Rhino 開源 JavaScript 實現的 JavaScript 解釋器。使用 JavaScript,我們無需在類路徑中添加腳本語言 JAR 文件。
ScriptEngineManager 間接查找和創建腳本引擎。也就是說,當實例化腳本引擎治理程序時,ScriptEngineManager 會使用 Java 6 中新增的服務發現機制在類路徑中查找所有注冊的 javax.script.ScriptEngineFactory 實現。這些工廠類封裝在 Java 腳本 API 實現中;也許您永遠都不需要直接處理這些工廠類。
ScriptEngineManager 找到所有的腳本引擎工廠類之後,它會查詢各個類並判定是否能夠創建所請求類型的腳本引擎 —— 清單 1 中為 JavaScript 引擎。假如工廠說可以創建所需語言的腳本引擎,那麼治理程序將要求工廠創建一個引擎並將其返回給調用者。假如沒有找到所請求語言的工廠,那麼治理程序將返回 null,清單 1 中的代碼將檢查 null 返回值並做出預防。
ScriptEngine 接口
圖 1:腳本 API 組件關系
通過腳本代碼調用 Java 方法
清單 3 和 清單 4 中的示例展示了 Java 代碼如何調用腳本語言中定義的函數或方法。您可能會問:腳本語言中編寫的代碼是否可以反過來對 Java 對象調用方法呢?答案是可以。清單 5 中的 invokeJavaFromScriptFunction() 方法顯示了如何使腳本引擎能夠訪問 Java 對象,以及腳本代碼如何才能對這些 Java 對象調用方法。明確的說,invokeJavaFromScriptFunction() 方法使用腳本引擎的 put() 方法將 HelloScriptingWorld 類的實例本身提供給引擎。當引擎擁有 Java 對象的訪問權之後(使用 put() 調用所提供的名稱),eval() 方法腳本中的腳本代碼將使用該對象。
清單 5. invokeJavaFromScriptFunction 和 getHelloReply 方法
private static void invokeJavaFromScriptFunction(ScriptEngine engine)
throws ScriptException
{
engine.put("helloScriptingWorld", new HelloScriptingWorld());
engine.eval(
"println('Invoking getHelloReply method from JavaScript...');" +
"var msg = helloScriptingWorld.getHelloReply(vJavaScript');" +
"println('Java returned: ' + msg)"
);
}
/** Method invoked from the above script to return a string. */
public String getHelloReply(String name) {
return "Java method getHelloReply says, 'Hello, " + name + "'";
}
清單 5 中的 eval() 方法調用中所包含的 JavaScript 代碼使用腳本引擎的 put() 方法調用所提供的變量名稱 helloScriptingWorld 訪問並使用 HelloScriptingWorld Java 對象。清單 5 中的第二行 JavaScript 代碼將調用 getHelloReply() 公有 Java 方法。getHelloReply() 方法將返回 Java method getHelloReply says, 'Hello, <parameter>' 字符串。eval() 方法中的 JavaScript 代碼將 Java 返回值賦給 msg 變量,然後再將其打印輸出給控制台。
Java 對象轉換
當腳本引擎使運行於引擎環境中的腳本能夠使用 Java 對象時,引擎需要將其封裝到適用於該腳本語言的對象類型中。封裝可能會涉及到一些適當的對象-值轉換,比如說答應 Java Integer 對象直接在腳本語言的數學表達式中使用。關於如何將 Java 對象轉換為腳本對象的研究是與各個腳本語言的引擎非凡相關的,並且不在本文的討論范圍之內。但是,您應該意識到轉換的發生,因為可以通過測試來確保所使用的腳本語言執行轉換的方式符合您的期望。
ScriptEngine.put 及其相關 get() 方法是在運行於腳本引擎中的 Java 代碼和腳本之間共享對象和數據的主要途徑。(有關這一方面的具體論述,請參閱本文後面的 Script-execution scope 一節。)當我們調用引擎的 put() 方法時,腳本引擎會將第二個參數(任何 Java 對象)關聯到特定的字符串要害字。大多數腳本引擎都是讓腳本使用特定的變量名稱來訪問 Java 對象。腳本引擎可以隨意對待傳遞給 put() 方法的名稱。比如說,JRuby 腳本引擎讓 Ruby 代碼使用全局 $helloScriptingWorld 對象訪問 helloScriptingWorld,以符合 Ruby 全局變量的語法。
腳本引擎的 get() 方法檢索腳本環境中可用的值。一般而言,Java 代碼通過 get() 方法可以訪問腳本環境中的所有全局變量和函數。但是只有明確使用 put() 與腳本共享的 Java 對象才可以被腳本訪問。
外部腳本在運行著的應用程序中訪問和操作 Java 對象的這種功能是擴展 Java 程序功能的一項強有力的技巧。(第 2 部分將通過示例研究這一技巧)。
運行 HelloScriptingWorld 應用程序
您可以通過下載和構建源代碼來運行 HelloScriptingWorld 應用程序。此 .zip 中文件含有一個 Ant 腳本和一個 Maven 構建腳本,可以幫助大家編譯和運行示例應用程序。請執行以下步驟:
·下載 此 .zip 文件。
·創建一個新目錄,比如說 java-scripting,並將步驟 1 中所下載的文件解壓到該目錄中。
·打開命令行 shell 並轉到該目錄。
·運行 ant run-hello 命令。
您應該可以看到類似於清單 6 的 Ant 控制台輸出。注重,defineScriptFunction() 函數沒有產生任何輸出,因為它雖然定義了輸出但是卻沒有調用 JavaScript 函數。
清單 6. 運行 HelloScriptingWorld 時的輸出
Calling invokeHelloScript...
Hello from JavaScript
Calling defineScriptFunction...
Calling invokeScriptFunctionFromEngine...
Hello, World!
Calling invokeScriptFunctionFromJava...
Hello, from Java
Calling invokeJavaFromScriptFunction...
Invoking getHelloReply method from JavaScript...
Java returned: Java method getHelloReply says, 'Hello, JavaScript'
Java 5 兼容性
Java SE 6 引入了 Java 腳本 API,但是您也可以使用 Java SE 5 運行此 API。只需要提供缺少的 javax.script 包類的一個實現即可。所幸的是,Java Specification Request 223 參考實現中含有這個實現(請參閱 參考資料 獲得下載鏈接。)JSR 223 對 Java 腳本 API 做出了定義。
假如您已經下載了 JSR 223 參考實現,解壓下載文件並將 script-api.jar、script-js.jar 和 js.jar 文件復制到您的類路徑下。這些文件將提供腳本 API、JavaScript 腳本引擎接口和 Java SE 6 中所附帶的 JavaScript 腳本引擎。
腳本執行作用域
與簡單地調用引擎的 get() 和 put() 方法相比,如何將 Java 對象公開給運行於腳本引擎中的腳本具有更好的可配置性。當我們在腳本引擎上調用 get() 或 put() 方法時,引擎將會在 javax.script.Bindings 接口的默認實例中檢索或保存所請求的要害字。(Bindings 接口只是一個 Map 接口,用於強制要害字為字符串。)
當代碼調用腳本引擎的 eval() 方法時,將使用引擎默認綁定的要害字和值。但是,您可以為 eval() 調用提供自己的 Bindings 對象,以限制哪些變量和對象對於該特定腳本可見。該調用外表上類似於 eval(String, Bindings) 或 eval(Reader, Bindings)。要幫助您創建自定義的 Bindings,腳本引擎將提供一個 createBindings() 方法,該方法和返回值是一個內容為空的 Bindings 對象。使用 Bindings 對象臨時調用 eval 將隱藏先前保存在引擎默認綁定中的 Java 對象。
要添加功能,腳本引擎含有兩個默認綁定:其一為 get() 和 put() 調用所使用的 “引擎作用域” 綁定 ;其二為 “全局作用域” 綁定,當無法在 “引擎作用域” 中找到對象時,引擎將使用第二種綁定進行查找。腳本引擎並不需要使腳本能夠訪問全局綁定。大多數腳本都可以訪問它。
“全局作用域” 綁定的設計目的是在不同的腳本引擎之間共享對象。ScriptEngineManager 實例返回的所有腳本引擎都是 “全局作用域” 綁定對象。您可以使用 getBindings(ScriptContext.GLOBAL_SCOPE) 方法檢索某個引擎的全局綁定,並且可以使用 setBindings(Bindings, ScriptContext.GLOBAL_SCOPE) 方法為引擎設置全局綁定。
ScriptContext 是一個定義和控制腳本引擎運行時上下文的接口。腳本引擎的 ScriptContext 含有 “引擎” 和 “全局” 作用域綁定,以及用於標准輸入和輸出操作的輸入和輸出流。您可以使用引擎的 getContext() 方法獲取並操作腳本引擎的上下文。
一些腳本 API 概念,比如說作用域、綁定 和上下文,開始看來會令人迷惑,因為它們的含義有交叉的地方。本文的源代碼下載文件含有一個名為 ScriptApiRhinoTest 的 JUnit 測試文件,位於 src/test/java Directory 目錄,該文件可以通過 Java 代碼幫助解釋這些概念。
未來的計劃
現在,大家已經對 Java 腳本 API 有了最基本的熟悉,本系列文章的第 2 部分將在此基礎上進行擴展,為大家演示一個更為實際的示例應用程序。該應用程序將使用 Groovy、Ruby 和 JavaScript 一起編寫的外部腳本文件來定義可在運行時修改的業務邏輯。如您如見,在腳本語言中定義業務規則可以使規則的編寫更加輕松,並且更易於程序員之外的人員閱讀,比如說業務分析師或規則編寫人員。