Mustang與Rhino:Java6中的腳本編寫
最新的Java主版本(Java SE 6,又稱Mustang)現在正處于beta版本階段。雖然該版本并不像Java 5的更新那么多,但是它確實有一些有趣的新特性。毋庸置疑,其中的一個就是對腳本編寫語言的支持。
諸如PHP、Ruby、JavaScript、Python(或Jython)之類的腳本編寫語言被廣泛應用于許多領域,并由于其靈活性和簡單性而大受歡迎。由于腳本是被解釋而不是被編譯的,所以可以輕松地從命令行運行和測試它們。這就壓縮了編碼/測試周期,并提高了開發人員的生產率。腳本通常是動態鍵入的,其語法極富表現力,所編寫出的算法要比Java中的等效算法簡明得多。使用起來通常也很有趣。
在很多情況下,從Java使用腳本編寫語言會很有用,比如為Java應用程序提供擴展,以便用戶可以編寫自己的腳本進行擴展或定制化核心功能。腳本編寫語言可讀性更強,也更容易編寫,所以(從技術上來說)它們是用于為終端用戶提供根據需求定制化產品的可能性的理想語言。
早已經有許多Java可用的獨立腳本編寫包了,包括Rhino、Jacl、Jython、BeanShell、JRuby等。新消息是Java 6通過一個標準接口為腳本編寫語言提供了內置支持。
Java 6提供對JSR-223規范的全面支持。該規范提供了一種從Java內部執行腳本編寫語言的方便、標準的方式,并提供從腳本內部訪問Java資源和類的功能。Java 6附帶了與Mozilla Rhino的JavaScript 實現的內置集成。基于該規范,對諸如PHP、Groovy和BeanShell之類的其它腳本編寫語言的支持也正在進行中。本文關注的是Rhino實現,但是其它語言應該是基本相同的。
腳本編寫語言的名稱都從何而來?由于大多數腳本編寫語言都來自于開源項目,所以其名稱通常都是由其各自的編寫者想出來的。Rhino(犀牛)的名稱來自于O'Reilly關于JavaScript的書封面上的動物。PHP則遵從Unix自解釋的慣例,是PHP: Hypertext Preprocessor的簡寫。Jython是Python腳本編寫語言的Java實現。而Groovy只是為了顯酷。
使用腳本引擎
JSR 223規范方便易用。要使用腳本,您只需了解一些關鍵類。主要是ScriptEngine類,它處理腳本解釋和求值。要實例化一個腳本引擎,應該使用ScriptEngineManager類來檢索感興趣的腳本編寫語言的ScriptEngine對象。每種腳本編寫語言都有一個名稱。Mozilla Rhino ECMAScript腳本編寫語言(通常稱為JavaScript)使用“js”進行標識。
嵌入式的JavaScript可用于各種用途。因為它要比硬編碼的Java靈活且更容易配置,所以通常還可以用于編寫頻繁更改的業務規則。使用eval()方法對腳本表達式進行求值。腳本編寫環境中所使用的任何變量都可以使用put()方法從Java代碼內部賦值。
eval()方法還接受一個Reader對象,這使它容易在文件或其他外部源中保存腳本,如下例所示:
檢索結果
現在可以運行腳本了,那么接下來做什么呢?通常我們都希望從腳本編寫環境獲取求值后的值或表達式,以便用于Java代碼。這有兩種實現方法。第一種是使用eval()函數返回執行腳本后所返回的值。默認情況下,將返回上次執行的表達式的值。
下例演示了一個虛構的保險公司的保險費計算方法。對于年齡小于25歲的司機,將額外支付50%的保險費。而對于有非保險補助的大于25歲的司機,保險費將打一個25%的折扣。其它情況則應用標準的保險費。這個規則可以使用如下的JavaScript表達式來實現:
返回值是上次執行的指令的值,所以在本例中就是為riskFactor所賦的值。注意,包含結果(在本例中是riskFactor)的JavaScript變量的值是無關的:只返回值。
與腳本交互的第二種方式是使用Bindings對象。Bindings對象基本上是一個鍵/值對映射,可用于在Java應用程序和JavaScript腳本之間交換信息。
訪問Java資源
還可以從腳本內部訪問Java類和資源。Rhino JavaScript引擎支持importPackage()函數,該函數允許導入Java包。導入之后,就可以在腳本中實例化Java對象,就像在Java中所做的那樣:
調用Java類上的方法也很容易做到,不管是傳遞給腳本引擎的對象實例,還是靜態類成員。
可編譯且可調用的引擎
某些腳本引擎實現支持腳本編譯,這將帶來相當大的性能提升。腳本可以被編譯或重用,而不是在每次執行時被解釋。compile()方法返回一個CompiledScript實例,隨后該實例可用于通過eval()方法計算編譯后的表達式:
等效的Java代碼如下:
需要根據具體的條件計算并測試腳本編譯所帶來的性能提升。一些使用此處所示腳本的簡單基準測試顯示了大約60%的性能提升。通常,腳本越復雜,從編譯中所獲得的提升就應該越多。作為一個粗略的測試,我將上面的腳本以及等效的Java代碼運行了10000次,得到了以下的結果:
解釋后的JS: 1,550ms
編譯后的JS: 579ms
編譯后的Java: 0.0172ms
編譯后的JavaScript大約比解釋后的JavaScript運行快3倍。解釋后的代碼平均運行時間為15ms而編譯后的代碼平均運行時間為6ms。當然了,正如可以預料到的,真正編譯后的Java比解釋后的JavaScript大約快了10萬倍。然而,如前所述,腳本編寫語言的優點在于其他地方。
Invocable接口允許從Java代碼調用定義在腳本中的單個函數。invoke()方法所帶參數包括要調用的函數名稱以及一個參數數組,并返回調用結果:
這種方法允許在JavaScript(或其他腳本編寫語言)中編寫和維護庫,并從一個Java應用程序調用它。在交易中,重要的是要能夠根據市場形勢快速更新價格規則。例如,一個保險公司可能希望保險精算師能夠使用一種平易的腳本編寫語言直接設計和維護保險規則和保險費計算算法,隨后可以從一個大型J2EE企業架構中對其進行調用。這樣的架構可能包括一個在線報價系統、一個用于保險費代理程序的外部應用程序,以及后臺業務應用程序,它們全都調用同一個集中式腳本。
Web開發
JSR 223規范最偉大的目標之一是要在Java web應用程序中提供非Java腳本編寫頁面(如PHP)的集成性。這旨在允許將非Java腳本編寫頁面整合為Java web應用程序的一部分,同時允許從該腳本編寫頁面調用Java類。例如,下面的PHP代碼展示了如何從PHP頁面中使用Java對象:
更為重要的是,該規范為與Java web應用服務器的集成提供了一個標準的API,用于訪問和修改servlet容器會話數據:
這種集成意義深遠。現在在一個J2EE環境中,不僅可以使用Java,還可以使用其它腳本編寫語言來編寫web應用程序,將Java用作一個強大的跨平臺架構。而且使用其他腳本編寫語言所編寫的現有頁面或應用程序現在可以輕松地與J2EE應用程序進行集成。
結束語
一些人將腳本編寫語言視為解決所有現有編程難題的答案,而另一些人則譴責它鼓勵了無組織且不可維護代碼的產生。像其它任何工具一樣,腳本編寫可以被使用或濫用。腳本語言靈活、易學,且編寫起來很快。但是Java IDE只對其提供了有限的支持,而且難于使用諸如JUnit之類的傳統測試框架對其進行測試,錯誤可能直到運行時才會出現。不過,在很多情況下,正確和適當地使用腳本編寫無疑會使生活更為輕松。應該考慮以下面的方式使用腳本編寫:
作為一種擴展或定制應用程序的手段。
作為一種實現頻繁改變的靈活(有時還很復雜)的業務規則的方便方式。
總而言之,腳本編寫支持無疑為Java開發人員的工具箱中有添加了一個新的得力工具。
諸如PHP、Ruby、JavaScript、Python(或Jython)之類的腳本編寫語言被廣泛應用于許多領域,并由于其靈活性和簡單性而大受歡迎。由于腳本是被解釋而不是被編譯的,所以可以輕松地從命令行運行和測試它們。這就壓縮了編碼/測試周期,并提高了開發人員的生產率。腳本通常是動態鍵入的,其語法極富表現力,所編寫出的算法要比Java中的等效算法簡明得多。使用起來通常也很有趣。
在很多情況下,從Java使用腳本編寫語言會很有用,比如為Java應用程序提供擴展,以便用戶可以編寫自己的腳本進行擴展或定制化核心功能。腳本編寫語言可讀性更強,也更容易編寫,所以(從技術上來說)它們是用于為終端用戶提供根據需求定制化產品的可能性的理想語言。
早已經有許多Java可用的獨立腳本編寫包了,包括Rhino、Jacl、Jython、BeanShell、JRuby等。新消息是Java 6通過一個標準接口為腳本編寫語言提供了內置支持。
Java 6提供對JSR-223規范的全面支持。該規范提供了一種從Java內部執行腳本編寫語言的方便、標準的方式,并提供從腳本內部訪問Java資源和類的功能。Java 6附帶了與Mozilla Rhino的JavaScript 實現的內置集成。基于該規范,對諸如PHP、Groovy和BeanShell之類的其它腳本編寫語言的支持也正在進行中。本文關注的是Rhino實現,但是其它語言應該是基本相同的。
腳本編寫語言的名稱都從何而來?由于大多數腳本編寫語言都來自于開源項目,所以其名稱通常都是由其各自的編寫者想出來的。Rhino(犀牛)的名稱來自于O'Reilly關于JavaScript的書封面上的動物。PHP則遵從Unix自解釋的慣例,是PHP: Hypertext Preprocessor的簡寫。Jython是Python腳本編寫語言的Java實現。而Groovy只是為了顯酷。
使用腳本引擎
JSR 223規范方便易用。要使用腳本,您只需了解一些關鍵類。主要是ScriptEngine類,它處理腳本解釋和求值。要實例化一個腳本引擎,應該使用ScriptEngineManager類來檢索感興趣的腳本編寫語言的ScriptEngine對象。每種腳本編寫語言都有一個名稱。Mozilla Rhino ECMAScript腳本編寫語言(通常稱為JavaScript)使用“js”進行標識。
| ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("js"); |
嵌入式的JavaScript可用于各種用途。因為它要比硬編碼的Java靈活且更容易配置,所以通常還可以用于編寫頻繁更改的業務規則。使用eval()方法對腳本表達式進行求值。腳本編寫環境中所使用的任何變量都可以使用put()方法從Java代碼內部賦值。
| ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("js"); engine.put("age", 21); engine.eval( "if (age >= 18){ " + " print('Old enough to vote!'); " + "} else {" + " print ('Back to school!');" + "}"); > Old enough to vote! |
eval()方法還接受一個Reader對象,這使它容易在文件或其他外部源中保存腳本,如下例所示:
| ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("js"); engine.put("age", 21); engine.eval(new FileReader("c:/voting.js")); |
檢索結果
現在可以運行腳本了,那么接下來做什么呢?通常我們都希望從腳本編寫環境獲取求值后的值或表達式,以便用于Java代碼。這有兩種實現方法。第一種是使用eval()函數返回執行腳本后所返回的值。默認情況下,將返回上次執行的表達式的值。
下例演示了一個虛構的保險公司的保險費計算方法。對于年齡小于25歲的司機,將額外支付50%的保險費。而對于有非保險補助的大于25歲的司機,保險費將打一個25%的折扣。其它情況則應用標準的保險費。這個規則可以使用如下的JavaScript表達式來實現:
| ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("js"); engine.put("age", 26); engine.put("noClaims", Boolean.TRUE); Object result = engine.eval( "if (age < 25){ " + " riskFactor = 1.5;" + "} else if (noClaims) {" + " riskFactor = 0.75;" + "} else {" + " riskFactor = 1.0;" + "}"); assertEquals(result,0.75); } |
返回值是上次執行的指令的值,所以在本例中就是為riskFactor所賦的值。注意,包含結果(在本例中是riskFactor)的JavaScript變量的值是無關的:只返回值。
與腳本交互的第二種方式是使用Bindings對象。Bindings對象基本上是一個鍵/值對映射,可用于在Java應用程序和JavaScript腳本之間交換信息。
| public void testEvalWithBindings() throws ScriptException { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("js"); Bindings bindings = engine.createBindings(); bindings.put("age", 26); bindings.put("noClaims", Boolean.TRUE); bindings.put("riskFactor", 1); engine.eval( "if (age < 25){ " + " riskFactor = 1.5;" + "} else if (noClaims) {" + " riskFactor = 0.75;" + "} else {" + " riskFactor = 1.0;" + "}"); double risk = bindings.get("riskFactor"); assertEquals(risk,0.75); } |
訪問Java資源
還可以從腳本內部訪問Java類和資源。Rhino JavaScript引擎支持importPackage()函數,該函數允許導入Java包。導入之后,就可以在腳本中實例化Java對象,就像在Java中所做的那樣:
| engine.eval("importPackage(java.util); " + "today = new Date(); " + "print('Today is ' + today);"); |
調用Java類上的方法也很容易做到,不管是傳遞給腳本引擎的對象實例,還是靜態類成員。
| engine.put("name","John Doe"); engine.eval( "name2 = name.toUpperCase();" + "print('Converted name = ' + name2);"); > Converted name = JOHN DOE |
可編譯且可調用的引擎
某些腳本引擎實現支持腳本編譯,這將帶來相當大的性能提升。腳本可以被編譯或重用,而不是在每次執行時被解釋。compile()方法返回一個CompiledScript實例,隨后該實例可用于通過eval()方法計算編譯后的表達式:
| ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("js"); Compilable compilable = (Compilable) engine; CompiledScript script = compilable.compile( "if (age < 25){ " + " riskFactor = 1.5;" + "} else if (noClaims) {" + " riskFactor = 0.75;" + "} else {" + " riskFactor = 1.0;" + "}"); Bindings bindings = engine.createBindings(); bindings.put("age", 26); bindings.put("noClaims", Boolean.TRUE); bindings.put("riskFactor", 1); script.eval(); |
等效的Java代碼如下:
| public double calculateRiskFactor(int age, boolean noClaims) { double riskFactor; if (age < 25) { riskFactor = 1.5; } else if (noClaims) { riskFactor = 0.75; } else { riskFactor = 1.0; } return riskFactor; } |
需要根據具體的條件計算并測試腳本編譯所帶來的性能提升。一些使用此處所示腳本的簡單基準測試顯示了大約60%的性能提升。通常,腳本越復雜,從編譯中所獲得的提升就應該越多。作為一個粗略的測試,我將上面的腳本以及等效的Java代碼運行了10000次,得到了以下的結果:
解釋后的JS: 1,550ms
編譯后的JS: 579ms
編譯后的Java: 0.0172ms
編譯后的JavaScript大約比解釋后的JavaScript運行快3倍。解釋后的代碼平均運行時間為15ms而編譯后的代碼平均運行時間為6ms。當然了,正如可以預料到的,真正編譯后的Java比解釋后的JavaScript大約快了10萬倍。然而,如前所述,腳本編寫語言的優點在于其他地方。
Invocable接口允許從Java代碼調用定義在腳本中的單個函數。invoke()方法所帶參數包括要調用的函數名稱以及一個參數數組,并返回調用結果:
| ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("js"); engine.eval("function increment(i) {return i + 1;}"); Invocable invocable = (Invocable) engine; Object result = invocable.invoke("increment", new Object[] {10}); System.out.print("result = " + result); > result = 11 |
這種方法允許在JavaScript(或其他腳本編寫語言)中編寫和維護庫,并從一個Java應用程序調用它。在交易中,重要的是要能夠根據市場形勢快速更新價格規則。例如,一個保險公司可能希望保險精算師能夠使用一種平易的腳本編寫語言直接設計和維護保險規則和保險費計算算法,隨后可以從一個大型J2EE企業架構中對其進行調用。這樣的架構可能包括一個在線報價系統、一個用于保險費代理程序的外部應用程序,以及后臺業務應用程序,它們全都調用同一個集中式腳本。
Web開發
JSR 223規范最偉大的目標之一是要在Java web應用程序中提供非Java腳本編寫頁面(如PHP)的集成性。這旨在允許將非Java腳本編寫頁面整合為Java web應用程序的一部分,同時允許從該腳本編寫頁面調用Java類。例如,下面的PHP代碼展示了如何從PHP頁面中使用Java對象:
| //instantiate a java object = new Java( java.util.Date ); //call a method =->toString(); //display return value echo(); |
更為重要的是,該規范為與Java web應用服務器的集成提供了一個標準的API,用于訪問和修改servlet容器會話數據:
| <ul> <? //display session attributes in table =->getSession()->getAttributeNames(); foreach ( as ) { = ->getSession()->getAttribute(); print("<li> = <li>"); } ?> </ul> |
這種集成意義深遠。現在在一個J2EE環境中,不僅可以使用Java,還可以使用其它腳本編寫語言來編寫web應用程序,將Java用作一個強大的跨平臺架構。而且使用其他腳本編寫語言所編寫的現有頁面或應用程序現在可以輕松地與J2EE應用程序進行集成。
結束語
一些人將腳本編寫語言視為解決所有現有編程難題的答案,而另一些人則譴責它鼓勵了無組織且不可維護代碼的產生。像其它任何工具一樣,腳本編寫可以被使用或濫用。腳本語言靈活、易學,且編寫起來很快。但是Java IDE只對其提供了有限的支持,而且難于使用諸如JUnit之類的傳統測試框架對其進行測試,錯誤可能直到運行時才會出現。不過,在很多情況下,正確和適當地使用腳本編寫無疑會使生活更為輕松。應該考慮以下面的方式使用腳本編寫:
作為一種擴展或定制應用程序的手段。
作為一種實現頻繁改變的靈活(有時還很復雜)的業務規則的方便方式。
總而言之,腳本編寫支持無疑為Java開發人員的工具箱中有添加了一個新的得力工具。