標籤:函數式 方式 阻塞 asc running lib 應用程式 平台 return語句
原文地址:http://www.infoq.com/cn/articles/nashorn
從JDK 6開始,Java就已經捆綁了JavaScript引擎,該引擎基於Mozilla的Rhino。該特性允許開發人員將JavaScript代碼嵌入到Java中,甚至從嵌入的JavaScript中調用Java。此外,它還提供了使用jrunscript從命令列運行JavaScript的能力。如果不需要非常好的效能,並且可以接受ECMAScript 3有限的功能集的話,那它相當不錯了。
從JDK 8開始,Nashorn取代Rhino成為Java的嵌入式JavaScript引擎。Nashorn完全支援ECMAScript 5.1規範以及一些擴充。它使用基於JSR 292的新語言特性,其中包含在JDK 7中引入的invokedynamic,將JavaScript編譯成Java位元組碼。
與先前的Rhino實現相比,這帶來了2到10倍的效能提升,雖然它仍然比Chrome和Node.js中的V8引擎要差一些。如果你對實現細節感興趣,那麼可以看看這些來自2013 JVM語言峰會的投影片。
由於Nashorn隨JDK 8而來,它還增加了簡潔的函數式介面支援。接下來,我們很快就會看到更多細節。
讓我們從一個小例子開始。首先,你可能需要安裝JDK 8和NetBeans、IntelliJ IDEA或者Eclipse。對於整合JavaScript開發,它們都至少提供了基本的支援。讓我們建立一個簡單的Java項目,其中包含下面兩個樣本檔案,並運行它:
(點擊圖片可以查看大圖)
在第12行,我們使用引擎的“eval”方法對任意JavaScript代碼求值。在本樣本中,我們只是載入了上面的JavaScript檔案並對其求值。你可能會發現那個“print”並不熟悉。它不是JavaScript的內建函數,而是Nashorn提供的,它還提供了其它方便的、在指令碼環境中大有用武之地的函數。你也可以將 “hello world”的列印代碼直接嵌入到傳遞給“eval”方法的字串,但將JavaScript放在它自己的檔案中為其開啟了全新的工具世界。
Eclipse目前還沒有對Nashorn提供專門的支援,不過,通過JavaScript開發工具(JSDT)項目,它已經支援JavaScript的基本工具和編輯。
(點擊圖片可以查看大圖)
IntelliJ IDEA 13.1(社區版和旗艦版)提供了出色的JavaScript和Nashorn支援。它有一個全功能的調試器,甚至允許在Java和JavaScript之間保持重構同步,因此舉例來說,如果你重新命名一個被JavaScript引用的Java類,或者重新命名一個用於Java原始碼中的JavaScript檔案,那麼該IDE將跨語言修改相應的引用。
下面是一個例子,展示如何調試從Java調用的JavaScript(請注意,NetBeans也提供了JavaScript調試器,如下所示):
(點擊圖片可以查看大圖)
你可能會說,工具看上去不錯,而且新實現修複了效能以及一致性問題,但我為什麼應該用它呢?一個原因是一般的指令碼編寫。有時候,能夠直接插入任何類型的字串,並任由它被解釋,會很方便。有時候,沒有礙事的編譯器,或者不用為靜態類型擔心,可能也是不錯的。或者,你可能對Node.js編程模型感興趣,它也可以和Java一起使用,在本文的末尾我們會看到。另外,還有個情況不得不提一下,與Java相比,使用JavaScript進行JavaFX開發會快很多。
Shell指令碼
Nashorn引擎可以使用jjs命令從命令列調用。你可以不帶任何參數調用它,這會將你帶入一個互動模式,或者你可以傳遞一個希望執行的JavaScript檔案名稱,或者你可以用它作為shell指令碼的替代,像這樣:
#!/usr/bin/env jjs var name = $ARG[0]; print(name ? "Hello, ${name}!" : "Hello, world!");
向jjs傳遞程式參數,需要加“—”首碼。因此舉例來說,你可以這樣調用:
./hello-script.js – Joe
如果沒有“—”首碼,參數會被解釋為檔案名稱。
向Java傳遞資料或者從Java傳出資料
正如上文所說的那樣,你可以從Java代碼直接調用JavaScript;只需擷取一個引擎對象並調用它的“eval”方法。你可以將資料作為字串顯式傳遞……
ScriptEngineManager scriptEngineManager = new ScriptEngineManager(); ScriptEngine nashorn = scriptEngineManager.getEngineByName("nashorn"); String name = "Olli"; nashorn.eval("print(‘" + name + "‘)");
……或者你可以在Java中傳遞綁定,它們是可以從JavaScript引擎內部訪問的全域變數:
int valueIn = 10; SimpleBindings simpleBindings = new SimpleBindings(); simpleBindings.put("globalValue", valueIn); nashorn.eval("print (globalValue)", simpleBindings);
JavaScript eval的求值結果將會從引擎的“eval”方法返回:
Integer result = (Integer) nashorn.eval("1 + 2"); assert(result == 3);在Nashorn中使用Java類
前面已經提到,Nashorn最強大的功能之一源於在JavaScript中調用Java類。你不僅能夠訪問類並建立執行個體,你還可以繼承他們,調用他們的靜態方法,幾乎可以做任何你能在Java中做的事。
作為一個例子,讓我們看下來龍去脈。JavaScript沒有任何語言特性是面向並發的,所有常見的運行時環境都是單線程的,或者至少沒有任何共用狀態。有趣的是,在Nashorn環境中,JavaScript確實可以並發運行,並且有共用狀態,就像在Java中一樣:
// 訪問Java類Thread var Thread = Java.type("java.lang.Thread"); // 帶有run方法的子類var MyThread = Java.extend(Thread, { run: function() { print("Run in separate thread"); } }); var th = new MyThread(); th.start(); th.join();
請注意,從Nashorn訪問類的規範做法是使用Java.type,並且可以使用Java.extend擴充一個類。
令人高興的函數式
從各方面來說,隨著JDK 8的發布,Java——至少在某種程度上——已經變成一種函數式語言。開發人員可以在集合上使用高階函數,比如,遍曆所有的元素。高階函數是把另一個函數當作參數的函數,它可以用這個函數參數做些有意義的事情。請看下面Java中高階函數的樣本:
List<Integer> list = Arrays.asList(3, 4, 1, 2); list.forEach(new Consumer() { @Override public void accept(Object o) { System.out.println(o); } });
對於這個例子,我們的傳統實現方式是使用一個“外部”迴圈遍曆元素,但現在,我們沒有那樣做,而是將一個“Consumer”函數傳遞給了“forEach”操作,一個高階的“內部迴圈”操作會將集合中的每個元素一個一個地傳遞給Consumer的“accept”方法並執行它。
如上所述,對於這樣的高階函數,函數式語言的做法是接收一個函數參數,而不是一個對象。雖然在傳統上講,傳遞函數引用本身超出了Java的範圍,但現在,JDK 8有一些文法糖,使它可以使用Lambda運算式(又稱為“閉包”)來實現那種表示方式。例如:
List<Integer> list = Arrays.asList(3, 4, 1, 2); list.forEach(el -> System.out.println(el));
在這種情況下,“forEach”的參數是這樣一個函數引用的形式。這是可行的,因為Customer是一個函數式介面(有時稱為“單一抽象方法(Single Abstract Method)”類型或“SAM”)。
那麼,我們為什麼要在討論Nashorn時談論Lambda運算式呢?因為在JavaScript中,開發人員也可以這樣編寫代碼,而在這種情況下,Nashorn可以特別好地縮小Java和JavaScript之間的差距。尤其是,它甚至允許開發人員將純JavaScript函數作為函數式介面(SAM類型)的實現來傳遞。
讓我們來看一些純JavaScript代碼,它們與上述Java代碼實現一樣的功能。注意,在JavaScript中沒有內建的清單類型,只有數組;不過這些數組的大小是動態分配的,而且有與Java列表類似的方法。因此,在這個例子中,我們調用一個JavaScript數組的“for Each”方法:
var jsArray = [4,1,3,2]; jsArray.forEach(function(el) { print(el) } );
相似之處顯而易見;但那還不是全部。開發人員還可以將這樣一個JavaScript數群組轉換成一個Java列表:
var list = java.util.Arrays.asList(jsArray);
看見了嗎?是的,這就是在Nashorn中啟動並執行JavaScript。既然它現在是一個Java列表,那麼開發人員就可以調用其“forEach”方法。注意,這個“forEach”方法不同於我們在JavaScript數組上調用的那個,它是定義在java集合上的“forEach”方法。這裡,我們仍然傳遞一個純JavaScript函數:
list.forEach(function(el) { print(el) } );
Nashorn允許開發人員在需要使用函數式介面(SAM類型)的地方提供純JavaScript函數引用。這不僅適應於Java,也適應於JavaScript。
ECMAScript的下一個版本——預計是今年的最後一個版本——將包含函數的短文法,允許開發人員將函數寫成近似Java Lambda運算式的形式,只不過它使用雙箭頭=>。這進一步增強了一致性。
Nashorn JavaScript特有的方言
正如簡介部分所提到的那樣,Nashorn支援的JavaScript實現了ECMAScript 5.1版本及一些擴充。我並不建議使用這些擴充,因為它們既不是Java,也不是JavaScript,兩類開發人員都會覺得它不正常。另一方面,有兩個擴充在整個Oracle文檔中被大量使用,因此,我們應該瞭解它們。首先,讓我們為瞭解第一個擴充做些準備。正如前文所述,開發人員可以使用Java.extend從JavaScript中擴充一個Java類。如果需要繼承一個抽象Java類或者實現一個介面,那麼可以使用一種更簡便的文法。在這種情況下,開發人員實際上可以調用抽象類別或介面的建構函式,並傳入一個描述方法實現的JavaScript對象常量。這種常量不過是name/value對,你可能瞭解JSON格式,這與那個類似。這使我們可以像下面這樣實現Runnable介面:
var r = new java.lang.Runnable({ run: function() { print("running...\n"); }});
在這個例子中,一個對象常量指定了run方法的實現,我們實際上是用它調用了Runnable的建構函式。注意,這是Nashorn的實現提供給我們的一種方式,否則,我們無法在JavaScript這樣做。
範例程式碼已經與我們在Java中以匿名內部類實現介面的方式類似了,但還不完全一樣。這將我們帶到了第一個擴充,它允許開發人員在調用建構函式時在右括弧“)”後面傳遞最後一個參數。這種做法的代碼如下:
var r = new java.lang.Runnable() { run: function() { print("running...\n"); }};
……它實現了完全相同的功能,但更像Java。
第二個常用的擴充一種函數的簡便寫法,它允許刪除單行函數方法體中的兩個花括弧以及return語句。這樣,上一節中的例子:
list.forEach(function(el) { print(el) } );
可以表達的更簡潔一些:
list.forEach(function(el) print(el));
Avatar.js
我們已經看到,有了Nashorn,我們就有了一個嵌入到Java的優秀的JavaScript引擎。我們也已經看到,我們可以從Nashorn訪問任意Java類。Avatar.js更進一步,它“為Java平台帶來了Node編程模型、API和模組生態系統”。要瞭解這意味著什麼以及它為什麼令人振奮,我們首先必須瞭解Node是什麼。從根本上說,Node是將Chrome的V8 JavaScript引擎剝離出來,使它可以從命令列運行,而不再需要瀏覽器。這樣,JavaScript就不是只能在瀏覽器中運行了,而且可以在伺服器端運行。在伺服器端以任何有意義的方式運行JavaScript都至少需要訪問檔案系統和網路。為了做到這一點,Node內嵌了一個名為libnv的庫,以非同步方式實現該項功能。實際上,這意味著作業系統調用永遠不會阻塞,即使它過一段時間才能返回。開發人員需要提供一個回呼函數代替阻塞。該函數會在調用完成時立即觸發,如果有任何結果就返回。
有若干公司都在重要的應用程式中使用了Node,其中包括Walmart和Paypal。
讓我們來看一個JavaScript的小例子,它是我根據Node網站上的例子改寫而來:
//載入“http”模組(這是阻塞的)來處理http請求var http = require(‘http‘); //當有請求時,返回“Hello,World\n”function handleRequest(req, res) { res.writeHead(200, {‘Content-Type‘: ‘text/plain‘}); res.end(‘Hello, World\n‘); } //監聽localhost,連接埠1337//並提供回呼函數handleRequest//這裡體現了其非阻塞/非同步特性http.createServer(handleRequest).listen(1337, ‘127.0.0.1‘); //記錄到控制台,確保我們在沿著正確的方向前進console.log(‘Get your hello at http://127.0.0.1:1337/‘);
要運行這段代碼,需要安裝Node,然後將上述JavaScript代碼儲存到一個檔案中。最後,將該檔案作為一個參數調用Node。
將libuv綁定到Java類,並使JavaScript可以訪問它們,Avatar.js旨在以這種方式提供與Node相同的核心API。雖然這可能聽上去很繁瑣,但這種方法很有效。Avatar.js支援許多Node模組。對Node主流Web架構“express”的支援表明,這種方式確實適用於許多現有的項目。
令人遺憾的是,在寫這篇文章的時候,還沒有一個Avatar.js的二進位分發包。有一個讀我檔案說明了如何從原始碼進行構建,但是如果真沒有那麼多時間從頭開始構建,那麼也可以從這裡下載二進位檔案而不是自行構建。兩種方式都可以,但為了更快的得到結果,我建議選擇第二種方式。
一旦建立了二進位檔案並放進了lib檔案夾,就可以使用下面這樣的語句調用Avatar.js架構:
java -Djava.library.path=lib -jar lib/avatar-js.jar helloWorld.js
假設示範伺服器(上述代碼)儲存到了一個名為“helloWorld.js”的檔案中。
讓我們再問一次,這為什麼有用?Oracle的專家(投影片10)指出了該庫的幾個適用情境。我對其中的兩點持大致相同的看法,即:
- 有一個Node應用程式,並希望使用某個Java庫作為Node API的補充
- 希望切換到JavaScript和Node API,但需要將遺留的Java代碼部分或全部嵌入
兩個應用情境都可以通過使用Avatar.js並從JavaScript代碼中調用任何需要的Java類來實現。我們已經看到,Nashorn支援這種做法。
下面我將舉一個第一個應用情境的例子。JavaScript目前只有一種表示數值的類型,名為“number”。這相當於Java的“double”精度,並且有同樣的限制。JavaScript的number,像Java的double一樣,並不能表示任意的範圍和精度,比如在計量貨幣時。
在Java中,我們可以使用BigDecimal,它正是用於此類情況。但JavaScript沒有內建與此等效的類型,因此,我們就可以直接從JavaScript代碼中訪問BigDecimal類,安全地處理貨幣值。
讓我們看一個Web服務樣本,它計算某個數量的百分之幾是多少。首先,需要有一個函數執行實際的計算:
var BigDecimal = Java.type(‘java.math.BigDecimal‘); function calculatePercentage(amount, percentage) { var result = new BigDecimal(amount).multiply( new BigDecimal(percentage)).divide( new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_EVEN); return result.toPlainString(); }
JavaScript沒有型別宣告,除此之外,上述代碼與我針對該任務編寫的Java代碼非常像:
public static String calculate(String amount, String percentage) { BigDecimal result = new BigDecimal(amount).multiply( new BigDecimal(percentage)).divide( new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_EVEN); return result.toPlainString(); }
我們只需要替換上文Node樣本中的handleRequest函數就可以完成代碼。替換後的代碼如下:
//載入工具模組“url”來解析urlvar url = require(‘url‘); function handleRequest(req, res) { // ‘/calculate‘ Web服務地址 if (url.parse(req.url).pathname === ‘/calculate‘) { var query = url.parse(req.url, true).query; //數量和百分比作為查詢參數傳入 var result = calculatePercentage(query.amount, query.percentage); res.writeHead(200, {‘Content-Type‘: ‘text/plain‘}); res.end(result + ‘\n‘); } }
我們又使用了Node核心模組來處理請求URL,從中解析出查詢參數amount和percentage。
當啟動伺服器(如前所述)並使用瀏覽器發出下面這樣一個請求時,
http://localhost:1337/calculate?amount=99700000000000000086958613&percentage=7.59
就會得到正確的結果“7567230000000000006600158.73”。這在單純使用JavaScript的“number”類型時是不可能。
當你決定將現有的JEE應用程式遷移到JavaScript和Node時,第二個應用情境就有意義了。在這種情況下,你很容易就可以從JavaScript代碼內訪問現有的所有服務。另一個相關的應用情境是,在使用JavaScript和Node構建新的伺服器功能時,仍然可以受益於現有的JEE服務。
此外,基於Avatar.js的Avatar項目也朝著相同的方向發展。該項目的詳細資料超出了本文的討論範圍,但讀者可以閱讀這份Oracle公告做一個粗略的瞭解。該項目的基本思想是,用JavaScript編寫應用程式,並訪問JEE服務。Avatar項目包含Avatar.js的一個二進位分發包,但它需要Glassfish用於安裝和開發。
小結
Nashorn項目增強了JDK 6中原有的Rhino實現,極大地提升了已耗用時間較長的應用程式的效能,例如用在Web伺服器中的時候。Nashorn將Java與JavaScript整合,甚至還考慮了JDK 8的新Lambda運算式。Avatar.js帶來了真正的創新,它基於這些特性構建,並提供了企業級Java與JavaScript代碼的整合,同時在很大程度上與JavaScript伺服器端編程事實上的標準相容。
完整執行個體以及用於Mac OS X的Avatar.js二進位檔案可以從Github上下載。
Nashorn——在JDK 8中融合Java與JavaScript之力--轉