Java 平台上更簡單的腳本編寫方法
現在,許多 Java 開發人員都喜歡在 Java 平台中使用腳本語言,但是使用編譯到 Java 字節碼中的動態語言有時是不可行的。在某些情況中,直接編寫一個 Java 應用程序的腳本 部分 或者在一個腳本中調用特定的 Java 對象是更快捷、更高效的方法。
這就是 javax.script 產生的原因了。Java Scripting API 是從 Java 6 開始引入的,它填補了便捷的小腳本語言和健壯的 Java 生態系統之間的鴻溝。通過使用 Java Scripting API,您就可以在您的 Java 代碼中快速整合幾乎所有的腳本語言,這使您能夠在解決一些很小的問題時有更多可選擇的方法。
1. 使用 jrunscript 執行 JavaScript
每一個新的 Java 平台發布都會帶來新的命令行工具集,它們位於 JDK 的 bin 目錄。Java 6 也一樣,其中 jrunscript 便是 Java 平台工具集中的一個不小的補充。
設想一個編寫命令行腳本進行性能監控的簡單問題。這個工具將借用 jmap(見本系列文章 前一篇文章 中的介紹),每 5 秒鐘運行一個 Java 進程,從而了解進程的運行狀況。一般情況下,我們會使用命令行 shell 腳本來完成這樣的工作,但是這裡的服務器應用程序部署在一些差別很大的平台上,包括 Windows® 和 Linux®。系統管理員將會發現編寫能夠同時運行在兩個平台的 shell 腳本是很痛苦的。通常的做法是編寫一個 Windows 批處理文件和一個 UNIX® shell 腳本,同時保證這兩個文件同步更新。
但是,任何閱讀過 The Pragmatic Programmer 的人都知道,這嚴重違反了 DRY (Don't Repeat Yourself) 原則,而且會產生許多缺陷和問題。我們真正希望的是編寫一種與操作系統無關的腳本,它能夠在所有的平台上運行。
當然,Java 語言是平台無關的,但是這裡並不是需要使用 “系統” 語言的情況。我們需要的是一種腳本語言 — 如,JavaScript。
清單 1 顯示的是我們所需要的簡單 shell 腳本:
清單 1. periodic.JS
- while (true)
- {
- echo("Hello, world!");
- }
由於經常與 Web 浏覽器打交道,許多 Java 開發人員已經知道了 Javascript(或 ECMAScript;JavaScript 是由 Netscape 開發的一種 ECMAScript 語言)。問題是,系統管理員要如何運行這個腳本?
當然,解決方法是 JDK 所帶的 jrunscript 實用程序,如清單 2 所示:
清單 2. jrunscript
- C:\developerWorks\5things-scripting\code\JSsrc>jrunscript periodic.JS
- Hello, world!
- Hello, world!
- Hello, world!
- Hello, world!
- Hello, world!
- Hello, world!
- Hello, world!
- ...
注意,您也可以使用 for 循環按照指定的次數來循環執行這個腳本,然後才退出。基本上,jrunscript 能夠讓您執行 JavaScript 的所有操作。惟一不同的是它的運行環境不是浏覽器,所以運行中不會有 DOM。因此,最頂層的函數和對象稍微有些不同。
因為 Java 6 將 Rhino ECMAScript 引擎作為 JDK 的一部分,jrunscript 可以執行任何傳遞給它的 ECMAScript 代碼,不管是一個文件(如此處所示)或是在更加交互式的 REPL(“Read-Evaluate-Print-Loop”)shell 環境。運行 jrunscript 就可以訪問 REPL shell。
2. 從腳本訪問 Java 對象
能夠編寫 Javascript/ECMAScript 代碼是非常好的,但是我們不希望被迫重新編譯我們在 Java 語言中使用的所有代碼 — 這是違背我們初衷的。幸好,所有使用 Java Scripting API 引擎的代碼都完全能夠訪問整個 Java 生態系統,因為本質上一切代碼都還是 Java 字節碼。所以,回到我們之前的問題,我們可以在 Java 平台上使用傳統的 Runtime.exec() 調用來啟動進程,如清單 3 所示:
清單 3. Runtime.exec() 啟動 jmap
- var p = Java.lang.Runtime.getRuntime().exec("jmap", [ "-histo", arguments[0] ])
- p.waitFor()
數組 arguments 是指向傳遞到這個函數參數的 ECMAScript 標准內置引用。在最頂層的腳本環境中,則是傳遞給腳本本身的的參數數組(命令行參數)。所以,在清單 3 中,這個腳本預期接收一個參數,該參數包含要映射的 Java 進程的 VMID。
除此之外,我們可以利用本身為一個 Java 類的 jmap,然後直接調用它的 main() 方法,如清單 4 所示。有了這個方法,我們不需要 “傳輸” Process 對象的 in/out/err 流。
清單 4. JMap.main()
- var args = [ "-histo", arguments[0] ]
- Packages.sun.tools.jmap.JMap.main(args)
Packages 語法是一個 Rhino ECMAScript 標識,它指向已經 Rhino 內創建的位於核心 java.* 包之外的 Java 包。
3. 從 Java 代碼調用腳本
從腳本調用 Java 對象僅僅完成了一半的工作:Java 腳本環境也提供了從 Java 代碼調用腳本的功能。這只需要實例化一個 ScriptEngine 對象,然後加載和評估腳本,如清單 5 所示:
清單 5. Java 平台的腳本調用
- import Java.io.*;
- import Javax.script.*;
- public class App
- {
- public static void main(String[] args)
- {
- try
- {
- ScriptEngine engine =
- new ScriptEngineManager().getEngineByName("Javascript");
- for (String arg : args)
- {
- FileReader fr = new FileReader(arg);
- engine.eval(fr);
- }
- }
- catch(IOException ioEx)
- {
- ioEx.printStackTrace();
- }
- catch(ScriptException scrEx)
- {
- scrEx.printStackTrace();
- }
- }
- }
eval() 方法也可以直接操作一個 String,所以這個腳本不一定必須是文件系統的一個文件 — 它可以來自於數據庫、用戶輸入,或者甚至可以基於環境和用戶操作在應用程序中生成。
4. 將 Java 對象綁定到腳本空間
僅僅調用一個腳本還不夠:腳本通常會與 Java 環境中創建的對象進行交互。這時,Java 主機環境必須創建一些對象並將它們綁定,這樣腳本就可以很容易找到和使用這些對象。這個過程是 ScriptContext 對象的任務,如清單 6 所示:
清單 6. 為腳本綁定對象
- import Java.io.*;
- import Javax.script.*;
- public class App
- {
- public static void main(String[] args)
- {
- try
- {
- ScriptEngine engine =
- new ScriptEngineManager().getEngineByName("Javascript");
- for (String arg : args)
- {
- Bindings bindings = new SimpleBindings();
- bindings.put("author", new Person("Ted", "Neward", 39));
- bindings.put("title", "5 Things You Didn't Know");
- FileReader fr = new FileReader(arg);
- engine.eval(fr, bindings);
- }
- }
- catch(IOException ioEx)
- {
- ioEx.printStackTrace();
- }
- catch(ScriptException scrEx)
- {
- scrEx.printStackTrace();
- }
- }
- }
訪問所綁定的對象很簡單 — 所綁定對象的名稱是作為全局命名空間引入到腳本的,所以在 Rhino 中使用 Person 很簡單,如清單 7 所示:
清單 7.
- println("Hello from inside scripting!")
- println("author.firstName = " + author.firstName)
您可以看到,JavaBeans 樣式的屬性被簡化為使用名稱直接訪問,這就好像它們是字段一樣。
5. 編譯頻繁使用的腳本
腳本語言的缺點一直存在於性能方面。其中的原因是,大多數情況下腳本語言是 “即時” 解譯的,因而它在執行時會損失一些解析和驗證文本的時間和 CPU 周期。運行在 JVM 的許多腳本語言最終會將接收的代碼轉換為 Java 字節碼,至少在腳本被第一次解析和驗證時進行轉換;在 Java 程序關閉時,這些即時編譯的代碼會消失。將頻繁使用的腳本保持為字節碼形式可以幫助提升可觀的性能。
我們可以以一種很自然和有意義的方法使用 Java Scripting API。如果返回的 ScriptEngine 實現了 Compilable 接口,那麼這個接口所編譯的方法可用於將腳本(以一個 String 或一個 Reader 傳遞過來的)編譯為一個 CompiledScript 實例,然後它可用於在 eval() 方法中使用不同的綁定重復地處理編譯後的代碼,如清單 8 所示:
清單 8. 編譯解譯後的代碼
- import Java.io.*;
- import Javax.script.*;
- public class App
- {
- public static void main(String[] args)
- {
- try
- {
- ScriptEngine engine =
- new ScriptEngineManager().getEngineByName("Javascript");
- for (String arg : args)
- {
- Bindings bindings = new SimpleBindings();
- bindings.put("author", new Person("Ted", "Neward", 39));
- bindings.put("title", "5 Things You Didn't Know");
- FileReader fr = new FileReader(arg);
- if (engine instanceof Compilable)
- {
- System.out.println("Compiling....");
- Compilable compEngine = (Compilable)engine;
- CompiledScript cs = compEngine.compile(fr);
- cs.eval(bindings);
- }
- else
- engine.eval(fr, bindings);
- }
- }
- catch(IOException ioEx)
- {
- ioEx.printStackTrace();
- }
- catch(ScriptException scrEx)
- {
- scrEx.printStackTrace();
- }
- }
- }
在大多數情況中,CompiledScript 實例需要存儲在一個長時間存儲中(例如,servlet-context),這樣才能避免一次次地重復編譯相同的腳本。然而,如果腳本發生變化,您就需要創建一個新的 CompiledScript 來反映這個變化;一旦編譯完成,CompiledScript 就不再執行原始的腳本文件內容。
結束語
Java Scripting API 在擴展 Java 程序的范圍和功能方面前進了很大一步,並且它將腳本語言的編碼效率的優勢帶到 Java 環境。jrunscript — 它顯然不是很難編寫的程序 — 以及 javax.script 給 Java 開發人員帶來了諸如 Ruby (JRuby) 和 ECMAScript (Rhino) 等腳本語言的優勢,同時還不會破壞 Java 環境的生態系統和可擴展性。
關於作者
Ted Neward
Ted Neward是Neward&Associates的總裁,從事關於Java、.Net、XML Services以及其他平台方面的咨詢、指導和演示等工作。他居住在華盛頓西雅圖。