本文我们来介绍一下Java 8的Nashorn JavaScript引擎。Nashorn是于Java 8中用于取代Rhino(Java 6,Java 7)的JavaScript引擎。Nashorn完全支持ECMAScript 5.1规范以及一些扩展。与先前的Rhino引擎相比,它有二到十倍的性能提升。本文中将使用各种各样的例子来说明Nashorn的强大功能。

jjs

jjs是个基于Nashorn引擎的命令行工具。你可以通过该工具快速地在Java上运行JavaScript代码,就像是一个REPL。

例如,运行一个hello.js文件:

1
2
$ $JAVA_HOME/bin/jjs hello.js
Hello World

或者,你还可以直接运行代码:

1
2
3
$ $JAVA_HOME/bin/jjs
jjs> print("Hello World")
Hello World

在Java中调用Nashorn引擎

本文专注于在Java中调用Nashorn,所以现在在Java代码中实现简单的HelloWorld:

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

或者,我们还可以从文件中运行JS:

1
2
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
engine.eval(new FileReader("hello.js"));

编译JavaScript代码

你同样可以将脚本编译为Java字节码后调用,这样在多次调用的情况下效率会更高,例如:

1
2
3
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
CompiledScript compiledScript = ((Compilable) engine).complie("print('Hello World!');");
engine.eval();

传递数据到脚本

数据可以通过定义Bindings传递到引擎中:

1
2
3
4
Bindings bindings = engine.createBindings();
bindings.put("name", "Nashorn");

engine.eval("print('Hello ' + name);");

运行该程序将输出Hello Nashorn。

在Java中调用JavaScript函数

Nashorn支持从Java代码中直接调用定义在脚本中的JavaScript函数。你可以将Java对象作为函数参数传递,并且使用函数返回值调用Java方法。

例如在脚本中定义如下代码:

1
2
3
4
5
6
7
8
var fun1 = function(name) {
print('Hello ' + name);
return "Hi!";
}

var fun2 = function (object) {
print(Object.prototype.toString.call(object));
};

为了调用函数,你首先需要将脚本引擎转换为Invocable接口。NashornScriptEngine已经实现了Invocable接口,并且定义了invokeFunction方法来调用指定名称的JavaScript函数。

1
2
3
4
5
6
7
8
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
engine.eval(new FileReader("hello.js"));

Invocable invocable = (Invocable) engine;

Object result = invocable.invokeFunction("fun1", "Nashorn");
System.out.println(result);
System.out.println(result.getClass());

最终将输出如下内容:

1
2
3
Hello Nashorn
Hi!
java.lang.String

Java对象在传入时不会在JavaScript上损失任何类型信息。由于脚本在JVM上运行,我们可以在Nashron上使用Java API或外部库的全部类或方法。

现在让我们传入任意Java对象来调用第二个方法:

1
2
3
4
5
6
7
8
invocable.invokeFunction("fun2", new Date());
// [object java.util.Date]

invocable.invokeFunction("fun2", LocalDateTime.now());
// [object java.time.LocalDateTime]

invocable.invokeFunction("fun2", new Person());
// [object my.package.Person]

调用Java静态方法和字段

在JavaScript中调用Java方法非常容易,就像在Java中所做的一样。首先我们先定义一个Java静态方法:

1
2
3
4
public static String sayHello(String name) {
System.out.println("Hello " + name);
return "Hi!";
}

然后Java类就可以通过Java.typeAPI在JavaScript中引用,就像Java的import一样,例如:

1
2
3
4
var MyJavaClass = Java.type(`my.package.MyJavaClass`);

var result = MyJavaClass.sayHello('Nashorn');
print(result);

最终的结果是:

1
2
Hello Nashorn
Hi!

为了理解在使用JavaScript原生类型调用Java方法时,Nashorn是如何处理类型转换的。我们将通过简单的例子来展示:

下面的方法将打印实际的参数类型:

1
2
3
public static void fun(Object obj) {
System.out.println(obj.getClass());
}

