簡(jiǎn)介: Java? 語(yǔ)言足以滿(mǎn)足您的一些項目的需求,但是腳本語(yǔ)言在性能方面一直表現不佳。Java Scripting API (javax.script) 支持在 Java 程序中調用腳本,反之亦然,通過(guò)本文,您將了解它在這兩方面是如何做到最好的。
現在,許多 Java 開(kāi)發(fā)人員都喜歡在 Java 平臺中使用腳本語(yǔ)言,但是使用編譯到 Java 字節碼中的動(dòng)態(tài)語(yǔ)言有時(shí)是不可行的。在某些情況中,直接編寫(xiě)一個(gè) Java 應用程序的腳本 部分 或者在一個(gè)腳本中調用特定的 Java 對象是更快捷、更高效的方法。
這就是 javax.script 產(chǎn)生的原因了。Java Scripting API 是從 Java 6
開(kāi)始引入的,它填補了便捷的小腳本語(yǔ)言和健壯的 Java 生態(tài)系統之間的鴻溝。通過(guò)使用 Java Scripting
API,您就可以在您的 Java 代碼中快速整合幾乎所有的腳本語(yǔ)言,這使您能夠在解決一些很小的問(wèn)題時(shí)有更多可選擇的方法。
1. 使用 jrunscript 執行 JavaScript
每一個(gè)新的 Java 平臺發(fā)布都會(huì )帶來(lái)新的命令行工具集,它們位于 JDK 的 bin 目錄。Java 6 也一樣,其中
jrunscript 便是 Java 平臺工具集中的一個(gè)不小的補充。
設想一個(gè)編寫(xiě)命令行腳本進(jìn)行性能監控的簡(jiǎn)單問(wèn)題。這個(gè)工具將借用 jmap(見(jiàn)本系列文章
前一篇文章
中的介紹),每 5 秒鐘運行一個(gè) Java 進(jìn)程,從而了解進(jìn)程的運行狀況。一般情況下,我們會(huì )使用命令行 shell 腳本來(lái)完成這樣的工作,但是這里的服務(wù)器應用程序部署在一些差別很大的平臺上,包括 Windows? 和
Linux?。系統管理員將會(huì )發(fā)現編寫(xiě)能夠同時(shí)運行在兩個(gè)平臺的 shell 腳本是很痛苦的。通常的做法是編寫(xiě)一個(gè) Windows
批處理文件和一個(gè) UNIX? shell 腳本,同時(shí)保證這兩個(gè)文件同步更新。
但是,任何閱讀過(guò) The Pragmatic Programmer 的人都知道,這嚴重違反了 DRY (Don't Repeat Yourself) 原則,而且會(huì )產(chǎn)生許多缺陷和問(wèn)題。我們真正希望的是編寫(xiě)一種與操作系統無(wú)關(guān)的腳本,它能夠在所有的平臺上運行。
當然,Java 語(yǔ)言是平臺無(wú)關(guān)的,但是這里并不是需要使用 “系統” 語(yǔ)言的情況。我們需要的是一種腳本語(yǔ)言 — 如,JavaScript。
清單 1 顯示的是我們所需要的簡(jiǎn)單 shell 腳本:
while (true)
{
echo("Hello, world!");
}
|
由于經(jīng)常與 Web 瀏覽器打交道,許多 Java 開(kāi)發(fā)人員已經(jīng)知道了 JavaScript(或 ECMAScript;JavaScript 是由 Netscape 開(kāi)發(fā)的一種 ECMAScript 語(yǔ)言)。問(wèn)題是,系統管理員要如何運行這個(gè)腳本?
當然,解決方法是 JDK 所帶的 jrunscript 實(shí)用程序,如清單 2 所示:
C:\developerWorks\5things-scripting\code\jssrc>jrunscript periodic.js
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
...
|
注意,您也可以使用 for 循環(huán)按照指定的次數來(lái)循環(huán)執行這個(gè)腳本,然后才退出?;旧?,jrunscript
能夠讓您執行 JavaScript 的所有操作。惟一不同的是它的運行環(huán)境不是瀏覽器,所以運行中不會(huì )有
DOM。因此,最頂層的函數和對象稍微有些不同。
因為 Java 6 將 Rhino ECMAScript 引擎作為 JDK 的一部分,jrunscript
可以執行任何傳遞給它的 ECMAScript 代碼,不管是一個(gè)文件(如此處所示)或是在更加交互式的 REPL(“Read-Evaluate-Print-Loop”)shell 環(huán)境。運行 jrunscript
就可以訪(fǎng)問(wèn) REPL shell。
2. 從腳本訪(fǎng)問(wèn) Java 對象
能夠編寫(xiě) JavaScript/ECMAScript 代碼是非常好的,但是我們不希望被迫重新編譯我們在 Java 語(yǔ)言中使用的所有代碼 —
這是違背我們初衷的。幸好,所有使用 Java Scripting API 引擎的代碼都完全能夠訪(fǎng)問(wèn)整個(gè) Java
生態(tài)系統,因為本質(zhì)上一切代碼都還是 Java 字節碼。所以,回到我們之前的問(wèn)題,我們可以在 Java 平臺上使用傳統的
Runtime.exec() 調用來(lái)啟動(dòng)進(jìn)程,如清單 3 所示:
var p = java.lang.Runtime.getRuntime().exec("jmap", [ "-histo", arguments[0] ])
p.waitFor()
|
數組 arguments 是指向傳遞到這個(gè)函數參數的 ECMAScript
標準內置引用。在最頂層的腳本環(huán)境中,則是傳遞給腳本本身的的參數數組(命令行參數)。所以,在清單 3
中,這個(gè)腳本預期接收一個(gè)參數,該參數包含要映射的 Java 進(jìn)程的 VMID。
除此之外,我們可以利用本身為一個(gè) Java 類(lèi)的 jmap,然后直接調用它的
main() 方法,如清單 4 所示。有了這個(gè)方法,我們不需要 “傳輸” Process
對象的 in/out/err 流。
var args = [ "-histo", arguments[0] ]
Packages.sun.tools.jmap.JMap.main(args)
|
Packages 語(yǔ)法是一個(gè) Rhino ECMAScript 標識,它指向已經(jīng) Rhino 內創(chuàng )建的位于核心
java.* 包之外的 Java 包。
3. 從 Java 代碼調用腳本
從腳本調用 Java 對象僅僅完成了一半的工作:Java 腳本環(huán)境也提供了從 Java 代碼調用腳本的功能。這只需要實(shí)例化一個(gè)
ScriptEngine 對象,然后加載和評估腳本,如清單 5 所示:
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() 方法也可以直接操作一個(gè) String,所以這個(gè)腳本不一定必須是文件系統的一個(gè)文件 —
它可以來(lái)自于數據庫、用戶(hù)輸入,或者甚至可以基于環(huán)境和用戶(hù)操作在應用程序中生成。
4. 將 Java 對象綁定到腳本空間
僅僅調用一個(gè)腳本還不夠:腳本通常會(huì )與 Java 環(huán)境中創(chuàng )建的對象進(jìn)行交互。這時(shí),Java
主機環(huán)境必須創(chuàng )建一些對象并將它們綁定,這樣腳本就可以很容易找到和使用這些對象。這個(gè)過(guò)程是
ScriptContext 對象的任務(wù),如清單 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();
}
}
}
|
訪(fǎng)問(wèn)所綁定的對象很簡(jiǎn)單 — 所綁定對象的名稱(chēng)是作為全局命名空間引入到腳本的,所以在
Rhino 中使用 Person 很簡(jiǎn)單,如清單 7 所示:
println("Hello from inside scripting!")
println("author.firstName = " + author.firstName)
|
您可以看到,JavaBeans 樣式的屬性被簡(jiǎn)化為使用名稱(chēng)直接訪(fǎng)問(wèn),這就好像它們是字段一樣。
5. 編譯頻繁使用的腳本
腳本語(yǔ)言的缺點(diǎn)一直存在于性能方面。其中的原因是,大多數情況下腳本語(yǔ)言是 “即時(shí)” 解譯的,因而它在執行時(shí)會(huì )損失一些解析和驗證文本的時(shí)間和 CPU 周期。運行在 JVM 的許多腳本語(yǔ)言最終會(huì )將接收的代碼轉換為 Java 字節碼,至少在腳本被第一次解析和驗證時(shí)進(jìn)行轉換;在 Java 程序關(guān)閉時(shí),這些即時(shí)編譯的代碼會(huì )消失。將頻繁使用的腳本保持為字節碼形式可以幫助提升可觀(guān)的性能。
我們可以以一種很自然和有意義的方法使用 Java Scripting API。如果返回的 ScriptEngine
實(shí)現了 Compilable 接口,那么這個(gè)接口所編譯的方法可用于將腳本(以一個(gè)
String 或一個(gè) Reader 傳遞過(guò)來(lái)的)編譯為一個(gè)
CompiledScript 實(shí)例,然后它可用于在 eval()
方法中使用不同的綁定重復地處理編譯后的代碼,如清單 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
實(shí)例需要存儲在一個(gè)長(cháng)時(shí)間存儲中(例如,servlet-context),這樣才能避免一次次地重復編譯相同的腳本。然而,如果腳本發(fā)生變化,您就需要創(chuàng )建一個(gè)新的
CompiledScript 來(lái)反映這個(gè)變化;一旦編譯完成,CompiledScript
就不再執行原始的腳本文件內容。
結束語(yǔ)
Java Scripting API 在擴展 Java 程序的范圍和功能方面前進(jìn)了很大一步,并且它將腳本語(yǔ)言的編碼效率的優(yōu)勢帶到 Java
環(huán)境。jrunscript
— 它顯然不是很難編寫(xiě)的程序 — 以及
javax.script 給 Java 開(kāi)發(fā)人員帶來(lái)了諸如 Ruby (JRuby) 和 ECMAScript (Rhino)
等腳本語(yǔ)言的優(yōu)勢,同時(shí)還不會(huì )破壞 Java 環(huán)境的生態(tài)系統和可擴展性。
請繼續閱讀下一篇文章 5 件事 系列文章:JDBC。
參考資料
學(xué)習
jmap。聯(lián)系客服