原文:Java 8 Nashorn Tutorial
譯者:飛龍
協議:CC BY-NC-SA 4.0javascript
這個教程中,你會經過簡單易懂的代碼示例,來了解Nashorn JavaScript引擎。Nashorn JavaScript引擎是Java SE 8 的一部分,而且和其它獨立的引擎例如Google V8(用於Google Chrome和Node.js的引擎)互相競爭。Nashorn經過在JVM上,以原生方式運行動態的JavaScript代碼來擴展Java的功能。php
在接下來的15分鐘內,你會學到如何在JVM上在運行時動態執行JavaScript。我會使用小段代碼示例來演示最新的Nashron語言特性。你會學到如何在Java代碼中調用JavaScript函數,或者相反。最後你會準備好將動態腳本集成到你的Java平常業務中。html
更新 - 我如今正在編寫用於瀏覽器的Java8數據流API的JavaScript實現。若是你對此感興趣,請在Github上訪問Stream.js。很是期待你的反饋。前端
Nashorn JavaScript引擎能夠在Java代碼中編程調用,也能夠經過命令行工具jjs
使用,它在$JAVA_HOME/bin
中。若是打算使用jjs
,你可能但願設置符號連接來簡化訪問:java
$ cd /usr/bin $ ln -s $JAVA_HOME/bin/jjs jjs $ jjs jjs> print('Hello World');
這個教程專一於在Java代碼中調用Nashron,因此讓咱們先跳過jjs
。Java代碼中簡單的HelloWorld以下所示:node
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); engine.eval("print('Hello World!');");
爲了在Java中執行JavaScript,你首先要經過javax.script
包建立腳本引擎。這個包已經在Rhino(來源於Mozilla、Java中的遺留JS引擎)中使用了。git
JavaScript代碼既能夠經過傳遞JavaScript代碼字符串,也能夠傳遞指向你的JS腳本文件的FileReader
來執行:github
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); engine.eval(new FileReader("script.js"));
Nashorn JavaScript基於ECMAScript 5.1,可是它的後續版本會對ES6提供支持:ajax
Nashorn的當前策略遵循ECMAScript規範。當咱們在JDK8中發佈它時,它將基於ECMAScript 5.1。Nashorn將來的主要發佈基於ECMAScript 6。shell
Nashorn定義了大量對ECMAScript標準的語言和API擴展。可是首先讓咱們看一看Java和JavaScript代碼如何交互。
Nashorn 支持從Java代碼中直接調用定義在腳本文件中的JavaScript函數。你能夠將Java對象傳遞爲函數參數,而且從函數返回數據來調用Java方法。
下面的JavaScript函數稍後會在Java端調用:
var fun1 = function(name) { print('Hi there from Javascript, ' + name); return "greetings from javascript"; }; var fun2 = function (object) { print("JS Class Definition: " + Object.prototype.toString.call(object)); };
爲了調用函數,你首先須要將腳本引擎轉換爲Invocable
。Invocable
接口由NashornScriptEngine
實現,而且定義了invokeFunction
方法來調用指定名稱的JavaScript函數。
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); engine.eval(new FileReader("script.js")); Invocable invocable = (Invocable) engine; Object result = invocable.invokeFunction("fun1", "Peter Parker"); System.out.println(result); System.out.println(result.getClass()); // Hi there from Javascript, Peter Parker // greetings from javascript // class java.lang.String
執行這段代碼會在控制檯產生三行結果。調用函數print
將結果輸出到System.out
,因此咱們會首先看到JavaScript輸出。
如今讓咱們經過傳入任意Java對象來調用第二個函數:
invocable.invokeFunction("fun2", new Date()); // [object java.util.Date] invocable.invokeFunction("fun2", LocalDateTime.now()); // [object java.time.LocalDateTime] invocable.invokeFunction("fun2", new Person()); // [object com.winterbe.java8.Person]
Java對象在傳入時不會在JavaScript端損失任何類型信息。因爲腳本在JVM上原生運行,咱們能夠在Nashron上使用Java API或外部庫的所有功能。
在JavaScript中調用Java方法十分容易。咱們首先須要定義一個Java靜態方法。
static String fun1(String name) { System.out.format("Hi there from Java, %s", name); return "greetings from java"; }
Java類能夠經過Java.type
API擴展在JavaScript中引用。它就和Java代碼中的import
相似。只要定義了Java類型,咱們就能夠天然地調用靜態方法fun1()
,而後像sout
打印信息。因爲方法是靜態的,咱們不須要首先建立實例。
var MyJavaClass = Java.type('my.package.MyJavaClass'); var result = MyJavaClass.fun1('John Doe'); print(result); // Hi there from Java, John Doe // greetings from java
在使用JavaScript原生類型調用Java方法時,Nashorn 如何處理類型轉換?讓咱們經過簡單的例子來弄清楚。
下面的Java方法簡單打印了方法參數的實際類型:
static void fun2(Object object) { System.out.println(object.getClass()); }
爲了理解背後如何處理類型轉換,咱們使用不一樣的JavaScript類型來調用這個方法:
MyJavaClass.fun2(123); // class java.lang.Integer MyJavaClass.fun2(49.99); // class java.lang.Double MyJavaClass.fun2(true); // class java.lang.Boolean MyJavaClass.fun2("hi there") // class java.lang.String MyJavaClass.fun2(new Number(23)); // class jdk.nashorn.internal.objects.NativeNumber MyJavaClass.fun2(new Date()); // class jdk.nashorn.internal.objects.NativeDate MyJavaClass.fun2(new RegExp()); // class jdk.nashorn.internal.objects.NativeRegExp MyJavaClass.fun2({foo: 'bar'}); // class jdk.nashorn.internal.scripts.JO4
JavaScript原始類型轉換爲合適的Java包裝類,而JavaScript原生對象會使用內部的適配器類來表示。要記住jdk.nashorn.internal
中的類可能會有所變化,因此不該該在客戶端面向這些類來編程。
任何標記爲「內部」的東西均可能會從你那裏發生改變。
ScriptObjectMirror
在向Java傳遞原生JavaScript對象時,你可使用ScriptObjectMirror
類,它其實是底層JavaScript對象的Java表示。ScriptObjectMirror
實現了Map
接口,位於jdk.nashorn.api
中。這個包中的類能夠用於客戶端代碼。
下面的例子將參數類型從Object
改成ScriptObjectMirror
,因此咱們能夠從傳入的JavaScript對象中得到一些信息。
static void fun3(ScriptObjectMirror mirror) { System.out.println(mirror.getClassName() + ": " + Arrays.toString(mirror.getOwnKeys(true))); }
當向這個方法傳遞對象(哈希表)時,在Java端能夠訪問其屬性:
MyJavaClass.fun3({ foo: 'bar', bar: 'foo' }); // Object: [foo, bar]
咱們也能夠在Java中調用JavaScript的成員函數。讓咱們首先定義JavaScript Person
類型,帶有屬性firstName
和 lastName
,以及方法getFullName
。
function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; this.getFullName = function() { return this.firstName + " " + this.lastName; } }
JavaScript方法getFullName
能夠經過callMember()
在ScriptObjectMirror
上調用。
static void fun4(ScriptObjectMirror person) { System.out.println("Full Name is: " + person.callMember("getFullName")); }
當向Java方法傳遞新的Person
時,咱們會在控制檯看到預期的結果:
var person1 = new Person("Peter", "Parker"); MyJavaClass.fun4(person1); // Full Name is: Peter Parker
Nashorn定義了多種對ECMAScript標準的語言和API擴展。讓咱們看一看最新的特性:
JavaScript的原生數組是無類型的。Nashron容許你在JavaScript中使用Java的類型數組:
var IntArray = Java.type("int[]"); var array = new IntArray(5); array[0] = 5; array[1] = 4; array[2] = 3; array[3] = 2; array[4] = 1; try { array[5] = 23; } catch (e) { print(e.message); // Array index out of range: 5 } array[0] = "17"; print(array[0]); // 17 array[0] = "wrong type"; print(array[0]); // 0 array[0] = "17.3"; print(array[0]); // 17
int[]
數組就像真實的Java整數數組那樣。可是此外,在咱們試圖向數組添加非整數時,Nashron在背後執行了一些隱式的轉換。字符串會自動轉換爲整數,這十分便利。
咱們可使用任何Java集合,而避免使用數組瞎折騰。首先須要經過Java.type
定義Java類型,以後建立新的實例。
var ArrayList = Java.type('java.util.ArrayList'); var list = new ArrayList(); list.add('a'); list.add('b'); list.add('c'); for each (var el in list) print(el); // a, b, c
爲了迭代集合和數組,Nashron引入了for each
語句。它就像Java的範圍遍歷那樣工做。
下面是另外一個集合的範圍遍歷示例,使用HashMap
:
var map = new java.util.HashMap(); map.put('foo', 'val1'); map.put('bar', 'val2'); for each (var e in map.keySet()) print(e); // foo, bar for each (var e in map.values()) print(e); // val1, val2
每一個人都熱愛lambda和數據流 -- Nashron也同樣!雖然ECMAScript 5.1沒有Java8 lmbda表達式的簡化箭頭語法,咱們能夠在任何接受lambda表達式的地方使用函數字面值。
var list2 = new java.util.ArrayList(); list2.add("ddd2"); list2.add("aaa2"); list2.add("bbb1"); list2.add("aaa1"); list2.add("bbb3"); list2.add("ccc"); list2.add("bbb2"); list2.add("ddd1"); list2 .stream() .filter(function(el) { return el.startsWith("aaa"); }) .sorted() .forEach(function(el) { print(el); }); // aaa1, aaa2
Java類型能夠由Java.extend
輕易擴展。就像你在下面的例子中看到的那樣,你甚至能夠在你的腳本中建立多線程的代碼:
var Runnable = Java.type('java.lang.Runnable'); var Printer = Java.extend(Runnable, { run: function() { print('printed from a separate thread'); } }); var Thread = Java.type('java.lang.Thread'); new Thread(new Printer()).start(); new Thread(function() { print('printed from another thread'); }).start(); // printed from a separate thread // printed from another thread
方法和函數能夠經過點運算符或方括號運算符來調用:
var System = Java.type('java.lang.System'); System.out.println(10); // 10 System.out["println"](11.0); // 11.0 System.out["println(double)"](12); // 12.0
當使用重載參數調用方法時,傳遞可選參數類型println(double)
會指定所調用的具體方法。
你能夠簡單地使用屬性名稱來向Java Beans獲取或設置值,不須要顯式調用讀寫器:
var Date = Java.type('java.util.Date'); var date = new Date(); date.year += 1900; print(date.year); // 2014
對於簡單的單行函數,咱們能夠去掉花括號:
function sqr(x) x * x; print(sqr(3)); // 9
兩個不一樣對象的屬性能夠綁定到一塊兒:
var o1 = {}; var o2 = { foo: 'bar'}; Object.bindProperties(o1, o2); print(o1.foo); // bar o1.foo = 'BAM'; print(o2.foo); // BAM
我喜歡去掉空白的字符串:
print(" hehe".trimLeft()); // hehe print("hehe ".trimRight() + "he"); // hehehe
以防你忘了本身在哪裏:
print(__FILE__, __LINE__, __DIR__);
有時一次導入多個Java包會很方便。咱們可使用JavaImporter
類,和with
語句一塊兒使用。全部被導入包的類文件均可以在with
語句的局部域中訪問到。
var imports = new JavaImporter(java.io, java.lang); with (imports) { var file = new File(__FILE__); System.out.println(file.getAbsolutePath()); // /path/to/my/script.js }
一些相似java.util
的包能夠不使用java.type
或JavaImporter
直接訪問:
var list = new java.util.ArrayList(); list.add("s1"); list.add("s2"); list.add("s3");
下面的代碼將Java列表轉換爲JavaScript原生數組:
var jsArray = Java.from(list); print(jsArray); // s1,s2,s3 print(Object.prototype.toString.call(jsArray)); // [object Array]
下面的代碼執行相反操做:
var javaArray = Java.to([3, 5, 7, 11], "int[]");
在JavaScript中訪問被覆蓋的成員一般比較困難,由於Java的super
關鍵字在ECMAScript中並不存在。幸運的是,Nashron有一套補救措施。
首先咱們須要在Java代碼中定義超類:
class SuperRunner implements Runnable { @Override public void run() { System.out.println("super run"); } }
下面我在JavaScript中覆蓋了SuperRunner
。要注意建立新的Runner
實例時的Nashron語法:覆蓋成員的語法取自Java的匿名對象。
var SuperRunner = Java.type('com.winterbe.java8.SuperRunner'); var Runner = Java.extend(SuperRunner); var runner = new Runner() { run: function() { Java.super(runner).run(); print('on my run'); } } runner.run(); // super run // on my run
咱們經過Java.super()
擴展調用了被覆蓋的SuperRunner.run()
方法。
在JavaScript中加載額外的腳本文件很是方便。咱們可使用load
函數加載本地或遠程腳本。
我在個人Web前端中大量使用Underscore.js,因此讓咱們在Nashron中複用它:
load('http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js'); var odds = _.filter([1, 2, 3, 4, 5, 6], function (num) { return num % 2 == 1; }); print(odds); // 1, 3, 5
外部腳本會在相同JavaScript上下文中被執行,因此咱們能夠直接訪問underscore 的對象。要記住當變量名稱互相沖突時,腳本的加載可能會使你的代碼崩潰。
這一問題能夠經過把腳本文件加載到新的全局上下文來繞過:
loadWithNewGlobal('script.js');
若是你對編寫命令行(shell)腳本感興趣,來試一試Nake吧。Nake是一個Java 8 Nashron的簡化構建工具。你只須要在項目特定的Nakefile
中定義任務,以後經過在命令行鍵入nake -- myTask
來執行這些任務。任務編寫爲JavaScript,而且在Nashron的腳本模式下運行,因此你可使用你的終端、JDK8 API和任意Java庫的所有功能。
對Java開發者來講,編寫命令行腳本是史無前例的簡單...
我但願這個教程對你有所幫助,而且你可以享受Nashron JavaScript引擎之旅。有關Nashron的更多信息,請見這裏、這裏和這裏。使用Nashron編寫shell腳本的教程請見這裏。
我最近發佈了一篇後續文章,關於如何在Nashron中使用Backbone.js模型。若是你想要進一步學習Java8,請閱讀個人Java8教程,和個人Java8數據流教程。
這篇Nashron教程中的可運行的源代碼託管在Github上。請隨意fork個人倉庫,或者在Twitter上向我反饋。
請堅持編程!