本文我们来介绍一下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 ()); invocable.invokeFunction("fun2" , LocalDateTime.now()); invocable.invokeFunction("fun2" , new 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);
最终的结果是:
为了理解在使用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 ); MyJavaClass.fun(49.99 ); MyJavaClass.fun(true ); MyJavaClass.fun("Hi!" ); MyJavaClass.fun(new Number (127 )); MyJavaClass.fun(new Date ()); MyJavaClass.fun(new RegExp ()); MyJavaClass.fun({foo: 'bar' });
创建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包的访问,分别是:com
、edu
、java
、javafx
、javax
和org
。
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[0 ] = "17" ; print (array[0 ]); array[0 ] = "wrong type" ; print (array[0 ]); array[0 ] = "17.3" ; print (array[0 ]); `` `` 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 } ` `` ` 函数字面量 在简单的函数声明中,可以省略括号 ` `` jsfunction 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); });
类的继承
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();
函数重载
方法和函数可以通过点运算符或方括号运算符来调用:
1 2 3 4 var System = Java.type('java.lang.System' );System.out.println(10 ); System.out["println" ](11.0 ); System.out["println(double)" ](12 );
当使用重载参数调用方法时,传递可选参数类型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);
属性绑定
两个不同对象的属性可以绑定到一起:
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原型上提供了两个简单但非常有用的扩展。这就是trimRight
和trimLeft
函数,它们可返回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); print (Object .prototype .toString .call (jsArray));
下面的代码执行相反操作:
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();
我们通过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' });
我们也可以在Java中调用JavaScript的函数。让我们首先在JavaScript中定义一个Person
类型,其含有属性firstName
和lastName
,以及函数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);
限制脚本对特定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