Mustang 的指令碼引擎
JSR 233 為 Java 設計了一套指令碼語言 API。這一套 API 提供了在 Java 程式中調用各種指令碼語言引擎的介面。任何實現了這一介面的指令碼語言引擎都可以在 Java 程式中被調用。在 Mustang 的發行版本中包括了一個基於 Mozilla Rhino 的 JavaScript 指令碼引擎。
Mozilla Rhino
Rhino 是一個純 Java 的開源的 JavaScript 實現。他的名字來源於 O'Reilly 關於 JavaScript 的書的封面:
Rhino 項目可以追朔到 1997 年,當時 Netscape 計劃開發一個純 Java 實現的 Navigator,為此需要一個 Java 實現的 JavaScript —— Javagator。它也就是 Rhino 的前身。起初 Rhino 將 JavaScript 編譯成 Java 的二進位代碼執行,這樣它會有最好的效能。後來由於編譯執行的方式存在垃圾收集的問題並且編譯和裝載過程的開銷過大,不能滿足一些項目的需求,Rhino 提供瞭解釋執行的方式。隨著 Rhino 開放原始碼,越來越多的使用者在自己的產品中使用了
Rhino,同時也有越來越多的開發人員參與了 Rhino 的開發並做出了很大的貢獻。如今 Rhino1.6R2 版本將被包含在 Java SE6 中發行,更多的 Java 開發人員將從中獲益。
Rhino 提供了如下功能
對 JavaScript 1.5 的完全支援
直接在 Java 中使用 JavaScript 的功能
一個 JavaScript shell 用於運行 JavaScript 指令碼
一個 JavaScript 的編譯器,用於將 JavaScript 編譯成 Java 二進位檔案
支援的指令碼語言
在dev.java.net可以找到官方的指令碼引擎的實現項目。這一項目基於BSD License ,表示這些指令碼引擎的使用將十分自由。目前該項目已對包括 Groovy, JavaScript, Python, Ruby, PHP 在內的二十多種指令碼語言提供了支援。這一支援列表還將不斷擴大。
在 Mustang 中對指令碼引擎的檢索使用了原廠模式。首先需要執行個體化一個工廠 —— ScriptEngineManager。
// create a script engine manager
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngineManager 將在 Thread Context ClassLoader 的 Classpath 中根據 jar 檔案的 META-INF 來尋找可用的指令碼引擎。它提供了 3 種方法來檢索指令碼引擎:
// create engine by name
ScriptEngine engine = factory.getEngineByName ("JavaScript");
// create engine by name
ScriptEngine engine = factory.getEngineByExtension ("js");
// create engine by name
ScriptEngine engine = factory.getEngineByMimeType ("application/javascript");
下面的代碼將會列印出當前的 JDK 所支援的所有指令碼引擎
ScriptEngineManager factory = new ScriptEngineManager();
for (ScriptEngineFactory available : factory.getEngineFactories()) {
System.out.println(available.getEngineName());
}
以下各章節代碼將以 JavaScript 為例。
在 Java 中解釋指令碼
有了指令碼引擎執行個體就可以很方便的執行指令碼語言,按照慣例,我們還是從一個簡單的 Hello World 開始:
public class RunJavaScript {
public static void main(String[] args){
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName ("JavaScript");
engine.eval("print('Hello World')");
}
}
這段 Java 代碼將會執行 JavaScript 並列印出 Hello World。如果 JavaScript 有語法錯誤將會如何?
engine.eval("if(true){println ('hello')");
故意沒有加上”}”,執行這段代碼 Java 將會拋出一個 javax.script.ScriptException 並準確的列印出錯資訊: Exception in thread "main" javax.script.ScriptException:
sun.org.mozilla.javascript.internal.EvaluatorException:
missing } in compound statement (<Unknown source>#1) in <Unknown source>
at line number 1
at ...
如果我們要解釋一些更複雜的指令碼語言,或者想在運行時改變該指令碼該如何做呢?指令碼引擎支援一個重載的 eval 方法,它可以從一個 Reader 讀入所需的指令碼:
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName ("JavaScript");
engine.eval(new Reader("HelloWorld.js"));
如此這段 Java 代碼將在運行時動態尋找 HelloWorld.js 並執行,使用者可以隨時通過改變這一指令檔來改變 Java 代碼的行為。做一個簡單的實驗,Java 代碼如下: public class RunJavaScript {
public static void main(String[] args) throws FileNotFoundException,
ScriptException, InterruptedException {
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName ("JavaScript");
while (true) {
engine.eval(new FileReader("HelloWorld.js"));
Thread.sleep(1000);
}
}
}
HelloWorld.js 內容為簡單的列印一個 Hello World: print('Hello World');
運行 RunJavaScript 將會每一秒鐘列印一個 Hello World。這時候修改 HelloWorld.js 內容為 print('Hello Tony');
列印的內容將變為 Hello Tony,由此可見 Java 程式將動態去讀取指令檔並解釋執行。對於這一簡單的 Hello World 指令碼來說,IO 操作將比直接執行指令碼損失 20% 左右的效能(在我的 Think Pad 上),但他帶來的靈活性——在運行時動態改變代碼的能力,在某些場合是十分激動人心的。
指令碼語言與 Java 的通訊
ScriptEngine 的 put 方法用於將一個 Java 對象映射成一個指令碼語言的變數。現在有一個 Java Class,它只有一個方法,功能就是列印一個字串 Hello World:
package tony;
public class HelloWorld {
String s = "Hello World";
public void sayHello(){
System.out.println(s);
}
}
那麼如何在指令碼語言中使用這個類呢?put 方法可以做到:
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class TestPut {
public static void main(String[] args) throws ScriptException {
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("JavaScript");
HelloWorld hello = new HelloWorld();
engine.put("script_hello", hello);
engine.eval("script_hello.sayHello()");
}
}
首先我們執行個體化一個 HelloWorld,然後用 put 方法將這個執行個體映射為指令碼語言的變數 script_hello。那麼我們就可以在 eval() 函數中像 Java 程式中同樣的方式來調用這個執行個體的方法。同樣的,假設我們有一個指令碼函數,它進行一定的計算並傳回值,我們在 Java 代碼中也可以方便的調用這一指令碼:
package tony;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class TestInv {
public static void main(String[] args) throws ScriptException,
NoSuchMethodException {
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("JavaScript");
String script = "function say(first,second) { print(first +' '+ second); }";
engine.eval(script);
Invocable inv = (Invocable) engine;
inv.invokeFunction("say", "Hello", "Tony");
}
}
在這個例子中我們首先定義了一個指令碼函數 say,它的作用是接受兩個字串參數將他們拼接並返回。這裡我們第一次遇到了 ScriptEngine 的兩個可選介面之一 —— Invocable,Invocable 表示當前的 engine 可以作為函數被調用。這裡我們將 engine 強制轉換為 Invocable 類型,使用 invokeFunction 方法將參數傳遞給指令碼引擎。invokeFunction這個方法使用了可變參數的定義方式,可以一次傳遞多個參數,並且將指令碼語言的傳回值作為它的傳回值。下面這個例子用JavaScript實現了一個簡單的max函數,接受兩個參數,返回較大的那個。為了便於斷言結果正確性,這裡繼承了JUnit
Testcase,關於JUnit請參考www.junit.org。
package tony;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import junit.framework.TestCase;
public class TestScripting extends TestCase {
public void testInv() throws ScriptException, NoSuchMethodException {
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("JavaScript");
String script = "function max(first,second) "
+ "{ return (first > second) ?first:second;}";
engine.eval(script);
Invocable inv = (Invocable) engine;
Object obj = inv.invokeFunction("max", "1", "0");
assertEquals("1", obj.toString());
}
}
Invocable 介面還有一個方法用於從一個 engine 中得到一個 Java Interface 的執行個體,它的定義如下:
<T> T getInterface(Class<T> clasz)
它接受一個 Java 的 Interface 類型作為參數,返回這個 Interface 的一個執行個體。也就是說你可以完全用指令碼語言來寫一個 Java Interface 的所有實現。以下是一個例子。首先定一了個 Java Interface,它有兩個簡單的函數,分別為求最大值和最小值:
package tony;
public interface MaxMin {
public int max(int a, int b);
public int min(int a, int b);
}
這個 Testcase 用 JavaScript 實現了 MaxMin 介面,然後用 getInterface 方法返回了一個執行個體並驗證了結果。
public void testInvInterface() throws ScriptException,
NoSuchMethodException {
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("JavaScript");
String script = "function max(first,second) "
+ "{ return (first > second) ?first:second;}";
script += "function min(first,second) { return (first < second) ?first:second;}";
engine.eval(script);
Invocable inv = (Invocable) engine;
MaxMin maxMin = inv.getInterface(MaxMin.class);
assertEquals(1, maxMin.max(1, 0));
assertEquals(0, maxMin.min(1, 0));
}
指令碼的編譯執行
到目前為止,我們的指令碼全部都是解釋執行的,相比較之下編譯執行將會獲得更好的效能。這裡將介紹 ScriptEngine 的另外一個可選介面 —— Compilable,實現了這一介面的指令碼引擎支援指令碼的編譯執行。下面這個例子實現了一個判斷給定字串是否是 email 地址或者 ip 地址的指令碼:
public void testComplie() throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
String script = "var email=/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]"
+ "+(\\.[a-zA-Z0-9_-]+)+$/;";
script += "var ip = /^(\\d{1,2}|1\\d\\d|2[0-4]\\d|25[0-5])"
+"(\\.(\\d{1,2}|1\\d\\d|2[0-4]\\d|25[0-5])){3}$/;";
script += "if(email.test(str)){println('it is an email')}"
+ "else if(ip.test(str)){println('it is an ip address')}"
+ "else{println('I don\\'t know')}";
engine.put("str", "email@address.tony");
Compilable compilable = (Compilable) engine;
CompiledScript compiled = compilable.compile(script);
compiled.eval();
}
指令碼編譯的過程如下:首先將 engine 轉換為 Compilable 介面,然後調用 Compilable 介面的 compile 方法得到一個 CompiledScript 的執行個體,這個執行個體就代表一個編譯過的指令碼,如此用 CompiledScript 的 eval 方法即為調用編譯好的指令碼了。在我的 Think Pad 上,這段代碼編譯後的調用大約比直接調用 engine.eval 要快 3-4 倍。隨著指令碼複雜性的提升,效能的提升會更加明顯。
指令碼上下文與綁定
真正將指令碼語言與 Java 聯絡起來的不是 ScriptEngine,而是 ScriptContext,它作為 Java 與 ScriptEngine 之間的橋樑而存在。
一個 ScriptEngine 會有一個相應的 ScriptContext,它維護了一個 Map,這個 Map 中的每個元素都是指令碼語言對象與 Java 對象之間的映射。同時這個 Map 在我們的 API 中又被稱為 Bindings。一個 Bindings 就是一個限定了 key 必須為 String 類型的 Map —— Map<String, Object>。所以一個 ScriptContext 也會有對應的一個 Bindings,它可以通過 getBindings 和 setBindings 方法來擷取和更改。
一個 Bindings 包括了它的 ScriptContext 中的所有指令碼變數,那麼如何擷取指令碼變數的值呢?當然,從 Bindings 中 get 是一個辦法,同時 ScriptContext 也提供了 getAttribute 方法,在只希望獲得某一特定指令碼變數值的時候它顯然是十分有效。相應地 setAttribute 和 removeAttribute 可以增加、修改或者刪除一個特定變數。
在 ScriptContext 中儲存的所有變數也有自己的範圍,它們可以是 ENGINE_SCOPE 或者是 GLOBAL_SCOPE,前者表示這個 ScriptEngine 專屬的變數,後者則是所有 ScriptEngine 共有的變數。例如我們執行 engine.put(key, value) 方法之後,這時便會增加一個 ENGINE_SCOPE 的變數,如果要定義一個 GLOBAL_SCOPE 變數,可以通過 setAttribute(key, value, ScriptContext.GLOBAL_SCOPE)
來完成。
此外 ScriptContext 還提供了標準輸入和輸出的重新導向功能,它可以用於指定指令碼語言的輸入和輸出。
回頁首
在 JavaScript 中使用 Java 進階特性
這一部分不同於前述內容,將介紹 JavaScript引擎 —— Rhino 專屬的特性。
使用 Java 對象
前面的部分已經介紹過如何在 JavaScript 中使用一個已經執行個體化的 Java 對象,那麼如何在 JavaScript 中去執行個體化一個 Java 對象呢?在 Java 中所有 Class 是按照包名分層次存放的,而在 JavaScript 沒有這一結構,Rhino 使用了一個巧妙的方法實現了對所有 Java 對象的引用。Rhino 中定義了一個全域變數—— Packages,並且它的所有元素也是全域變數,這個全域變數維護了 Java 類的階層。例如 Packages.java.io.File 引用了
Java 的 io 包中 File 對象。如此一來我們便可以在 JavaScript 中方便的使用 Java 對象了,new 和 Packages 都是可以被省略的:
//The same as: var frame = new Packages.java.io.File("filename");
var frame = java.io.File("filename");
我們也可以像 Java 代碼中一樣把這個對象引用進來:
importClass (java.io.File);
var file = File("filename");
如果要將整個包下的所有類都引用進來可以用 importPackage:
importPackage(java.io);
如果只需要在特定程式碼片段中引用某些包,可以使用 JavaImporter 搭配 JavaScript 的 with 關鍵字,如:
var MyImport = JavaImporter(java.io.File);
with (MyImport) {
var myFile = File("filename");
}
使用者自訂的包也可以被引用進來,不過這時候 Packages 引用不能被省略:
importPackage(Packages.tony);
var hello = HelloWorld();
hello.sayHello();
注意這裡只有 public 的成員和方法才會在 JavaScript 中可見,例如對 hello.s 的引用將得到 undefined。下面簡單介紹一些常用的特性:
使用 Java 數組
需要用反射的方式構造:
var a = java.lang.reflect.Array.newInstance(java.lang.String, 5);
對於大部分情況,可以使用 JavaScript 的數組。將一個 JavaScript 的數組作為參數傳遞給一個 Java 方法時 Rhino 會做自動轉換,將其轉換為 Java 數組。
實現一個 Java 介面
除了上面提到的 Invocable 介面的 getInterface 方法外,我們也可以在指令碼中用如下方式:
//Define a JavaScript Object which has corresponding method
obj={max:function(a,b){return (a > b) ?a:b;}};
//Pass this object to an Interface
maxImpl=com.tony.MaxMin(obj);
//Invocation
print (maxImpl.max(1,2));
如果介面只有一個方法需要實現,那麼在 JavaScript 中你可以傳遞一個函數作為參數:
function func(){
println("Hello World");
}
t=java.lang.Thread(func);
t.start();
對於 JavaBean 的支援
Rhino 對於 JavaBean 的 get 和 is 方法將會自動匹配,例如調用 hello.string,如果不存在 string 這個變數,Rhino 將會自動匹配這個執行個體的 isString 方法然後再去匹配 getString 方法,若這兩個方法均不存在才會返回 undefined。
回頁首
命令列工具 jrunscript
在 Mustang 的發行版本中還將包含一個指令碼語言的的命令列工具,它能夠解釋所有當前 JDK 支援的指令碼語言。同時它也是一個用來學習指令碼語言很好的工具。你可以在http://java.sun.com/javase/6/docs/technotes/tools/share/jrunscript.html找到這一工具的詳細介紹。
文章出處:DIY部落(http://www.diybl.com/course/1_web/javascript/Javascriptxl/2008921/144278.html)