接下来我们用JavaScript来调用该方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
MyJavaClass.fun(127);
// class java.lang.Integer

MyJavaClass.fun(49.99);
// class java.lang.Double

MyJavaClass.fun(true);
// class java.lang.Boolean

MyJavaClass.fun("Hi!");
// class java.lang.String

MyJavaClass.fun(new Number(127));
// class jdk.nashorn.internal.objects.NativeNumber

MyJavaClass.fun(new Date());
// class jdk.nashorn.internal.objects.NativeDate

MyJavaClass.fun(new RegExp());
// class jdk.nashorn.internal.objects.NativeRegExp

MyJavaClass.fun({foo: 'bar'});
// class jdk.nashorn.internal.scripts.JO4

创建Java对象

创建Java对象也如图调用Java方法一样简单,代码如下:

1
2
3
var HashMap = Java.type(`java.util.HashMap`);
var mapDef = new HashMap();
var map100 = new HashMap(100);

访问Java类的补充说明

同样,访问Java类不一定需要Java.type函数,可直接书写类名访问。例如:

1
2
3
4
var result = my.package.MyJavaClass.sayHello('Nashorn');
print(result);

var mapDef = new java.util.HashMap();

同样,为了方便,Nashorn默认提供了对几个Java包的访问,分别是:comedujavajavafxjavaxorg

1
2
3
4
5
6
7
8
9
10
jjs> java.lang
[JavaPackage java.lang]
jjs> typeof java.lang
object
jjs> java.lang.System
[JavaClass java.lang.System]
jjs> typeof java.lang.System
function
jjs> typeof java.lang.System == "function"
true

语言扩展

Nashorn虽然是面向ECMAScript 5.1实现的但它提供了一些扩展,使JavaScript能更好的运用。

类型数组

JavaScript的原生数组是无类型的。Nashron允许你在JavaScript中使用Java的类型数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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执行了一些隐式的转换。字符串会自动转换为整数,这十分便利。

### 用foreach语句迭代数组或集合

我们可以在JavaScript使用`foreach`语句迭代数组或集合:

```js
var list = [1, 2, 3, 4, 5];
var result = '';
for each (var i in list) {
result+=i+'-';
}
print(result);

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
}

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
}
````
函数字面量
在简单的函数声明中,可以省略括号

```js
function increment(in) ++in
````

### 条件捕获语句

可以添加特定的catch字句,这些字句仅在指定条件为真时才执行:

