12.4 Nashorn的高级用法

Nashorn 是个复杂的编程环境,也是个适合部署应用的稳健平台,而且能和 Java 相互操作。这一节我们介绍一些高级用法,把 Java 代码集成在 JavaScript 代码中,还会深入 Nashorn 的一些实现细节,说明实现这种集成的方式。

12.4.1 在Nashorn中调用Java代码

我们知道,每个 JavaScript 对象都会编译成某个 Java 类的实例,那么,你或许就不会奇怪,在 Nashorn 中能无缝集成 Java 代码——尽管二者的类型系统和语言特性有重要区别。不过,为了实现这种集成,还需要实现一些机制。

我们已经知道,在 Nashorn 中可以直接访问 Java 类和方法,例如:

  1. $ jjs -Dkey=value
  2. jjs> print(java.lang.System.getProperty("key"));
  3. value

下面仔细分析一下这个句法,看看 Nashorn 是如何实现这种功能的。

1. JavaClassJavaPackage类型

在 Java 中,表达式 java.lang.System.getProperty("key") 的意思是通过完全限定的名称调用 java.lang.System 类中的 getProperty() 静态方法。不过,在 JavaScript 的句法中,这个表达式的意思是,从符号 java 开始,链式访问属性。下面在 jjs shell 中看一下这些符号的表现:

  1. jjs> print(java);
  2. [JavaPackage java]
  3. jjs> print(java.lang.System);
  4. [JavaClass java.lang.System]

可以看出,java 是个特殊的 Nashorn 对象,用于访问 Java 系统中的包。在 JavaScript 中,Java 包使用 JavaPackage 类型表示,而 Java 类使用 JavaClass 类型表示。任何顶层包都能直接作为包导航对象,子包则可以赋值给 JavaScript 对象。因此,可以使用简短的句法访问 Java 类:

  1. jjs> var juc = java.util.concurrent;
  2. jjs> var chm = new juc.ConcurrentHashMap;

除了可以使用包对象导航之外,还可以使用另一个对象 Java。在这个对象上可以调用一些有用的方法,其中一个最重要的方法是 Java.type()。使用这个方法可以查询 Java 的类型系统,访问 Java 类。例如:

  1. jjs> var clz = Java.type("java.lang.System");
  2. jjs> print(clz);
  3. [JavaClass java.lang.System]

如果在类路径(例如,使用 jjs-cp 选项指定)中找不到指定的类,会抛出 ClassNotFoundException 异常(jjs 会把这个异常包装在一个 Java RuntimeException 异常对象中):

  1. jjs> var klz = Java.type("Java.lang.Zystem");
  2. java.lang.RuntimeException: java.lang.ClassNotFoundException:
  3. Java.lang.Zystem

多数情况下,JavaScript 中的 JavaClass 对象都可以像 Java 的类对象一样使用(这两个类型稍微有所不同,不过可以把 JavaClass 理解为类对象在 Nashorn 中的镜像)。例如,在 Nashorn 中可以直接使用 JavaClass 创建 Java 新对象:

  1. jjs> var clz = Java.type("java.lang.Object");
  2. jjs> var obj = new clz;
  3. jjs> print(obj);
  4. java.lang.Object@73d4cc9e
  5. jjs> print(obj.hashCode());
  6. 1943325854
  7. // 注意,这种句法不起作用
  8. jjs> var obj = clz.new;
  9. jjs> print(obj);
  10. undefined

不过,使用时要稍微小心一些。jjs 环境会自动打印表达式的结果,这可能会导致一些意料之外的行为:

  1. jjs> var clz = Java.type("java.lang.System");
  2. jjs> clz.out.println("Baz!");
  3. Baz!
  4. null

这里的问题是,java.lang.System.out.println() 方法有返回值,类型为 void。而在 jjs 中,如果表达式没赋值给变量,就会得到一个值,并打印出来。所以,println() 方法的返回值会映射到 JavaScript 的 null 值上,并打印出来。

12.4 Nashorn的高级用法 - 图1 不熟悉 JavaScript 的 Java 程序员要注意,在 JavaScript 中处理 null 和缺失值很麻烦,尤其是 null != undefined

2. JavaScript函数和Java lambda表达式

JavaScript 和 Java 之间的相互操作层级非常深,甚至可以使用 JavaScript 函数作为 Java 接口的匿名实现(或者作为 lambda 表达式)。下面举个例子,使用 JavaScript 函数作为 Callable 接口的实例(表示后续调用的代码块)。Callable 接口中只有一个方法,call(),这个方法没有参数,返回值是 void。在 Nashorn 中,我们可以使用 JavaScript 函数作为 lambda 表达式:

  1. jjs> var clz = Java.type("java.util.concurrent.Callable");
  2. jjs> print(clz);
  3. [JavaClass java.util.concurrent.Callable]
  4. jjs> var obj = new clz(function () { print("Foo"); } );
  5. jjs> obj.call();
  6. Foo

这个示例要表明的基本事实是,在 Nashorn 中,JavaScript 函数和 Java lambda 表达式之间没有区别。和在 Java 中一样,函数会被自动转换成相应类型的对象。下面看一下如何在 Java 线程池中使用 Java 的 ExecutorService 对象执行一些 JavaScript 代码:

  1. jjs> var juc = java.util.concurrent;
  2. jjs> var exc = juc.Executors.newSingleThreadExecutor();
  3. jjs> var clbl = new juc.Callable(function (){
  4. \java.lang.Thread.sleep(10000); return 1; });
  5. jjs> var fut = exc.submit(clbl);
  6. jjs> fut.isDone();
  7. false
  8. jjs> fut.isDone();
  9. true

