Java 8 Nashorn 教程
譯者:飛龍
協(xié)議:CC BY-NC-SA 4.0
這個(gè)教程中撵割,你會(huì)通過(guò)簡(jiǎn)單易懂的代碼示例,來(lái)了解Nashorn JavaScript引擎谤绳。Nashorn JavaScript引擎是Java SE 8 的一部分,并且和其它獨(dú)立的引擎例如Google V8(用于Google Chrome和Node.js的引擎)互相競(jìng)爭(zhēng)卓嫂。Nashorn通過(guò)在JVM上且预,以原生方式運(yùn)行動(dòng)態(tài)的JavaScript代碼來(lái)擴(kuò)展Java的功能。
在接下來(lái)的15分鐘內(nèi)掌呜,你會(huì)學(xué)到如何在JVM上在運(yùn)行時(shí)動(dòng)態(tài)執(zhí)行JavaScript。我會(huì)使用小段代碼示例來(lái)演示最新的Nashron語(yǔ)言特性坪哄。你會(huì)學(xué)到如何在Java代碼中調(diào)用JavaScript函數(shù)质蕉,或者相反势篡。最后你會(huì)準(zhǔn)備好將動(dòng)態(tài)腳本集成到你的Java日常業(yè)務(wù)中。
![](http://winterbe.com/image/posts/nashorn.jpg)
更新 - 我現(xiàn)在正在編寫用于瀏覽器的Java8數(shù)據(jù)流API的JavaScript實(shí)現(xiàn)模暗。如果你對(duì)此感興趣禁悠,請(qǐng)?jiān)贕ithub上訪問(wèn)Stream.js。非常期待你的反饋兑宇。
使用 Nashron
Nashorn JavaScript引擎可以在Java代碼中編程調(diào)用碍侦,也可以通過(guò)命令行工具jjs
使用,它在$JAVA_HOME/bin
中隶糕。如果打算使用jjs
瓷产,你可能希望設(shè)置符號(hào)鏈接來(lái)簡(jiǎn)化訪問(wèn):
$ cd /usr/bin
$ ln -s $JAVA_HOME/bin/jjs jjs
$ jjs
jjs> print('Hello World');
這個(gè)教程專注于在Java代碼中調(diào)用Nashron,所以讓我們先跳過(guò)jjs
枚驻。Java代碼中簡(jiǎn)單的HelloWorld如下所示:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
engine.eval("print('Hello World!');");
為了在Java中執(zhí)行JavaScript濒旦,你首先要通過(guò)javax.script
包創(chuàng)建腳本引擎。這個(gè)包已經(jīng)在Rhino(來(lái)源于Mozilla再登、Java中的遺留JS引擎)中使用了尔邓。
JavaScript代碼既可以通過(guò)傳遞JavaScript代碼字符串,也可以傳遞指向你的JS腳本文件的FileReader
來(lái)執(zhí)行:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
engine.eval(new FileReader("script.js"));
Nashorn JavaScript基于ECMAScript 5.1锉矢,但是它的后續(xù)版本會(huì)對(duì)ES6提供支持:
Nashorn的當(dāng)前策略遵循ECMAScript規(guī)范梯嗽。當(dāng)我們?cè)贘DK8中發(fā)布它時(shí),它將基于ECMAScript 5.1沈撞。Nashorn未來(lái)的主要發(fā)布基于ECMAScript 6慷荔。
Nashorn定義了大量對(duì)ECMAScript標(biāo)準(zhǔn)的語(yǔ)言和API擴(kuò)展雕什。但是首先讓我們看一看Java和JavaScript代碼如何交互缠俺。
在Java中調(diào)用JavaScript函數(shù)
Nashorn 支持從Java代碼中直接調(diào)用定義在腳本文件中的JavaScript函數(shù)。你可以將Java對(duì)象傳遞為函數(shù)參數(shù)贷岸,并且從函數(shù)返回?cái)?shù)據(jù)來(lái)調(diào)用Java方法壹士。
下面的JavaScript函數(shù)稍后會(huì)在Java端調(diào)用:
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));
};
為了調(diào)用函數(shù),你首先需要將腳本引擎轉(zhuǎn)換為Invocable
偿警。Invocable
接口由NashornScriptEngine
實(shí)現(xiàn)躏救,并且定義了invokeFunction
方法來(lái)調(diào)用指定名稱的JavaScript函數(shù)。
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
執(zhí)行這段代碼會(huì)在控制臺(tái)產(chǎn)生三行結(jié)果螟蒸。調(diào)用函數(shù)print
將結(jié)果輸出到System.out
盒使,所以我們會(huì)首先看到JavaScript輸出。
現(xiàn)在讓我們通過(guò)傳入任意Java對(duì)象來(lái)調(diào)用第二個(gè)函數(shù):
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對(duì)象在傳入時(shí)不會(huì)在JavaScript端損失任何類型信息七嫌。由于腳本在JVM上原生運(yùn)行少办,我們可以在Nashron上使用Java API或外部庫(kù)的全部功能。
在JavaScript中調(diào)用Java方法
在JavaScript中調(diào)用Java方法十分容易诵原。我們首先需要定義一個(gè)Java靜態(tài)方法英妓。
static String fun1(String name) {
System.out.format("Hi there from Java, %s", name);
return "greetings from java";
}
Java類可以通過(guò)Java.type
API擴(kuò)展在JavaScript中引用挽放。它就和Java代碼中的import
類似。只要定義了Java類型蔓纠,我們就可以自然地調(diào)用靜態(tài)方法fun1()
辑畦,然后像sout
打印信息。由于方法是靜態(tài)的腿倚,我們不需要首先創(chuàng)建實(shí)例纯出。
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原生類型調(diào)用Java方法時(shí),Nashorn 如何處理類型轉(zhuǎn)換猴誊?讓我們通過(guò)簡(jiǎn)單的例子來(lái)弄清楚潦刃。
下面的Java方法簡(jiǎn)單打印了方法參數(shù)的實(shí)際類型:
static void fun2(Object object) {
System.out.println(object.getClass());
}
為了理解背后如何處理類型轉(zhuǎn)換,我們使用不同的JavaScript類型來(lái)調(diào)用這個(gè)方法:
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原始類型轉(zhuǎn)換為合適的Java包裝類懈叹,而JavaScript原生對(duì)象會(huì)使用內(nèi)部的適配器類來(lái)表示乖杠。要記住jdk.nashorn.internal
中的類可能會(huì)有所變化,所以不應(yīng)該在客戶端面向這些類來(lái)編程澄成。
任何標(biāo)記為“內(nèi)部”的東西都可能會(huì)從你那里發(fā)生改變胧洒。
ScriptObjectMirror
在向Java傳遞原生JavaScript對(duì)象時(shí),你可以使用ScriptObjectMirror
類墨状,它實(shí)際上是底層JavaScript對(duì)象的Java表示卫漫。ScriptObjectMirror
實(shí)現(xiàn)了Map
接口,位于jdk.nashorn.api
中肾砂。這個(gè)包中的類可以用于客戶端代碼列赎。
下面的例子將參數(shù)類型從Object
改為ScriptObjectMirror
,所以我們可以從傳入的JavaScript對(duì)象中獲得一些信息镐确。
static void fun3(ScriptObjectMirror mirror) {
System.out.println(mirror.getClassName() + ": " +
Arrays.toString(mirror.getOwnKeys(true)));
}
當(dāng)向這個(gè)方法傳遞對(duì)象(哈希表)時(shí)包吝,在Java端可以訪問(wèn)其屬性:
MyJavaClass.fun3({
foo: 'bar',
bar: 'foo'
});
// Object: [foo, bar]
我們也可以在Java中調(diào)用JavaScript的成員函數(shù)。讓我們首先定義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
可以通過(guò)callMember()
在ScriptObjectMirror
上調(diào)用息堂。
static void fun4(ScriptObjectMirror person) {
System.out.println("Full Name is: " + person.callMember("getFullName"));
}
當(dāng)向Java方法傳遞新的Person
時(shí)嚷狞,我們會(huì)在控制臺(tái)看到預(yù)期的結(jié)果:
var person1 = new Person("Peter", "Parker");
MyJavaClass.fun4(person1);
// Full Name is: Peter Parker
語(yǔ)言擴(kuò)展
Nashorn定義了多種對(duì)ECMAScript標(biāo)準(zhǔn)的語(yǔ)言和API擴(kuò)展。讓我們看一看最新的特性:
類型數(shù)組
JavaScript的原生數(shù)組是無(wú)類型的荣堰。Nashron允許你在JavaScript中使用Java的類型數(shù)組:
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[]
數(shù)組就像真實(shí)的Java整數(shù)數(shù)組那樣床未。但是此外,在我們?cè)噲D向數(shù)組添加非整數(shù)時(shí)振坚,Nashron在背后執(zhí)行了一些隱式的轉(zhuǎn)換薇搁。字符串會(huì)自動(dòng)轉(zhuǎn)換為整數(shù),這十分便利屡拨。
集合和范圍遍歷
我們可以使用任何Java集合只酥,而避免使用數(shù)組瞎折騰褥实。首先需要通過(guò)Java.type
定義Java類型,之后創(chuàng)建新的實(shí)例裂允。
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
為了迭代集合和數(shù)組损离,Nashron引入了for each
語(yǔ)句。它就像Java的范圍遍歷那樣工作绝编。
下面是另一個(gè)集合的范圍遍歷示例僻澎,使用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表達(dá)式和數(shù)據(jù)流
每個(gè)人都熱愛lambda和數(shù)據(jù)流 -- Nashron也一樣!雖然ECMAScript 5.1沒有Java8 lmbda表達(dá)式的簡(jiǎn)化箭頭語(yǔ)法十饥,我們可以在任何接受lambda表達(dá)式的地方使用函數(shù)字面值窟勃。
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
輕易擴(kuò)展。就像你在下面的例子中看到的那樣逗堵,你甚至可以在你的腳本中創(chuàng)建多線程的代碼:
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
參數(shù)重載
方法和函數(shù)可以通過(guò)點(diǎn)運(yùn)算符或方括號(hào)運(yùn)算符來(lái)調(diào)用:
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
當(dāng)使用重載參數(shù)調(diào)用方法時(shí)秉氧,傳遞可選參數(shù)類型println(double)
會(huì)指定所調(diào)用的具體方法。
Java Beans
你可以簡(jiǎn)單地使用屬性名稱來(lái)向Java Beans獲取或設(shè)置值蜒秤,不需要顯式調(diào)用讀寫器:
var Date = Java.type('java.util.Date');
var date = new Date();
date.year += 1900;
print(date.year); // 2014
函數(shù)字面值
對(duì)于簡(jiǎn)單的單行函數(shù)汁咏,我們可以去掉花括號(hào):
function sqr(x) x * x;
print(sqr(3)); // 9
屬性綁定
兩個(gè)不同對(duì)象的屬性可以綁定到一起:
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__);
導(dǎo)入作用域
有時(shí)一次導(dǎo)入多個(gè)Java包會(huì)很方便。我們可以使用JavaImporter
類作媚,和with
語(yǔ)句一起使用攘滩。所有被導(dǎo)入包的類文件都可以在with
語(yǔ)句的局部域中訪問(wèn)到。
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
}
數(shù)組轉(zhuǎn)換
一些類似java.util
的包可以不使用java.type
或JavaImporter
直接訪問(wèn):
var list = new java.util.ArrayList();
list.add("s1");
list.add("s2");
list.add("s3");
下面的代碼將Java列表轉(zhuǎn)換為JavaScript原生數(shù)組:
var jsArray = Java.from(list);
print(jsArray); // s1,s2,s3
print(Object.prototype.toString.call(jsArray)); // [object Array]
下面的代碼執(zhí)行相反操作:
var javaArray = Java.to([3, 5, 7, 11], "int[]");
訪問(wèn)超類
在JavaScript中訪問(wèn)被覆蓋的成員通常比較困難纸泡,因?yàn)镴ava的super
關(guān)鍵字在ECMAScript中并不存在漂问。幸運(yùn)的是,Nashron有一套補(bǔ)救措施女揭。
首先我們需要在Java代碼中定義超類:
class SuperRunner implements Runnable {
@Override
public void run() {
System.out.println("super run");
}
}
下面我在JavaScript中覆蓋了SuperRunner
蚤假。要注意創(chuàng)建新的Runner
實(shí)例時(shí)的Nashron語(yǔ)法:覆蓋成員的語(yǔ)法取自Java的匿名對(duì)象。
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
我們通過(guò)Java.super()
擴(kuò)展調(diào)用了被覆蓋的SuperRunner.run()
方法田绑。
加載腳本
在JavaScript中加載額外的腳本文件非常方便勤哗。我們可以使用load
函數(shù)加載本地或遠(yuǎn)程腳本抡爹。
我在我的Web前端中大量使用Underscore.js掩驱,所以讓我們?cè)贜ashron中復(fù)用它:
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
外部腳本會(huì)在相同JavaScript上下文中被執(zhí)行,所以我們可以直接訪問(wèn)underscore 的對(duì)象冬竟。要記住當(dāng)變量名稱互相沖突時(shí)欧穴,腳本的加載可能會(huì)使你的代碼崩潰。
這一問(wèn)題可以通過(guò)把腳本文件加載到新的全局上下文來(lái)繞過(guò):
loadWithNewGlobal('script.js');
命令行腳本
如果你對(duì)編寫命令行(shell)腳本感興趣泵殴,來(lái)試一試Nake吧涮帘。Nake是一個(gè)Java 8 Nashron的簡(jiǎn)化構(gòu)建工具。你只需要在項(xiàng)目特定的Nakefile
中定義任務(wù)笑诅,之后通過(guò)在命令行鍵入nake -- myTask
來(lái)執(zhí)行這些任務(wù)调缨。任務(wù)編寫為JavaScript疮鲫,并且在Nashron的腳本模式下運(yùn)行,所以你可以使用你的終端弦叶、JDK8 API和任意Java庫(kù)的全部功能俊犯。
對(duì)Java開發(fā)者來(lái)說(shuō),編寫命令行腳本是前所未有的簡(jiǎn)單...
到此為止
我希望這個(gè)教程對(duì)你有所幫助伤哺,并且你能夠享受Nashron JavaScript引擎之旅燕侠。有關(guān)Nashron的更多信息,請(qǐng)見這里立莉、這里和這里绢彤。使用Nashron編寫shell腳本的教程請(qǐng)見這里。
我最近發(fā)布了一篇后續(xù)文章蜓耻,關(guān)于如何在Nashron中使用Backbone.js模型茫舶。如果你想要進(jìn)一步學(xué)習(xí)Java8,請(qǐng)閱讀我的Java8教程刹淌,和我的Java8數(shù)據(jù)流教程奇适。
這篇Nashron教程中的可運(yùn)行的源代碼托管在Github上。請(qǐng)隨意fork我的倉(cāng)庫(kù)芦鳍,或者在Twitter上向我反饋嚷往。
請(qǐng)堅(jiān)持編程!