級別: 中級 吳 玥顥 (wuyuehao@cn.ibm.com), 軟件工程師, IBM
2006 年 10 月 30 日 在即將發(fā)布的 Java SE6(Mustang)中,增加了對腳本語言的支持。通過對腳本語言的調(diào)用,使得一些通常用 Java 比較難于實(shí)現(xiàn)的功能變得簡單和輕便。腳本語言與 Java 之間的互操作將變得優(yōu)雅而直接。 腳本語言與 Java 假設(shè)我們有一個簡單的需求,察看一份文檔中 5 個字母組成的單詞的個數(shù)。用 Java 一般實(shí)現(xiàn)如下: 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 語言實(shí)現(xiàn)同樣功能的代碼: open FILE, "<filename "; while (<FILE>) { for (split) { $num++ if /^\w{5}$/ } } print $num; | 那么有沒有一種優(yōu)雅的方式將 Java 與腳本語言結(jié)合呢,在今年秋季即將發(fā)布的 Java SE6(代號 Mustang)中,這將成為現(xiàn)實(shí)。
Mustang 的腳本引擎 JSR 233 為 Java 設(shè)計了一套腳本語言 API。這一套 API 提供了在 Java 程序中調(diào)用各種腳本語言引擎的接口。任何實(shí)現(xiàn)了這一接口的腳本語言引擎都可以在 Java 程序中被調(diào)用。在 Mustang 的發(fā)行版本中包括了一個基于 Mozilla Rhino 的 JavaScript 腳本引擎。 Mozilla Rhino Rhino 是一個純 Java 的開源的 JavaScript 實(shí)現(xiàn)。他的名字來源于 O‘Reilly 關(guān)于 JavaScript 的書的封面: Rhino 項(xiàng)目可以追朔到 1997 年,當(dāng)時 Netscape 計劃開發(fā)一個純 Java 實(shí)現(xiàn)的 Navigator,為此需要一個 Java 實(shí)現(xiàn)的 JavaScript —— Javagator。它也就是 Rhino 的前身。起初 Rhino 將 JavaScript 編譯成 Java 的二進(jìn)制代碼執(zhí)行,這樣它會有最好的性能。后來由于編譯執(zhí)行的方式存在垃圾收集的問題并且編譯和裝載過程的開銷過大,不能滿足一些項(xiàng)目的需求,Rhino 提供了解釋執(zhí)行的方式。隨著 Rhino 開放源代碼,越來越多的用戶在自己的產(chǎn)品中使用了 Rhino,同時也有越來越多的開發(fā)者參與了 Rhino 的開發(fā)并做出了很大的貢獻(xiàn)。如今 Rhino1.6R2 版本將被包含在 Java SE6 中發(fā)行,更多的 Java 開發(fā)者將從中獲益。 Rhino 提供了如下功能 - 對 JavaScript 1.5 的完全支持
- 直接在 Java 中使用 JavaScript 的功能
- 一個 JavaScript shell 用于運(yùn)行 JavaScript 腳本
- 一個 JavaScript 的編譯器,用于將 JavaScript 編譯成 Java 二進(jìn)制文件
支持的腳本語言 在dev.java.net可以找到官方的腳本引擎的實(shí)現(xiàn)項(xiàng)目。這一項(xiàng)目基于BSD License ,表示這些腳本引擎的使用將十分自由。目前該項(xiàng)目已對包括 Groovy, JavaScript, Python, Ruby, PHP 在內(nèi)的二十多種腳本語言提供了支持。這一支持列表還將不斷擴(kuò)大。 在 Mustang 中對腳本引擎的檢索使用了工廠模式。首先需要實(shí)例化一個工廠 —— ScriptEngineManager。 // create a script engine manager ScriptEngineManager factory = new ScriptEngineManager(); | ScriptEngineManager 將在 Thread Context ClassLoader 的 Classpath 中根據(jù) 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"); | 下面的代碼將會打印出當(dāng)前的 JDK 所支持的所有腳本引擎 ScriptEngineManager factory = new ScriptEngineManager(); for (ScriptEngineFactory available : factory.getEngineFactories()) { System.out.println(available.getEngineName()); } | 以下各章節(jié)代碼將以 JavaScript 為例。 在 Java 中解釋腳本 有了腳本引擎實(shí)例就可以很方便的執(zhí)行腳本語言,按照慣例,我們還是從一個簡單的 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 代碼將會執(zhí)行 JavaScript 并打印出 Hello World。如果 JavaScript 有語法錯誤將會如何? engine.eval("if(true){println (‘hello‘)"); | 故意沒有加上”}”,執(zhí)行這段代碼 Java 將會拋出一個 javax.script.ScriptException 并準(zhǔn)確的打印出錯信息: 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 ... | 如果我們要解釋一些更復(fù)雜的腳本語言,或者想在運(yùn)行時改變該腳本該如何做呢?腳本引擎支持一個重載的 eval 方法,它可以從一個 Reader 讀入所需的腳本: ScriptEngineManager factory = new ScriptEngineManager(); ScriptEngine engine = factory.getEngineByName ("JavaScript"); engine.eval(new Reader("HelloWorld.js")); | 如此這段 Java 代碼將在運(yùn)行時動態(tài)的尋找 HelloWorld.js 并執(zhí)行,用戶可以隨時通過改變這一腳本文件來改變 Java 代碼的行為。做一個簡單的實(shí)驗(yàn),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 內(nèi)容為簡單的打印一個 Hello World: print(‘Hello World‘); 運(yùn)行 RunJavaScript 將會每一秒鐘打印一個 Hello World。這時候修改 HelloWorld.js 內(nèi)容為 print(‘Hello Tony‘); 打印的內(nèi)容將變?yōu)?Hello Tony,由此可見 Java 程序?qū)討B(tài)的去讀取腳本文件并解釋執(zhí)行。對于這一簡單的 Hello World 腳本來說,IO 操作將比直接執(zhí)行腳本損失 20% 左右的性能(在我的 Think Pad 上),但他帶來的靈活性——在運(yùn)行時動態(tài)改變代碼的能力,在某些場合是十分激動人心的。 腳本語言與 Java 的通信 ScriptEngine 的 put 方法用于將一個 Java 對象映射成一個腳本語言的變量?,F(xiàn)在有一個 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()"); } } | 首先我們實(shí)例化一個 HelloWorld,然后用 put 方法將這個實(shí)例映射為腳本語言的變量 script_hello。那么我們就可以在 eval() 函數(shù)中像 Java 程序中同樣的方式來調(diào)用這個實(shí)例的方法。同樣的,假設(shè)我們有一個腳本函數(shù),它進(jìn)行一定的計算并返回值,我們在 Java 代碼中也可以方便的調(diào)用這一腳本: 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"); } } | 在這個例子中我們首先定義了一個腳本函數(shù) say,它的作用是接受兩個字符串參數(shù)將他們拼接并返回。這里我們第一次遇到了 ScriptEngine 的兩個可選接口之一 —— Invocable,Invocable 表示當(dāng)前的 engine 可以作為函數(shù)被調(diào)用。這里我們將 engine 強(qiáng)制轉(zhuǎn)換為 Invocable 類型,使用 invokeFunction 方法將參數(shù)傳遞給腳本引擎。invokeFunction這個方法使用了可變參數(shù)的定義方式,可以一次傳遞多個參數(shù),并且將腳本語言的返回值作為它的返回值。下面這個例子用JavaScript實(shí)現(xiàn)了一個簡單的max函數(shù),接受兩個參數(shù),返回較大的那個。為了便于斷言結(jié)果正確性,這里繼承了JUnit Testcase,關(guān)于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 的實(shí)例,它的定義如下: <T> T getInterface(Class<T> clasz) | 它接受一個 Java 的 Interface 類型作為參數(shù),返回這個 Interface 的一個實(shí)例。也就是說你可以完全用腳本語言來寫一個 Java Interface 的所有實(shí)現(xiàn)。以下是一個例子。首先定一了個 Java Interface,它有兩個簡單的函數(shù),分別為求最大值和最小值: package tony; public interface MaxMin { public int max(int a, int b); public int min(int a, int b); } | 這個 Testcase 用 JavaScript 實(shí)現(xiàn)了 MaxMin 接口,然后用 getInterface 方法返回了一個實(shí)例并驗(yàn)證了結(jié)果。 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)); } | 腳本的編譯執(zhí)行 到目前為止,我們的腳本全部都是解釋執(zhí)行的,相比較之下編譯執(zhí)行將會獲得更好的性能。這里將介紹 ScriptEngine 的另外一個可選接口 —— Compilable,實(shí)現(xiàn)了這一接口的腳本引擎支持腳本的編譯執(zhí)行。下面這個例子實(shí)現(xiàn)了一個判斷給定字符串是否是 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 轉(zhuǎn)換為 Compilable 接口,然后調(diào)用 Compilable 接口的 compile 方法得到一個 CompiledScript 的實(shí)例,這個實(shí)例就代表一個編譯過的腳本,如此用 CompiledScript 的 eval 方法即為調(diào)用編譯好的腳本了。在我的 Think Pad 上,這段代碼編譯后的調(diào)用大約比直接調(diào)用 engine.eval 要快 3-4 倍。隨著腳本復(fù)雜性的提升,性能的提升會更加明顯。 腳本上下文與綁定 真正將腳本語言與 Java 聯(lián)系起來的不是 ScriptEngine,而是 ScriptContext,它作為 Java 與 ScriptEngine 之間的橋梁而存在。 一個 ScriptEngine 會有一個相應(yīng)的 ScriptContext,它維護(hù)了一個 Map,這個 Map 中的每個元素都是腳本語言對象與 Java 對象之間的映射。同時這個 Map 在我們的 API 中又被稱為 Bindings。一個 Bindings 就是一個限定了 key 必須為 String 類型的 Map —— Map<String, Object>。所以一個 ScriptContext 也會有對應(yīng)的一個 Bindings,它可以通過 getBindings 和 setBindings 方法來獲取和更改。 一個 Bindings 包括了它的 ScriptContext 中的所有腳本變量,那么如何獲取腳本變量的值呢?當(dāng)然,從 Bindings 中 get 是一個辦法,同時 ScriptContext 也提供了 getAttribute 方法,在只希望獲得某一特定腳本變量值的時候它顯然是十分有效的。相應(yīng)地 setAttribute 和 removeAttribute 可以增加、修改或者刪除一個特定變量。 在 ScriptContext 中存儲的所有變量也有自己的作用域,它們可以是 ENGINE_SCOPE 或者是 GLOBAL_SCOPE,前者表示這個 ScriptEngine 獨(dú)有的變量,后者則是所有 ScriptEngine 共有的變量。例如我們執(zhí)行 engine.put(key, value) 方法之后,這時便會增加一個 ENGINE_SCOPE 的變量,如果要定義一個 GLOBAL_SCOPE 變量,可以通過 setAttribute(key, value, ScriptContext.GLOBAL_SCOPE) 來完成。 此外 ScriptContext 還提供了標(biāo)準(zhǔn)輸入和輸出的重定向功能,它可以用于指定腳本語言的輸入和輸出。
在 JavaScript 中使用 Java 高級特性 這一部分不同于前述內(nèi)容,將介紹 JavaScript引擎 —— Rhino 獨(dú)有的特性。 使用 Java 對象 前面的部分已經(jīng)介紹過如何在 JavaScript 中使用一個已經(jīng)實(shí)例化的 Java 對象,那么如何在 JavaScript 中去實(shí)例化一個 Java 對象呢?在 Java 中所有 Class 是按照包名分層次存放的,而在 JavaScript 沒有這一結(jié)構(gòu),Rhino 使用了一個巧妙的方法實(shí)現(xiàn)了對所有 Java 對象的引用。Rhino 中定義了一個全局變量—— Packages,并且它的所有元素也是全局變量,這個全局變量維護(hù)了 Java 類的層次結(jié)構(gòu)。例如 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 代碼中一樣把這個對象引用進(jìn)來: importClass (java.io.File); var file = File("filename"); | 如果要將整個包下的所有類都引用進(jìn)來可以用 importPackage: 如果只需要在特定代碼段中引用某些包,可以使用 JavaImporter 搭配 JavaScript 的 with 關(guān)鍵字,如: var MyImport = JavaImporter(java.io.File); with (MyImport) { var myFile = File("filename"); } | 用戶自定義的包也可以被引用進(jìn)來,不過這時候 Packages 引用不能被省略: importPackage(Packages.tony); var hello = HelloWorld(); hello.sayHello(); | 注意這里只有 public 的成員和方法才會在 JavaScript 中可見,例如對 hello.s 的引用將得到 undefined。下面簡單介紹一些常用的特性: 使用 Java 數(shù)組 需要用反射的方式構(gòu)造: var a = java.lang.reflect.Array.newInstance(java.lang.String, 5); | 對于大部分情況,可以使用 JavaScript 的數(shù)組。將一個 JavaScript 的數(shù)組作為參數(shù)傳遞給一個 Java 方法時 Rhino 會做自動轉(zhuǎn)換,將其轉(zhuǎn)換為 Java 數(shù)組。 實(shí)現(xiàn)一個 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)); | 如果接口只有一個方法需要實(shí)現(xiàn),那么在 JavaScript 中你可以傳遞一個函數(shù)作為參數(shù): function func(){ println("Hello World"); } t=java.lang.Thread(func); t.start(); | 對于 JavaBean 的支持 Rhino 對于 JavaBean 的 get 和 is 方法將會自動匹配,例如調(diào)用 hello.string,如果不存在 string 這個變量,Rhino 將會自動匹配這個實(shí)例的 isString 方法然后再去匹配 getString 方法,若這兩個方法均不存在才會返回 undefined。
命令行工具 jrunscript 在 Mustang 的發(fā)行版本中還將包含一個腳本語言的的命令行工具,它能夠解釋所有當(dāng)前 JDK 支持的腳本語言。同時它也是一個用來學(xué)習(xí)腳本語言很好的工具。你可以在http://java.sun.com/javase/6/docs/technotes/tools/share/jrunscript.html找到這一工具的詳細(xì)介紹。
結(jié)束語 腳本語言犧牲執(zhí)行速度換來更高的生產(chǎn)率和靈活性。隨著計算機(jī)性能的不斷提高,硬件價格不斷下降,可以預(yù)見的,腳本語言將獲得更廣泛的應(yīng)用。在 JavaSE 的下一個版本中加入了對腳本語言的支持,無疑將使 Java 程序變得更加靈活,也會使 Java 程序員的工作更加有效率。
參考資料
關(guān)于作者 | | | 吳玥顥,目前就職于 IBM 中國開發(fā)中心 Harmony 開發(fā)團(tuán)隊(duì)。 除了對 Java 和腳本語言的熱愛之外,他的興趣還包括哲學(xué)、神話、歷史與籃球。此外他還是個電腦游戲高手。您可以通過wuyuehao@cn.ibm.com聯(lián)系到他。 | %2 |