```java
try {
throw "BOOM";
} catch(e if typeof e === 'string') {
print("String thrown: " + e);
} catch(e) {
print("this shouldn't happen!");
}

用Object.setPrototypeOf设置对象原型

Nashorn定义了一个API扩展,它使我们能够更改对象的原型:

1
Object.setPrototypeOf(obj, newProto);

一般认为该函数是对Object.prototype.__proto__的一个更好选择,因为它应该是在所有代码中设置对象原型的首选方法。

Lambda表达式和数据流

每个人都热爱lambda和数据流 — Nashron也一样!虽然ECMAScript 5.1没有Java8 lmabda表达式的简化箭头语法,我们可以在任何接受lambda表达式的地方使用函数字面值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var list = new java.util.ArrayList();
list.add("a1");
list.add("a2");
list.add("a3");
list.add("b1");
list.add("b2");
list.add("b3");
list.add("c1");
list.add("c2");
list.add("c3");


list
.stream()
.filter(function(el) {
return el.startsWith("a");
})
.sorted()
.forEach(function(el) {
print(el);
});
// a1, a2, a3

类的继承

Java类型可以由Java.extend轻易继承。如下所示,你甚至可以在脚本中创建多线程的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

函数重载

方法和函数可以通过点运算符或方括号运算符来调用:

1
2
3
4
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获取或设置值,不需要显式调用读写器:

1
2
3
4
var Date = Java.type('java.util.Date');
var date = new Date();
date.year += 1900;
print(date.year); // 3918

属性绑定

两个不同对象的属性可以绑定到一起:

1
2
3
4
5
6
7
8
var o1 = {};
var o2 = { foo: 'bar'};

Object.bindProperties(o1, o2);

print(o1.foo); // bar
o1.foo = 'rab';
print(o2.foo); // rab

字符串扩展

Nashorn在String原型上提供了两个简单但非常有用的扩展。这就是trimRighttrimLeft函数,它们可返回String得副本并删除空格:

1
2
print("   hello world".trimLeft());
print("hello world ".trimRight());

位置

当前文件名,目录和行可以通过全局变量__FILE____LINE____DIR__获取:

1
print(__FILE__, __LINE__, __DIR__);

导入作用域

有时一次导入多个Java包会很方便。我们可以使用JavaImporter类,和with语句一起使用。所有被导入包的类文件都可以在with语句的局部域中访问到。

1
2
3
4
5
var imports = new JavaImporter(java.io, java.lang);
with (imports) {
var file = new File(__FILE__);
System.out.println(file.getAbsolutePath());
}

数组转换

下面的代码将Java的List转换为JavaScript原生数组:

1
2
3
4
5
6
7
var javaList = new java.util.ArrayList();
javaList.add("1");
javaList.add("2");
javaList.add("3");
var jsArray = Java.from(javaList);
print(jsArray); // 1, 2, 3
print(Object.prototype.toString.call(jsArray)); // [object Array]

下面的代码执行相反操作:

1
var javaArray = Java.to([3, 5, 7, 11], "int[]");

访问超类

在JavaScript中访问被覆盖的成员通常比较困难,因为Java的super关键字在ECMAScript中并不存在。幸运的是,Nashron有一套补救措施。

首先我们需要在Java代码中定义超类:

1
2
3
4
5
6
class SuperRunner implements Runnable {
@Override
public void run() {
System.out.println("super run");
}
}

下面我在JavaScript中覆盖了SuperRunner。要注意创建新的Runner实例时的Nashron语法:覆盖成员的语法取自Java的匿名对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
var SuperRunner = Java.type('my.package.SuperRunner');
var Runner = Java.extend(SuperRunner);

var runner = new Runner() {
run: function() {
Java.super(runner).run();
print('my run');
}
}
runner.run();

// super run
// my run

我们通过Java.super()扩展调用了被覆盖的SuperRunner.run()方法。

神奇的noSuchProperty和noSuchMethod

可以在对象上定义方法,每当访问未定义属性或调用未定义方法时,将调用该方法:

1
2
3
4
5
6
7
8
9
10
11
12
var demo = {
__noSuchProperty__: function (propName) {
print("Accessed non-existing property: " + propName);
},

__noSuchMethod__: function (methodName) {
print("Invoked non-existing method: " + methodName);
}
};

demo.doesNotExist;
demo.callNonExistingMethod()

这将输出:

1
2
Accessed non-existing property: doesNotExist
Invoked non-existing method: callNonExistingMethod

Java.asJSONCompatible 函数

使用该函数,我们可以得到一个与Java JSON库期望兼容的对象。代码如下:

1
2
3
4
5
6
7
Object obj = engine.eval("Java.asJSONCompatible(
{ number: 42, greet: 'hello', primes: [2,3,5,7,11,13] })");
Map<String, Object> map = (Map<String, Object>)obj;

System.out.println(map.get("greet"));
System.out.println(map.get("primes"));
System.out.println(List.class.isAssignableFrom(map.get("primes").getClass()));

这将输出:

1
2
hello
[2, 3, 5, 7, 11, 13]

载入脚本

你可以在脚本引擎中载入其他JavaScript文件:

1
load('classpath:script.js');

或者通过URL载入脚本:

1
2
3
4
5
6
7
8
9
10
load('/script.js');
````
请记住,JavaScript没有命名空间的概念,所以所有的内容都堆放在全局环境中。这使得加载的脚本有可能与你的代码或它们之间的命名冲突。这可以使用`loadWithNewGlobal`函数尽可能减少这种情况的发生:

```js
var math = loadWithNewGlobal('classpath:math_module.js')
math.increment(5);
````
`math_module.js`的文件内容如下:

