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