与等效的 Java 代码相比(就算使用 Java 8 引入的 lambda 表达式),样板代码的减少量十分惊人。不过,lambda 表达式的实现方式导致了一些限制。例如:

  1. jjs> var fut=exc.submit(function (){\
  2. java.lang.Thread.sleep(10000); return 1;});
  3. java.lang.RuntimeException: java.lang.NoSuchMethodException: Can't
  4. unambiguously select between fixed arity signatures
  5. [(java.lang.Runnable), (java.util.concurrent.Callable)] of the method
  6. java.util.concurrent.Executors.FinalizableDelegatedExecutorService
  7. .submit for argument types
  8. [jdk.nashorn.internal.objects.ScriptFunctionImpl]

这里的问题是,线程池中有一个重载的 submit() 方法。一个版本的参数是一个 Callable 对象,而另一个版本的参数是一个 Runnable 对象。可是,JavaScript 函数(作为 lambda 表达式时)既能转换成 Callable 对象,也能转换成 Runnable 对象。这就是错误消息中出现“unambiguously select”(明确选择)的原因。运行时能选择其中任何一个,但不能在二者之间作出抉择。

12.4.2 Nashorn对JavaScript语言所做的扩展

前面说过,Nashorn 是完全遵守 ECMAScript 5.1 标准(这是 JavaScript 的标准)的实现。不过,除此之外,Nashorn 还实现了一些 JavaScript 语言句法扩展,让开发者的生活更轻松。经常使用 JavaScript 的开发者应该会熟悉这些扩展,有相当一部分扩展实现的都是 Mozilla JavaScript 方言中的功能。下面介绍几个最常使用和最有用的扩展。

1. 遍历循环

标准的 JavaScript 没有提供等同于 Java 遍历循环的句法,不过 Nashorn 实现了 Mozilla 使用的 for each in 循环,如下所示:

  1. var jsEngs = [ "Nashorn", "Rhino", "V8", "IonMonkey", "Nitro" ];
  2. for each (js in jsEngs) {
  3. print(js);
  4. }

2. 单表达式函数

Nashorn 还支持另一个小小的句法增强,目的是让只由一个表达式组成的函数更易于阅读。如果函数(具名或匿名)只有一个表达式,那么可以省略花括号和返回语句。在下述示例中,cube()cube2() 这两个函数完全等效,不过 cube() 函数使用的句法在普通的 JavaScript 中不合法:

  1. function cube(x) x*x*x;
  2. function cube2(x) {
  3. return x*x*x;
  4. }
  5. print(cube(3));
  6. print(cube2(3));

3. 多个catch子句

JavaScript 支持 trycatchthrow 语句,而且处理方式和 Java 类似。

12.4 Nashorn的高级用法 - 图2 JavaScript 不支持已检异常,所有异常都是未检异常。

可是,标准的 JavaScript 只允许在 try 块后跟一个 catch 子句,也就是说,不支持使用不同的 catch 子句处理不同的异常类型。幸好,Mozilla 已经实现了支持多个 catch 子句的句法扩展,而且 Nashorn 也实现了,如下述示例所示:

  1. function fnThatMightThrow() {
  2. if (Math.random() < 0.5) {
  3. throw new TypeError();
  4. } else {
  5. throw new Error();
  6. }
  7. }
  8. try {
  9. fnThatMightThrow();
  10. } catch (e if e instanceof TypeError) {
  11. print("Caught TypeError");
  12. } catch (e) {
  13. print("Caught some other error");
  14. }

Nashorn 还实现了一些其他非标准的句法扩展(前面介绍 jjs 的脚本模式时见过一些其他有用的句法革新),不过前面介绍的这几个扩展最为人熟知,而且使用广泛。

12.4.3 实现细节

前面说过,Nashorn 的工作方式是直接把 JavaScript 程序编译成 JVM 字节码,然后像任何其他类一样运行。正是因为这样,才能把 JavaScript 函数当作 lambda 表达式,并在二者之间相互操作。

下面仔细分析前面的一个示例,说明 JavaScript 函数为何能当作 Java 接口的匿名实现:

  1. jjs> var clz = Java.type("java.util.concurrent.Callable");
  2. jjs> var obj = new clz(function () { print("Foo"); } );
  3. jjs> print(obj);
  4. jdk.nashorn.javaadapters.java.util.concurrent.Callable@290dbf45

可以看出,实现 Callable 接口的 JavaScript 对象其实属于 jdk.nashorn.javaadapters.java.util.concurrent.Callable 类。当然,Nashorn 没有提供这个类。Nashorn 会动态生成字节码,实现所需的任何接口,并且为了可读性,会在包结构中保留接口原来的名称。

12.4 Nashorn的高级用法 - 图3 记住,动态生成代码是 Nashorn 的基本特性,Nashorn 会把所有 JavaScript 代码编译成 Java 字节码,绝不会直接解释。

最后还有一点要注意,因为 Nashorn 坚持 100% 符合规范,所以有时会限制实现的功能。例如,像下面这样打印一个对象:

  1. jjs> var obj = {foo:"bar",cat:2};
  2. jjs> print(obj);
  3. [object Object]

根据 ECMAScript 规范,打印出的内容只能是 [object Object]——符合规范的实现不能提供更具体的有用信息(例如 obj 对象的完整属性列表和其中包含的值)。