動態調用動態語言之Java腳本API

火星人 @ 2014-03-10 , reply:0


  我們不需要將動態語言編譯為 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 實現組件處理使用特定腳本語言解釋器的細節.


圖 1:腳本 API 組件關係


  您可能會問:如何才能獲取腳本引擎實現和語言解釋器所需的 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 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 一起編寫的外部腳本文件來定義可在運行時修改的業務邏輯.如您如見,在腳本語言中定義業務規則可以使規則的編寫更加輕鬆,並且更易於程序員之外的人員閱讀,比如說業務分析師或規則編寫人員.





[火星人 via ] 動態調用動態語言之Java腳本API已經有192次圍觀

http://www.coctec.com/docs/java/show-post-61672.html