給 Java SE 注入腳本語言的活力(轉)
在即將發布的 Java SE6(Mustang)中,增加了對腳本語言的支持。通過對腳本語言的調用,使得一些通常用 Java 比較難于實現的功能變得簡單和輕便。腳本語言與 Java 之間的互操作將變得優雅而直接。
假設我們有一個簡單的需求,察看一份文檔中 5 個字母組成的單詞的個數。用 Java 一般實現如下:
import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; public class Find5Words { public static void main(String[] args) throws IOException { String result = ""; String line = null; int num = 0; FileReader fr = new FileReader("filename"); BufferedReader br = new BufferedReader(fr); while ((line = br.readLine()) != null) { result += line; } br.close(); fr.close(); String[] s = result.split(" "); for (int i = 0; i < s.length; i++) { if (s[i].matches("^\\w{5}$")) { num++; } } System.out.println(num); } } |
再看看 Perl 語言實現同樣功能的代碼:
open FILE, "<filename "; while (<FILE>) { for (split) { $num++ if /^\w{5}$/ } } print $num; |
那么有沒有一種優雅的方式將 Java 與腳本語言結合呢,在今年秋季即將發布的 Java SE6(代號 Mustang)中,這將成為現實。
JSR 233 為 Java 設計了一套腳本語言 API。這一套 API 提供了在 Java 程序中調用各種腳本語言引擎的接口。任何實現了這一接口的腳本語言引擎都可以在 Java 程序中被調用。在 Mustang 的發行版本中包括了一個基于 Mozilla Rhino 的 JavaScript 腳本引擎。
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 為例。
有了腳本引擎實例就可以很方便的執行腳本語言,按照慣例,我們還是從一個簡單的 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 上),但他帶來的靈活性——在運行時動態改變代碼的能力,在某些場合是十分激動人心的。
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引擎 —— Rhino 獨有的特性。
前面的部分已經介紹過如何在 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。下面簡單介紹一些常用的特性:
需要用反射的方式構造:
var a = java.lang.reflect.Array.newInstance(java.lang.String, 5); |
對于大部分情況,可以使用 JavaScript 的數組。將一個 JavaScript 的數組作為參數傳遞給一個 Java 方法時 Rhino 會做自動轉換,將其轉換為 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(); |
Rhino 對于 JavaBean 的 get 和 is 方法將會自動匹配,例如調用 hello.string,如果不存在 string 這個變量,Rhino 將會自動匹配這個實例的 isString 方法然后再去匹配 getString 方法,若這兩個方法均不存在才會返回 undefined。
在 Mustang 的發行版本中還將包含一個腳本語言的的命令行工具,它能夠解釋所有當前 JDK 支持的腳本語言。同時它也是一個用來學習腳本語言很好的工具。你可以在http://java.sun.com/javase/6/docs/technotes/tools/share/jrunscript.html找到這一工具的詳細介紹。
![]() |
腳本語言犧牲執行速度換來更高的生產率和靈活性。隨著計算機性能的不斷提高,硬件價格不斷下降,可以預見的,腳本語言將獲得更廣泛的應用。在 JavaSE 的下一個版本中加入了對腳本語言的支持,無疑將使 Java 程序變得更加靈活,也會使 Java 程序員的工作更加有效率。
- JSR 233 主頁http://www.jcp.org/en/jsr/detail?id=223 你可以在這里找到關于這個 Request 的詳細信息。
- Mozilla Rhino 項目主頁http://www.mozilla.org/rhino/ 這里有完善的項目文檔,并且可以下載到該項目的最新發行版本以及源代碼。
- https://scripting.dev.java.net/是官方腳本引擎項目的主頁,在這里可以查看到已經對哪些腳本引擎提供了支持。并且可以找到相應腳本引擎的網頁鏈接。
- http://www.opensource.org/licenses/ 在這里可以找到關于 Open source license 的詳細信息。
- http://www.junit.org/ 是 Junit 的官方站點
- http://java.sun.com/javase/6/docs/是 Java SE6 的在線文檔
![]() | ||
![]() | 吳玥顥,目前就職于 IBM 中國開發中心 Harmony 開發團隊。 除了對 Java 和腳本語言的熱愛之外,他的興趣還包括哲學、神話、歷史與籃球。此外他還是個電腦游戲高手。您可以 |
posted on 2007-03-21 16:21 liaojiyong 閱讀(429) 評論(0) 編輯 收藏 所屬分類: Java