我們不需要將動態語言編譯為 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("/nCalling defineScriptFunction..."); defineScriptFunction(jsEngine); System.out.println("/nCalling invokeScriptFunctionFromEngine..."); invokeScriptFunctionFromEngine(jsEngine); System.out.println("/nCalling invokeScriptFunctionFromJava..."); invokeScriptFunctionFromJava(jsEngine); System.out.println("/nCalling 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 介面
如前所述,代碼將使用 ScriptEngine 執行個體執行指令碼。指令碼引擎充當指令碼代碼和最後執行代碼的底層語言解譯器或編譯器之間的中間程式。這樣,我們就不需要瞭解各個解譯器使用哪些類來執行指令碼。比如說,JRuby 指令碼引擎可以將代碼傳遞給 JRuby 的 org.jruby.Ruby 類的一個執行個體,首先將指令碼編譯成中間形式,然後再調用它計算指令碼並處理傳回值。指令碼引擎實現隱藏了一些細節,包括解譯器如何與 Java 代碼共用類定義、應用程式物件和輸入/輸出流。
圖 1 顯示了應用程式、Java 指令碼 API 和 ScriptEngine 實現、指令碼語言解譯器之間的總體關係。我們可以看到,應用程式只依賴於指令碼 API,它提供了 ScriptEngineManager 類和 ScriptEngine 介面。ScriptEngine 實現組件處理使用特定指令碼語言解譯器的細節。
您可能會問:如何才能擷取指令碼引擎實現和語言解譯器所需的 JAR 檔案呢?最好的方法是在 java.net 上託管的開源 Scripting 項目中尋找指令碼引擎實現(請參閱 參考資料)。您可以在 java.net 上找到許多語言的指令碼引擎實現和其他網站的連結。Scripting 項目還提供了各種連結,通過這些連結可以下載受支援的指令碼語言的解譯器。
在 清單 1 中,main() 方法將 ScriptEngine 傳遞給各個方法用於計算該方法的 JavaScript 代碼。第一個方法如清單 2 所示。invokeHelloScript() 方法呼叫指令碼引擎的 eval 方法計算和執行 JavaScript 代碼中的特定字串。ScriptEngine 介面定義了 6 個重載的 eval() 方法,用於將接收的指令碼當作字串或 java.io.Reader 對象計算,java.io.Reader 對象一般用於從外部源(例如檔案)讀取指令碼。
清單 2. invokeHelloScript 方法
private static void invokeHelloScript(ScriptEngine jsEngine) throws ScriptException { jsEngine.eval("println('Hello from JavaScript')"); } |
指令碼執行內容
HelloScriptingWorld 應用程式中的樣本指令碼 使用 JavaScript println() 函數向控制台輸出結果,但是我們擁有輸入和輸出資料流的完全控制權。指令碼引擎提供了一個選項用於修改指令碼執行的上下文,這意味著我們可以修改標準輸入資料流、標準輸出資料流和標準錯誤流,同時還可以定義哪些全域變數和 Java 對象對正在執行的指令碼可用。
invokeHelloScript() 方法中的 JavaScript 將 Hello from JavaScript 輸出到標準輸出資料流,在本例中為控制台視窗。(清單 6 含有運行 HelloScriptingWorldApplication 時的完整輸出。)
注意,類中的這一方法和其他方法都聲明拋出了 javax.script.ScriptException。這個選中的異常(指令碼包中定義的惟一一個異常)表示引擎無法解析或執行給定的代碼。所有指令碼引擎 eval() 方法都聲明拋出一個 ScriptException,因此我們的代碼需要適當處理這些異常。
清單 3 顯示了兩個有關的方法:defineScriptFunction() 和 invokeScriptFunctionFromEngine()。defineScriptFunction() 方法還使用一段硬式編碼 JavaScript 代碼呼叫指令碼引擎的 eval() 方法。但是有一點需要注意,該方法的所有工作只是定義了一個 JavaScript 函數 sayHello()。並沒有執行任何代碼。sayHello() 函數只有一個參數,它會使用 println() 語句將這個參數輸出到控制台。指令碼引擎的 JavaScript 解譯器將這個函數添加到全域環境,以供後續的 eval 調用使用(該調用發生在 invokeScriptFunctionFromEngine() 方法中,這並不奇怪)。
清單 3. defineScriptFunction 和 invokeScriptFunctionFromEngine 方法
private static void defineScriptFunction(ScriptEngine engine) throws ScriptException { // Define a function in the script engine engine.eval( "function sayHello(name) {" + " println('Hello, ' + name)" + "}" ); }private static void invokeScriptFunctionFromEngine(ScriptEngine engine) throws ScriptException { engine.eval("sayHello('World!')"); } |
這兩個方法示範了指令碼引擎可以維持應用程式組件的狀態,並且能夠在後續的 eval() 方法調用過程中使用其狀態。invokeScriptFunctionFromEngine() 方法可以利用所維持的狀態,方法是調用定義在 eval() 調用中的 sayHello() JavaScript 函數。
許多指令碼引擎在 eval() 調用之間維持全域變數和函數的狀態。但是有一點值得格外注意,Java 指令碼 API 並不要求指令碼引擎提供這一特性。本文中所使用的 JavaScript、Groovy 和 JRuby 指令碼引擎確實在 eval() 調用之間維持了這些狀態。
清單 4 中的代碼在前一個樣本的基礎上做了幾分修改。原來的 invokeScriptFunctionFromJava() 方法在調用 sayHello() JavaScript 函數時沒有使用 ScriptEngine 的 eval() 方法或 JavaScript 代碼。與此不同,清單 4 中的方法使用 Java 指令碼 API 的 javax.script.Invocable 介面調用由指令碼引擎所維持的函數。invokeScriptFunctionFromJava() 方法將指令碼引擎對象傳遞給 Invocable 介面,然後對該介面調用 invokeFunction() 方法,最終使用給定的參數調用 sayHello() JavaScript 函數。如果調用的函數需要傳回值,則 invokeFunction() 方法會將值封裝為 Java 物件類型並返回。
清單 4. invokeScriptFunctionFromJava 方法
private static void invokeScriptFunctionFromJava(ScriptEngine engine) throws ScriptException, NoSuchMethodException { Invocable invocableEngine = (Invocable) engine; invocableEngine.invokeFunction("sayHello", "from Java"); } |
使用代理實現進階指令碼調用
當指令碼函數或方法實現了一個 Java 介面時,就可以使用進階 Invocable。Invocable 介面定義了一個 getInterface() 方法,該方法使用介面做為參數並且將返回一個實現該介面的 Java 代碼對象。從指令碼引擎獲得代理對象之後,可以將它作為正常的 Java 對象對待。對該代理調用的方法將委託給指令碼引擎通過指令碼語言執行。
注意,清單 4 中沒有 JavaScript 代碼。Invocable 介面允許 Java 代碼呼叫指令碼函數,而無需知道其實現語言。如果指令碼引擎無法找到給定名稱或參數類型的函數,那麼 invokeFunction() 方法將拋出一個 java.lang.NoSuchMethodException。
Java 指令碼 API 並不要求指令碼引擎實現 Invocable 介面。實際上,清單 4 中的代碼應該使用 instanceof 運算子確保指令碼引擎在轉換(cast)之前實現了 Invocable 介面。
通過指令碼代碼調用 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 JavaScriptCalling 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 一起編寫的外部指令檔來定義可在運行時修改的商務邏輯。如您如見,在指令碼語言中定義商務規則可以使規則的編寫更加輕鬆,並且更易於程式員之外的人員閱讀,比如說商務分析師或規則編寫人員。