目录
最近在弄一些javascript在java环境运行的东西,发现了nashorn,是java8中的一个新的javascript引擎。据说比Rhino快多了,反正挺厉害的。搜了些资料,都是入门级介绍,而且不够全面,感觉Orcale官网的这个介绍还不错,就翻译一下,加深理解。当然,每个译者都有自己的脾气,所以我会去掉一些我不喜欢东西(肯定不是因为我不会翻)。如果想了解所有内容,请看原文.
直到JavaSE 7, JDK里面都有一个Mozilla Rhino的JavaScript引擎。但是,在JavaSE8中,我们换成了一个叫做Oracle Nashorn的东西,他是基于JSR292(Java平台动态语言支持)哒。他更好地支持了ECMA,以及提供了更好更好的性能,反正很叼就是了。
这篇文章会介绍几种使用Orcal Nashorn(译者:下面我都叫Nashorn好了,两个单词打起来好麻烦)的方法。包括通过jjs命令行工具使用独立引擎,以及把nashorn当作Java应用里面内嵌脚本引擎来试用这两种方式。咱们会说到Java和JavaScript之间的互操作,以及在JavaScript脚本中如何继承Java中的类以及实现Java中的接口。(译者:好腻害的样子.)
这些栗子可以在最新JDK8里面运行,当然你也可以用你自己编译的OpenJDK8。
一个使用Nashorn的简单方式就是通过命令行运行JavaScript程序。Oracle的JDK和OpenJDK里面都包含了个命令行工具叫做jjs. 你可以在JDK的bin目录里面找到,就在传说中的 java, javac 和 jar 这些命令行工具的旁边。
jjs 接受几个JavaScript文件作为参数,比如这个hello.js:
var hello = function() {
print("Hello Nashorn!");
};
hello();
在命令行这样运行:
$ jjs hello.js
Hello Nashorn!
$
Nashorn是实现了ECMA的,所以,我们不仅仅可以写helloWorld,还可以运行一些复杂的代码. 看下面代码,他把一个数组里的奇数过滤掉了,然后打印出来所有偶数,而且打印除了所有偶数的和。
var data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var filtered = data.filter(function(i) {
return i % 2 == 0;
});
print(filtered);
var sumOfFiltered = filtered.reduce(function(acc, next) {
return acc + next;
}, 0);
print(sumOfFiltered);
//输出
2,4,6,8,10
30
请注意,Nashorn在运行JavaScript的时候,那些你在浏览器中使用的对象就没有啦,比如:console, window 等等..
你可以用jjs -help ,balabalabala
如果你计划用jjs运行一些JavaScript写的系统脚本的话(就像你用Python, Ruby或者Bash干的那样),你会发现脚本模式有点意思。这个脚本模式主要包括两个语言扩展:heredocs 和 shell invocations。
Heredocs是简单的多行文本, 他的语法在Bash, Perl和Ruby程序员看起来,有点眼熟。一个用<<开始,后面跟着中止记号(我们用的EOF)的文本。 JavaScript表达式可以被嵌入在${…}中,比如下面这段代码:
var data = {
foo: “bar”,
time: new Date()
};
print(<So... foo = ${data.foo} and the current time is ${data.time} EOF);
//运行
$ jjs -scripting heredocs.js
So...
foo = bar
and the current time is
Thu Aug 01 2013 16:21:16 GMT+0200 (CEST)
$
在脚本模式里,双引号和单引号是不同哒。”Hello name"中的 {name}会变成name的真实值,而’Hello ${name}’中的就不会。
Shell invocations允许调用外部程序,上代码:
var lines =
'ls -lsa'.split("\n");
for each (var line in lines) {
print("|> " + line);
}
这个会运行ls -lsa命令。 一个Shell调用把本来要输出到标准控制台的内容作为字符串返回。所以我们就可以把这些内容用换行符分开,然后在每一行前面加上”|>”,这样看起来就像真的一样。如果你想对调用进程进行更多的控制的话,你应该知道,我们还有个 $EXEC 可以用! 这东西可以让你访问标准的输入,输出和错误流。
jjs -scripting dir.js
|> total 72
|> 0 drwxr-xr-x 2 jponge staff 238 Aug 1 16:12 .
|> 0 drwxr-xr-x 5 jponge staff 170 Aug 1 12:15 ..
|> 8 -rw-r--r-- 1 jponge staff 90 Jul 31 23:36 dir.js
|> 8 -rw-r--r-- 1 jponge staff 304 Aug 1 15:56 hello.js
|> 8 -rw-r--r-- 1 jponge staff 143 Aug 1 16:12 heredocs.js
|>
$
脚本模式还有更多的好东西:
可以用#来写注释,也可以在Unix-like系统中用来表示脚本如何执行。exit(code)和quit()方法可以结束当前JVM进程。
看下面这段代码:
#!/usr/bin/env jjs -scripting
print(
"Arguments (${$ARG.length})");
for each (arg in $ARG) {
print("- ${arg}")
}
我们可以像这样运行这个脚本:
$ chmod +x executable.js
$ ./executable.js
Arguments (0)
$ ./executable.js -- hello world !
Arguments (3)
- hello
- world
- !
$
以内嵌方式使用Nashorn的API在javax.script中. 如果Nashorn可用的话,可以通过”nashorn”这个id来访问他的脚本引擎。
这段代码向我们展示了如何在Java应用中通过Nashorn来定义一个求和函数,然后调用他,并显示出结果。
package sample1;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
public class Hello {
public static void main(String... args) throws Throwable {
ScriptEngineManager engineManager =
new ScriptEngineManager();
ScriptEngine engine =
engineManager.getEngineByName("nashorn");
engine.eval("function sum(a, b) { return a + b; }");
System.out.println(engine.eval("sum(1, 2);"));
}
}
engine这个对象是nashorn解释器的单一入口点,他可以被强转成java.script.Invocable, 就像这样:
Invocable invocable = (Invocable) engine;
System.out.println(invocable.invokeFunction("sum", 10, 2));
Invocable这个接口提供了一个方法把代码转换成一个Java接口的引用。假设有这样一个接口:
public interface Adder {
int sum(int a, int b);
}
JavaScript代码中定义了一个有两个参数的sum函数,所以,我们可以这样用:
Adder adder = invocable.getInterface(Adder.class);
System.out.println(adder.sum(2, 3));
这是一种方便的方式来从JavaScript继承Java类,但是这不是唯一的办法。后面我们会提到另外一种方式。
不是每段JavaScript代码都是一个简单的字符串。我们还可以用java.io.Reader,就像这样:
engine.eval(new FileReader("src/sample1/greeter.js"));
System.out.println(invocable.invokeFunction("greet", "Julien"));
想了解更多?你可以去看javax.script里的API呀。你可以在那里发现定义scopes和脚本引擎上的bindings的能力(没太理解)。
现在我们来从JavaScript应用中调用一下真正的JavaScript库。就用一个很出名的HTML视图渲染库mustache.js吧。只要传入一个JSON对象{“name”:”Bean”} 和一个模板”Hello {{name}}”,Mustache就能把他渲染成”Hello Bean”. 当然这个模板引擎还可以干更多的事情,因为他还支持条件,集合遍历等等。
假设我们已经下载了mustache.js. 看下面的例子是如何使用的:
package sample2;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.io.FileReader;
public class Mustache {
public static void main(String... args) throws Throwable {
ScriptEngineManager engineManager =
new ScriptEngineManager();
ScriptEngine engine =
engineManager.getEngineByName("nashorn");
engine.eval(new FileReader("src/sample2/mustache.js"));
Invocable invocable = (Invocable) engine;
String template = "Email addresses of {{contact.name}}:\n" +
"{{#contact.emails}}\n" +
"- {{.}}\n" +
"{{/contact.emails}}";
String contactJson = "{" +
"\"contact\": {" +
"\"name\": \"Mr A\", \"emails\": [" +
"\"contact@some.tld\", \"sales@some.tld\"" +
"]}}";
Object json = engine.eval("JSON");
Object data =
invocable.invokeMethod(json, "parse", contactJson);
Object mustache = engine.eval("Mustache");
System.out.println(invocable.invokeMethod(
mustache, "render", template, data));
}
}
在获取了一个nashorn的脚本引擎后,我们运行mustache.js的代码。首先定义一个字符串作为Mustache的模板。然后还需要需要一个JSON对象作为数据模型,在这里,我们首先要定义一个字符串,然后调用JSON.parse方法把他变成JSON对象。之后就可以调用Mustache.render了。结果是这样:
$ java sample2.Mustache
Email addresses of Mr A:
- contact@some.tld
- sales@some.tld
$
大多数情况下,在nashorn中调用Java API都是很简单的:
print(java.lang.System.currentTimeMillis());
Java objects can be instantiated using the new operator:
var file =
new java.io.File("sample.js");
print(file.getAbsolutePath());
print(file.absolutePath);
在上面这个简单的栗子中,我们可以调用静态方法System.currentTimeMillis(). 不仅如此,还可以用new操作符来实例化Java对象:
var file = new java.io.File("sample.js");
print(file.getAbsolutePath());
print(file.absolutePath);
注意到没有?虽然java.io.File没有absolutePath这个方法,也没有一个叫absolutePath的公有域,但是我们在nashorn中可以把它当成属性用。这是为什么捏?他基本上等于file.getAbsolutePath()。实际上,nashorn会把getXY()和setXY(value)这样的方法当成属性XY对待。
var stack =
new java.util.LinkedList();
[1, 2, 3, 4].forEach(function(item) {
stack.push(item);
});
print(stack);
print(stack.getClass());
如下的输出,证实了我们是在通过JavaScript直接操作Java对象:
[4, 3, 2, 1]
class java.util.LinkedList
我们也可以试试看用Java 8中的 stream API来对一个集合进行排序。虽然在这种情况下,这不是最高效的方式(什么鬼):
var sorted = stack
.stream()
.sorted()
.toArray();
print(sorted);
输出是类似 [Ljava.lang.Object;@473b46c3这样的鬼东西,看起来是一个Java数组对象。因为Java数组和JavaScript数组是不一样的。在内部,Nashorn用一个实现了java.util.Map接口的自定义类来提供了JavaScript数组。可以用Nashorn提供的Java对象上的to和from方法来进行转换。
var jsArray = Java.from(sorted);
print(jsArray);
var javaArray =
Java.to(jsArray);
print(javaArray);
输出是这个样子的:
1,2,3,4
[Ljava.lang.Object;@23a5fd2
默认情况下,使用Java类型的时候,要用全名(比如这样:java.lang.String, java.util.LinkedHashSet)。Nashron没有默认导入java包,因为String和Object会跟JavaScript里的String和Object发生冲突。所以,Java中的String是java.lang.String,不是String。
Nashorn出现之前,JDK发行版中的JavaScript引擎是Mozilla Rhino。
Rhino提供了一个 load(path) 方法来加载第三方JavaScript文件。这个在Nashorn里还可以继续用。 你可以用他来加载一个特定的兼容模块。这个模块提供了importClass来import class,也提供了importPackage来导入包(就像在java里用通配符导入包一样)。
load("nashorn:mozilla_compat.js");
importClass(java.util.HashSet);
var set = new HashSet();
importPackage(java.util);
var list = new ArrayList();
注意:这些方法会把符号引用导入到JavaScript的全局scope中。虽然由于兼用性的原因,mozilla_compat.js和importClass还是支持的,但是我们并不鼓励这么干。 我们更推荐使用作为Rhino遗产被继承下来的JavaImporter:
var CollectionsAndFiles = new JavaImporter(
java.util,
java.io,
java.nio);
with (CollectionsAndFiles) {
var files = new LinkedHashSet();
files.add(new File("Plop"));
files.add(new File("Foo"));
files.add(new File("w00t.js"));
}
JavaImporter接受几个Java包作为参数,返回对象可以被用于with语句。在使用了这个返回对象的with作用域中,你可以当作已经导入了这些包。 正式因为这对JavaScope的全局scope没有影响,所以JavaImporter比importClass和importPackage更好。
Java允许方法重载,就是说,在一个类里面,可以有多个名字相同的方法,只要它们的方法签名不同就行(比如说参数类型或个数不一样啦)。java.io.PrintStream就是个很好的栗子,他有很多个print和println方法,参数类型分别是Object, String, 数组 和 原始类型。
在执行调用时,Nashorn会自动选择最合适的方法。意思就是说你在调用JavaAPI的时候可以不必为被重载过的方法担心。但是,如果你需要的话,仍然有方式可以直接指定使用哪一个方法。 其实就是在调用一个被重载过的方法,而你的参数又有歧义时,需要指明具体想调用的方法的参数类型而已。
在下面这段代码中,第一次调用println方法会使用println(String),第二次调用使用JavaScript对象属性的方式访问了println(Object)。 字符串”println(Object)”提供了一个签名帮助Nashorn确定使用哪一个方法。
这里有个例外,java包中的类可以不必写全名。因此我们才可以使用println(Object)来替代更长的println(java.lang.Object)。
var stdout =
java.lang.System.out;
stdout.println("Hello");
stdout["println(Object)"](
"Hello");
Java.type函数可以用来获取明确的Java类型的引用。他不仅可以获取对象,还可以获取原始类型和数组。
var LinkedList = Java.type("java.util.LinkedList");
var primitiveInt = Java.type("int");
var arrayOfInts = Java.type("int[]");
返回的对象是Java类型映射到Nashorn中的表示。需要注意的是他们跟java.lang.Class并不相同。类型对象可以被当做构造函数来用,也支持instanceof运算。看下面这个栗子:
var list = new LinkedList;
list.add(1);
list.add(2);
print(list);
print(list instanceof LinkedList);
var a = new arrayOfInts(3);
print(a.length);
print(a instanceof arrayOfInts);
可以在类型对象和Java类引用之间进行转换。类型对象有个class属性就是他的java.lang.Class。同样的,static属性可以用来获取正确的类型对象。
print(LinkedList.class);
print(list.getClass().static);
print(LinkedList.class === list.getClass());
print(list.getClass().static === LinkedList);
输出如下:
class java.util.LinkedList
[JavaClass java.util.LinkedList]
true
true
Nashorn提供了一个简单的机制来从JavaScript代码中继承Java类型。这样可以提供接口的实现或者抽象类的实现子类。
给定一个Java接口,提供他的实现的一个简单方式就是实例化这个接口。 然后给他的构造函数传递一个通过属性的方式实现了所有方法的JavaScript对象。
下面的代码提供了java.util.Iterator的实现类。实现了next方法和hasNext方法(remove方法在Java8中已经通过默认方法的方式提供了)。
var iterator = new java.util.Iterator({
i: 0,
hasNext: function() {
return this.i < 10;
},
next: function() {
return this.i++;
}
});
print(iterator instanceof Java.type("java.util.Iterator"));
while (iterator.hasNext()) {
print("-> " + iterator.next());
}
我们来运行一下,看看结果:
true
-> 0
-> 1
-> 2
-> 3
-> 4
-> 5
-> 6
-> 7
-> 8
-> 9
当接口中只有一个方法的时候,可以直接传入一个function。就像这样:
var list = java.util.Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
var odd = list.stream().filter(function(i) {
return i % 2 == 0;
});
odd.forEach(function(i) {
print(">>> " + i);
});
运行结果如下:
>>> 2
>>> 4
>>> 6
>>> 8
Nashorn也提供了一个语言扩展,支持简单语法来表达小的lambda函数。这个对于所有来自Java的只有一个抽象方法的类型都管用。所以,我们可以把这段代码:
var odd = list.stream().filter(
function(i) {
return i % 2 == 0;
});
写成这样:
var odd = list.stream().filter(function(i) i % 2 == 0);
这个语言扩展,在处理Java SE 8 中支持lambda表达式的API时,非常有用。因为JavaScript函数可以在所有需要Java lambda的地方使用。这种简单写法是被JavaScript 1.8引擎支持的。
实现抽象类的方式跟实现接口差不多。
为了继承一个类,你需要使用Java.extend函数。第一个参数是你想继承的类的类型对象。如果这个参数是个接口的话,会认为基类是java.lang.Object。可以传多个参数来表示实现多个接口。
看看下面的代码。Java.extend函数返回一个类型对象,我们就叫姑且他继承者吧。在我们的栗子中,这个继承者继承了java.lang.Object,而且实现了两个接口:java.lang .Comparable和java.o.Serializable。具体实现内容通过一个JavaScript对象传递给构造函数。
var ObjectType = Java.type("java.lang.Object");
var Comparable = Java.type("java.lang.Comparable");
var Serializable = Java.type("java.io.Serializable");
var MyExtender = Java.extend(
ObjectType, Comparable, Serializable);
var instance = new MyExtender({
someInt: 0,
compareTo: function(other) {
var value = other["someInt"];
if (value === undefined) {
return 1;
}
if (this.someInt < value) {
return -1;
} else if (this.someInt == value) {
return 0;
} else {
return 1;
}
}
});
print(instance instanceof Comparable);
print(instance instanceof Serializable);
print(instance.compareTo({ someInt: 10 }));
print(instance.compareTo({ someInt: 0 }));
print(instance.compareTo({ someInt: -10 }));
运行结果:
true
true
-1
0
1
相同的继承者类型的所有实例化对象共享同一个类。虽然他们的具体实现不相同。
var anotherInstance = new MyExtender({
compareTo: function(other) {
return -1;
}
});
// Prints 'true'!
print(instance.getClass() === anotherInstance.getClass());
虽然这种用法还不错,但是每次要实例化对象的时候,都要传递一个实现,难免显得麻烦。确实,在有些情况下,对象需要通过控制反转机制来实例化,比如一些依赖注入的API。在这些情况下,第三方API通常需要一个实现类的引用,这就让我们前面用的那种继承者机制显得有些不太合适了。
幸运的是,Java.extend不仅仅支持了每次实例化时传递实现的方式,也允许将实现绑定到一个类定义上。你只是需要为最后一个参数传递一个作为实现的JavaScript对象。
var Callable = Java.type("java.util.concurrent.Callable");
var FooCallable = Java.extend(Callable, {
call: function() {
return "Foo";
}
});
var BarCallable = Java.extend(Callable, {
call: function() {
return "Bar";
}
});
var foo = new FooCallable();
var bar = new BarCallable();
// 'false'
print(foo.getClass() === bar.getClass());
print(foo.call());
print(bar.call());
虽然没有通过这个栗子说明,通过类绑定实现定义的类可以提供一个继承了他的基类的构造方法。在这个栗子中,我们的对象含蓄的继承了java.lang.Object,并且实现了java.util.concurrent.Callable接口。因此,这个类定义有一个无参构造方法。
最后但并非最不重要的,实例绑定和类绑定可以一起用。
你可以给类绑定实现的类的构造函数传递一个实现来改进他的部分或所有方法。
var foobar = new FooCallable({
call: function() {
return “FooBar”;
}
});
// ‘FooBar’
print(foobar.call());
// ‘true’
print(foo.getClass() === foobar.getClass());
这片文章涵盖了把Nashorn当做命令行工具或者Java应用内嵌解释器的各种场景。同时包含了Java和JavaScript的互操作,包含了在JavaScript中实现Java接口或者继承Java类。
在运行在JVM上的多语言应用中,Nashorn是一种获得脚本语言的优势的极好方式。JavaScript是一门非常出名的语言,Java和JavaScript之间的无缝而简单的交互,给我们带来了很多的想象空间。