[譯] Java 8 Nashorn 教程

轉自:https://segmentfault.com/a/1190000006041626#articleHeader1

[譯] Java 8 Nashorn 教程

這個教程中,你會通過簡單易懂的代碼示例,來了解Nashorn JavaScript引擎。Nashorn JavaScript引擎是Java SE 8 的一部分,並且和其它獨立的引擎例如Google V8(用於Google Chrome和Node.js的引擎)互相競爭。Nashorn通過在JVM上,以原生方式運行動態的JavaScript代碼來擴展Java的功能。

在接下來的15分鐘內,你會學到如何在JVM上在運行時動態執行JavaScript。我會使用小段代碼示例來演示最新的Nashron語言特性。你會學到如何在Java代碼中調用JavaScript函數,或者相反。最後你會準備好將動態腳本集成到你的Java日常業務中。

更新 - 我現在正在編寫用於瀏覽器的Java8數據流API的JavaScript實現。如果你對此感興趣,請在Github上訪問Stream.js。非常期待你的反饋。

使用 Nashron

Nashorn JavaScript引擎可以在Java代碼中編程調用,也可以通過命令行工具jjs使用,它在$JAVA_HOME/bin中。如果打算使用jjs,你可能希望設置符號鏈接來簡化訪問:

$ cd /usr/bin
$ ln -s $JAVA_HOME/bin/jjs jjs
$ jjs
jjs> print('Hello World');

這個教程專注於在Java代碼中調用Nashron,所以讓我們先跳過jjs。Java代碼中簡單的HelloWorld如下所示:

ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
engine.eval("print('Hello World!');");

爲了在Java中執行JavaScript,你首先要通過javax.script包創建腳本引擎。這個包已經在Rhino(來源於Mozilla、Java中的遺留JS引擎)中使用了。

JavaScript代碼既可以通過傳遞JavaScript代碼字符串,也可以傳遞指向你的JS腳本文件的FileReader來執行:

ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
engine.eval(new FileReader("script.js"));

Nashorn JavaScript基於ECMAScript 5.1,但是它的後續版本會對ES6提供支持:

Nashorn的當前策略遵循ECMAScript規範。當我們在JDK8中發佈它時,它將基於ECMAScript 5.1。Nashorn未來的主要發佈基於ECMAScript 6

Nashorn定義了大量對ECMAScript標準的語言和API擴展。但是首先讓我們看一看Java和JavaScript代碼如何交互。

在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));
};

爲了調用函數,你首先需要將腳本引擎轉換爲InvocableInvocable接口由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方法

在JavaScript中調用Java方法十分容易。我們首先需要定義一個Java靜態方法。

static String fun1(String name) {
    System.out.format("Hi there from Java, %s", name);
    return "greetings from java";
}

Java類可以通過Java.typeAPI擴展在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表達式和數據流

每個人都熱愛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

你可以簡單地使用屬性名稱來向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.typeJavaImporter直接訪問:

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([35711], "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上向我反饋。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章