var math = {
increment: function(num) {
return ++num;
}
};

math;

1
2
3
4
5
6
7
8
9
10
11
12
13
这里我们定义了一个名为math的对象,其中有一个名为increment的函数。使用这个范例我们可以达成基本的模块化。

## ScriptObjectMirror

在向Java传递原生JavaScript对象时,你可以使用`ScriptObjectMirror`类,它实际上是底层JavaScript对象的Java表示。`ScriptObjectMirror`实现了`Map`接口,其位于`jdk.nashorn.api`中。这个包中的类可以用于Java代码。

下面的例子将参数类型从Object改为ScriptObjectMirror,我们可以从传入的JavaScript对象中获得一些信息。

```java
static void fun(ScriptObjectMirror mirror) {
System.out.println(mirror.getClassName() + ": " +
Arrays.toString(mirror.getOwnKeys(true)));
}

当向这个方法传递对象(哈希表)时,在Java中可以访问其属性:

1
2
3
4
5
6
MyJavaClass.fun({
foo: 'bar',
bar: 'foo'
});

// Object: [foo, bar]

我们也可以在Java中调用JavaScript的函数。让我们首先在JavaScript中定义一个Person类型,其含有属性firstNamelastName,以及函数getFullName

1
2
3
4
5
6
7
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
this.getFullName = function() {
return this.firstName + " " + this.lastName;
}
}

JavaScript方法getFullName可以通过callMember()ScriptObjectMirror上调用。

1
2
3
static void fun(ScriptObjectMirror person) {
System.out.println("Full Name is: " + person.callMember("getFullName"));
}

当向Java方法传递新的Person时,我们会在控制台看到预计的输出:

1
2
3
4
var person = new Person("Peter", "Parker");
MyJavaClass.fun(person);

// Full Name is: Peter Parker

限制脚本对特定Java类的访问

jdk.nashorn.api.scripting.ClassFilter接口限制通过Nashorn运行的脚本对特定Java类的访问,为JavaScript代码对Java类的访问提供了细粒度的控制。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import javax.script.ScriptEngine;
import jdk.nashorn.api.scripting.ClassFilter;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;

public class MyClassFilterTest {

class MyCF implements ClassFilter { // 创建类过滤器
@Override
public boolean exposeToScripts(String s) {
if (s.equals("java.io.File")) {
return false;
}
return true;
}
}

public void testClassFilter() {

final String script =
"print(java.lang.System.getProperty(\"java.home\"));" +
"print(\"Create file variable\");" +
"var File = Java.type(\"java.io.File\");";

NashornScriptEngineFactory factory = new NashornScriptEngineFactory();

ScriptEngine engine = factory.getScriptEngine(new MyClassFilterTest.MyCF());

try {
engine.eval(script);
} catch (Exception e) {
System.out.println("Exception caught: " + e.toString());
}
}

public static void main(String[] args) {
MyClassFilterTest myApp = new MyClassFilterTest();
myApp.testClassFilter();
}
}

最终这会抛出java.lang.ClassNotFoundException异常。

命令行脚本

如果你对编写命令行(shell)脚本感兴趣,来试一试Nake吧。Nake是一个Java 8 Nashron的简化构建工具。你只需要在项目特定的Nakefile中定义任务,之后通过在命令行键入nake -- myTask来执行这些任务。任务编写为JavaScript,并且在Nashron的脚本模式下运行,所以你可以使用你的终端、JDK8 API和任意Java库的全部功能。

对Java开发者来说,编写命令行脚本是前所未有的简单…


本文部分内容来自:

https://wizardforcel.gitbooks.io/modern-java/content/ch3.html

https://docs.oracle.com/javase/10/nashorn/nashorn-java-api.htm

https://www.baeldung.com/java-nashorn