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
屬性繫結
來自不同物件的屬性可以繫結在一起:
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__);
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 來給我反饋。
堅持程式設計!
相關文章
- java8 新特性 v8 nashorn 引擎(五)Java
- JS引擎(2):Java平臺上JavaScript引擎—Rhino/Nashorn概述JSJavaScript
- 使用Java實現一個JS指令碼引擎JavaJS指令碼
- BeanShell桌面---Java應用程式指令碼引擎 (轉)BeanJava指令碼
- 風控規則引擎(一):Java 動態指令碼Java指令碼
- 使用Java8的Nashorn彌補Node.js密集計算的缺陷JavaNode.js
- C#指令碼引擎RulesEngineC#指令碼
- Java 8 教程Java
- Java 8教程Java
- mysql 轉換表的儲存引擎指令碼MySql儲存引擎指令碼
- java 指令碼Java指令碼
- C#指令碼引擎CS-ScriptC#指令碼
- 基於Groovy的規則指令碼引擎實戰指令碼
- Java 8 CompletableFuture 教程Java
- Java 8簡明教程Java
- Java 8 簡明教程Java
- Unity引擎與C#指令碼簡介UnityC#指令碼
- 從零開始編寫指令碼引擎指令碼
- 《Bash 指令碼教程》釋出了指令碼
- Bash 指令碼 set 命令教程指令碼
- JSR223 Java使用指令碼引擎動態修改業務邏輯JSJava指令碼
- 簡單的 Shell 指令碼入門教程指令碼
- Java位元組碼指令Java
- 寫好shell指令碼的8個建議指令碼
- 從零開始 實現一個自己的指令碼引擎指令碼
- 從零開始實現一個自己的指令碼引擎指令碼
- Velocity指令碼入門教程指令碼
- BAT 批處理指令碼 教程BAT指令碼
- 從0到1編寫一個指令碼引擎指令碼
- 複雜多變場景下的Groovy指令碼引擎實戰指令碼
- java中呼叫groovy指令碼Java指令碼
- 編寫可靠 shell 指令碼的 8 個建議指令碼
- Xcode8後的自動打包指令碼XCode指令碼
- 編寫更好 Bash 指令碼的 8 個建議指令碼
- 【譯】Java8官方教程:Java技術概述Java
- centos789手動無腦用sh指令碼安裝Java8CentOS指令碼Java
- 關係錶轉dooris 的java 指令碼Java指令碼
- JAVA與groovy指令碼的結合使用Java指令碼