本文我们来介绍一下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 | $JAVA_HOME/bin/jjs hello.js |
或者,你还可以直接运行代码:
1 | $JAVA_HOME/bin/jjs |
在Java中调用Nashorn引擎
本文专注于在Java中调用Nashorn,所以现在在Java代码中实现简单的HelloWorld:
1 | ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); |
或者,我们还可以从文件中运行JS:
1 | ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); |
编译JavaScript代码
你同样可以将脚本编译为Java字节码后调用,这样在多次调用的情况下效率会更高,例如:
1 | ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); |
传递数据到脚本
数据可以通过定义Bindings
传递到引擎中:
1 | Bindings bindings = engine.createBindings(); |
运行该程序将输出Hello Nashorn
。
在Java中调用JavaScript函数
Nashorn支持从Java代码中直接调用定义在脚本中的JavaScript函数。你可以将Java对象作为函数参数传递,并且使用函数返回值调用Java方法。
例如在脚本中定义如下代码:
1 | var fun1 = function(name) { |
为了调用函数,你首先需要将脚本引擎转换为Invocable
接口。NashornScriptEngine
已经实现了Invocable
接口,并且定义了invokeFunction
方法来调用指定名称的JavaScript函数。
1 | ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); |
最终将输出如下内容:
1 | Hello Nashorn |
Java对象在传入时不会在JavaScript上损失任何类型信息。由于脚本在JVM上运行,我们可以在Nashron上使用Java API或外部库的全部类或方法。
现在让我们传入任意Java对象来调用第二个方法:
1 | invocable.invokeFunction("fun2", new Date()); |
调用Java静态方法和字段
在JavaScript中调用Java方法非常容易,就像在Java中所做的一样。首先我们先定义一个Java静态方法:
1 | public static String sayHello(String name) { |
然后Java类就可以通过Java.type
API在JavaScript中引用,就像Java的import
一样,例如:
1 | var MyJavaClass = Java.type(`my.package.MyJavaClass`); |
最终的结果是:
1 | Hello Nashorn |
为了理解在使用JavaScript原生类型调用Java方法时,Nashorn是如何处理类型转换的。我们将通过简单的例子来展示:
下面的方法将打印实际的参数类型:
1 | public static void fun(Object obj) { |
接下来我们用JavaScript来调用该方法:
1 | MyJavaClass.fun(127); |
创建Java对象
创建Java对象也如图调用Java方法一样简单,代码如下:
1 | var HashMap = Java.type(`java.util.HashMap`); |
访问Java类的补充说明
同样,访问Java类不一定需要Java.type
函数,可直接书写类名访问。例如:
1 | var result = my.package.MyJavaClass.sayHello('Nashorn'); |
同样,为了方便,Nashorn默认提供了对几个Java包的访问,分别是:com
、edu
、java
、javafx
、javax
和org
。
1 | jjs> java.lang |
语言扩展
Nashorn虽然是面向ECMAScript 5.1实现的但它提供了一些扩展,使JavaScript能更好的运用。
类型数组
JavaScript的原生数组是无类型的。Nashron允许你在JavaScript中使用Java的类型数组:
1 | var IntArray = Java.type("int[]"); |
int[]
数组就像真实的Java整数数组那样。但在试图向数组添加非整数时,Nashron执行了一些隐式的转换。字符串会自动转换为整数,这十分便利。
用foreach语句迭代数组或集合
我们可以在JavaScript使用foreach语句迭代数组或集合:
1 | var list = [1, 2, 3, 4, 5]; |
函数字面量
在简单的函数声明中,可以省略括号
1 | function increment(in) ++in |
条件捕获语句
可以添加特定的catch字句,这些字句仅在指定条件为真时才执行:
1 | try { |
用Object.setPrototypeOf设置对象原型
Nashorn定义了一个API扩展,它使我们能够更改对象的原型:
1 | Object.setPrototypeOf(obj, newProto); |
一般认为该函数是对Object.prototype.__proto__
的一个更好选择,因为它应该是在所有代码中设置对象原型的首选方法。
Lambda表达式和数据流
每个人都热爱lambda和数据流 — Nashron也一样!虽然ECMAScript 5.1没有Java8 lmabda表达式的简化箭头语法,我们可以在任何接受lambda表达式的地方使用函数字面值。
1 | var list = new java.util.ArrayList(); |
类的继承
Java类型可以由Java.extend
轻易继承。如下所示,你甚至可以在脚本中创建多线程的代码:
1 | var Runnable = Java.type('java.lang.Runnable'); |
函数重载
方法和函数可以通过点运算符或方括号运算符来调用:
1 | var System = Java.type('java.lang.System'); |
当使用重载参数调用方法时,传递可选参数类型println(double)
会指定所调用的具体方法。
Java Beans
你可以简单地使用属性名称来向Java Beans获取或设置值,不需要显式调用读写器:
1 | var Date = Java.type('java.util.Date'); |
属性绑定
两个不同对象的属性可以绑定到一起:
1 | var o1 = {}; |
字符串扩展
Nashorn在String原型上提供了两个简单但非常有用的扩展。这就是trimRight
和trimLeft
函数,它们可返回String得副本并删除空格:
1 | print(" hello world".trimLeft()); |
位置
当前文件名,目录和行可以通过全局变量__FILE__
、__LINE__
和__DIR__
获取:
1 | print(__FILE__, __LINE__, __DIR__); |
导入作用域
有时一次导入多个Java包会很方便。我们可以使用JavaImporter
类,和with
语句一起使用。所有被导入包的类文件都可以在with
语句的局部域中访问到。
1 | var imports = new JavaImporter(java.io, java.lang); |
数组转换
下面的代码将Java的List
转换为JavaScript原生数组:
1 | var javaList = new java.util.ArrayList(); |
下面的代码执行相反操作:
1 | var javaArray = Java.to([3, 5, 7, 11], "int[]"); |
访问超类
在JavaScript中访问被覆盖的成员通常比较困难,因为Java的super关键字在ECMAScript中并不存在。幸运的是,Nashron有一套补救措施。
首先我们需要在Java代码中定义超类:
1 | class SuperRunner implements Runnable { |
下面我在JavaScript中覆盖了SuperRunner。要注意创建新的Runner实例时的Nashron语法:覆盖成员的语法取自Java的匿名对象。
1 | var SuperRunner = Java.type('my.package.SuperRunner'); |
我们通过Java.super()扩展调用了被覆盖的SuperRunner.run()方法。
神奇的noSuchProperty和noSuchMethod
可以在对象上定义方法,每当访问未定义属性或调用未定义方法时,将调用该方法:
1 | var demo = { |
这将输出:
1 | Accessed non-existing property: doesNotExist |
Java.asJSONCompatible 函数
使用该函数,我们可以得到一个与Java JSON库期望兼容的对象。代码如下:
1 | Object obj = engine.eval("Java.asJSONCompatible( |
这将输出:
1 | hello |
载入脚本
你可以在脚本引擎中载入其他JavaScript文件:
1 | load('classpath:script.js'); |
或者通过URL载入脚本:
1 | load('/script.js'); |
请记住,JavaScript没有命名空间的概念,所以所有的内容都堆放在全局环境中。这使得加载的脚本有可能与你的代码或它们之间的命名冲突。这可以使用loadWithNewGlobal
函数尽可能减少这种情况的发生:
1 | var math = loadWithNewGlobal('classpath:math_module.js') |
math_module.js
的文件内容如下:
1 | var math = { |
这里我们定义了一个名为math
的对象,其中有一个名为increment
的函数。使用这个范例我们可以达成基本的模块化。
ScriptObjectMirror
在向Java传递原生JavaScript对象时,你可以使用ScriptObjectMirror
类,它实际上是底层JavaScript对象的Java表示。ScriptObjectMirror
实现了Map
接口,其位于jdk.nashorn.api
中。这个包中的类可以用于Java代码。
下面的例子将参数类型从Object
改为ScriptObjectMirror
,我们可以从传入的JavaScript对象中获得一些信息。
1 | static void fun(ScriptObjectMirror mirror) { |
当向这个方法传递对象(哈希表)时,在Java中可以访问其属性:
1 | MyJavaClass.fun({ |
我们也可以在Java中调用JavaScript的函数。让我们首先在JavaScript中定义一个Person
类型,其含有属性firstName
和lastName
,以及函数getFullName
。
1 | function Person(firstName, lastName) { |
JavaScript方法getFullName
可以通过callMember()
在ScriptObjectMirror
上调用。
1 | static void fun(ScriptObjectMirror person) { |
当向Java方法传递新的Person
时,我们会在控制台看到预计的输出:
1 | var person = new Person("Peter", "Parker"); |
限制脚本对特定Java类的访问
jdk.nashorn.api.scripting.ClassFilter
接口限制通过Nashorn运行的脚本对特定Java类的访问,为JavaScript代码对Java类的访问提供了细粒度的控制。示例代码如下:
1 | import javax.script.ScriptEngine; |
最终这会抛出
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