Java 8的Nashorn腳本引擎教程
本文為了解所有關于 Nashorn JavaScript 引擎易于理解的代碼例子。 Nashorn JavaScript 引擎是Java SE 8的一部分,它與其它像Google V8 (它是Google Chrome 和Node.js的引擎)的獨立引擎相互競爭。 Nashorn 擴展了Java在JVM上運行動態JavaScript腳本的能力。
在接下來的大約15分鐘里,您將學習如何在 JVM 上動態運行 JavaScript。 通過一些簡短的代碼示例演示最近 Nashorn 的語言特性。 學習 Java 與 JavaScript 的相互調用。***包括如何在日常的 Java 業務中整合動態腳本。
使用Nashorn
Nashorn javascript 引擎要么在java程序中以編程的方式使用要么在命令行工具jjs使用,jjs在目錄$JAVA_HOME/bin
中。如果你準備建立一個jjs的符號鏈接,如下:
- $ cd /usr/bin
- $ ln -s $JAVA_HOME/bin/jjs jjs
- $ jjs
- jjs> print('Hello World');
本教程關注的是在java代碼中使用 nashorn ,所以我們現在跳過jjs。用java代碼來一個簡單的 HelloWorld示例,如下:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); engine.eval("print('Hello World!');");
為了在java中執行JavaScript代碼,首先使用原先Rhino (舊版Java中來自Mozilla的引擎)中的包javax.script來創建一個nashorn腳本引擎。.
既可以向上面那樣把JavaScript代碼作為一個字符串來直接執行,也可放入一個js腳本文件中,如:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); engine.eval(new FileReader("script.js"));
Nashorn javascript是基于 ECMAScript 5.1 ,但nashorn后續版本將支持 ECMAScript 6:
當前Nashorn的策略是遵循ECMAScript規范。 當我們發布JDK 8時,我們將實現ECMAScript 5.1標準。后續的 Nashorn的版本將實現 ECMAScript Edition 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));
- };
為了調用函數,你首先得把腳本引擎轉換為 Invocable。NashornScriptEngine
實現了 Invocable 接口且定義一個調用JavaScript函數的方法 invokeFunction
,傳入函數名即可。
- 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 虛擬機中執行的,我們可以完全利用 nashorn 引擎的 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";
- }
JavaScript 可通過 Java.type API 來引用 Java 類。這跟在 Java 類中引入其他類是類似的。當定義了 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
當調用java 方法時,Nashorn怎樣處理原生JavaScript類型與java類型轉換?讓我們用一個簡單的例子來發現。
下面的java方法簡單打印實際的類方法參數的類型:
- static void fun2(Object object) {
- System.out.println(object.getClass());
- }
為了解引擎如何處理類型轉換,我使用不同JavaScript類型來調用java方法:
- 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
,所以你不應該在客戶端使用這些類:
Anything marked internal will likely change out from underneath you.
ScriptObjectMirror
當使用ScriptObjectMirror
把本地JavaScript對象傳入時,實際上是有一個java對象表示JavaScript 對象。 ScriptObjectMirror 實現了接口與jdk.nashorn.api
內部的映射。這個包下的類目的就是用于客戶端代碼使用。
下一個示例更改參數類型Object為ScriptObjectMirror,因此我們能獲取到傳入JavaScript中對象的一些信息:
- static void fun3(ScriptObjectMirror mirror) {
- System.out.println(mirror.getClassName() + ": " +
- Arrays.toString(mirror.getOwnKeys(true)));
- }
當我們把傳遞對象hash到方法中,在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
能被 ScriptObjectMirror 的callMember()調用。
- static void fun4(ScriptObjectMirror person) {
- System.out.println("Full Name is: " + person.callMember("getFullName"));
- }
當我們傳入一個新的person給java 方法時,我們能在控制臺看到預期結果:
- var person1 = new Person("Peter", "Parker");
- MyJavaClass.fun4(person1);
- // Full Name is: Peter Parker
語言擴展
Nashorn 定義一系列的語言和擴展了 ECMAScript 標準的API。 讓我們直接進入***的功能:
類型數組
原始javascript 數組時無類型的。 Nashorn 運行你在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 int 數組。 但當我們試圖添加非整數的值的數組時,Nashorn 會執行隱式類型轉換。 字符串會自動轉換為int,這相當方便。
集合與For Each
我們可以使用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
為了遍歷集合和數組中的元素,Nashorn 引入了 for each 語句。這就像是 Java 的 for 循環一樣。
這里是一個對集合元素進行遍歷的例子,使用的是 :
- 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 表達式和 Streams
似乎大家都比較喜歡 Lambda 和 Streams —— Nashorn 也是!雖然 ECMAScript 5.1 中缺少 Java 8 Lambda 表達式中的緊縮箭頭的語法,但我們可以在接受 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
我們不需要常規的用 getter 或者 setter 來訪問類成員屬性,可直接用屬性名簡單訪問 Java Bean 中的屬性。例如:
- 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
屬性綁定
來自不同對象的屬性可以綁定在一起:
- function sqr(x) x * x;
- print(sqr(3)); // 9
字符串處理
我喜歡字符串裁剪.
- print(" hehe".trimLeft()); // hehe print("hehe ".trimRight() + "he"); // hehehe
在哪里
以防忘記你在哪里:
- print(__FILE__, __LINE__, __DIR__);
Import 的范圍
有時,這在一次性導入多個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.type
或JavaImporter引入,如 java.util
:
- var list = new java.util.ArrayList();
- list.add("s1"); list.add("s2"); list.add("s3");
如下的代碼演示了將java list轉換為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 中訪問重載的成員會有一點點尷尬,因為 ECMAScript 沒有類似 Java 的 super 關鍵字一樣的東西。所幸的是 Nashorn 有辦法解決。
首先我們在 Java 代碼中定義一個超類:
- class SuperRunner implements Runnable {
- @Override
- public void run() {
- System.out.println("super run");
- }
- }
接下來我們在 JavaScript 中重載 SuperRunner 。創建一個新的 Runner 實例時請注意 Nashorn 的擴展語法:其重載成員的語法是參考 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 ,因此在Nashorn中我們可以重用 Underscore:
- 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');
命令行腳本
如果你對用 Java 編寫命令行腳本很感興趣的話,可以試試 Nake 。Nake 是一個為 Java 8 Nashorn 準備的簡單 Make 工具。你可以在 Nakefile 文件中定義任務,然后使用 nake — myTask 來運行任務。任務使用 JavaScript 編寫并通過 Nashorn 腳本模式運行,因此你可以讓你的終端應用完全利用 Java 8 API 和其他 Java 庫強大的功能。
對 Java 開發者而言,編寫命令行腳本從來沒有如此簡單過。
總結
我希望這篇文章對你有用,可以讓你輕松理解 Nashorn JavaScript 引擎。更多關于 Nashorn 的信息請閱讀 這里, 這里 和 這里. 如果你是要用 Nashorn 編寫 Shell 腳本的話可以參考 這里.
過去我也發表了一些 文章 是關于如何在 Nashron 引擎中使用 Backbone.js 模型數據的。如果你想要了解更多 Java 8 的話可以去看看我的文章 Java 8 Tutorial 和 Java 8 Stream Tutorial.
本文中的示例代碼可以通過 GitHub 獲取,你可以 fork 這個倉庫并通過 Twitter 來給我反饋。
堅持編程!