Nashorn——在 JDK 8 中融合 Java 与 JavaScript 之力

阅读数:38318 2014 年 12 月 29 日 05:54

从 JDK 6 开始,Java 就已经捆绑了JavaScript 引擎,该引擎基于 Mozilla 的 Rhino 。该特性允许开发人员将 JavaScript 代码嵌入到 Java 中,甚至从嵌入的 JavaScript 中调用 Java。此外,它还提供了使用 jrunscript 从命令行运行 JavaScript 的能力。如果不需要非常好的性能,并且可以接受 ECMAScript 3 有限的功能集的话,那它相当不错了。

从 JDK 8 开始, Nashorn 取代 Rhino 成为 Java 的嵌入式 JavaScript 引擎。Nashorn 完全支持 ECMAScript 5.1 规范以及一些扩展。它使用基于 JSR 292 的新语言特性,其中包含在 JDK 7 中引入的 invokedynamic,将 JavaScript 编译成 Java 字节码。

与先前的 Rhino 实现相比,这带来了 2 到 10 倍的性能提升,虽然它仍然比Chrome 和Node.js 中的V8 引擎要差一些。如果你对实现细节感兴趣,那么可以看看这些来自 2013 JVM 语言峰会的幻灯片。

由于 Nashorn 随 JDK 8 而来,它还增加了简洁的函数式接口支持。接下来,我们很快就会看到更多细节。

让我们从一个小例子开始。首先,你可能需要安装 JDK 8 和 NetBeans、IntelliJ IDEA 或者 Eclipse。对于集成 JavaScript 开发,它们都至少提供了基本的支持。让我们创建一个简单的 Java 项目,其中包含下面两个示例文件,并运行它:

(点击图片可以查看大图)

在第 12 行,我们使用引擎的“eval”方法对任意 JavaScript 代码求值。在本示例中,我们只是加载了上面的 JavaScript 文件并对其求值。你可能会发现那个“print”并不熟悉。它不是 JavaScript 的内建函数,而是Nashorn 提供的,它还提供了其它方便的、在脚本环境中大有用武之地的函数。你也可以将 “hello world”的打印代码直接嵌入到传递给“eval”方法的字符串,但将JavaScript 放在它自己的文件中为其开启了全新的工具世界。

Eclipse 目前还没有对 Nashorn 提供专门的支持,不过,通过 JavaScript 开发工具(JSDT)项目,它已经支持 JavaScript 的基本工具和编辑。

(点击图片可以查看大图)

IntelliJ IDEA 13.1(社区版和旗舰版)提供了出色的 JavaScript 和 Nashorn 支持。它有一个全功能的调试器,甚至允许在 Java 和 JavaScript 之间保持重构同步,因此举例来说,如果你重命名一个被 JavaScript 引用的 Java 类,或者重命名一个用于 Java 源代码中的 JavaScript 文件,那么该 IDE 将跨语言修改相应的引用。

下面是一个例子,展示如何调试从 Java 调用的 JavaScript(请注意,NetBeans 也提供了 JavaScript 调试器,如下截图所示):

(点击图片可以查看大图)

你可能会说,工具看上去不错,而且新实现修复了性能以及一致性问题,但我为什么应该用它呢?一个原因是一般的脚本编写。有时候,能够直接插入任何类型的字符串,并任由它被解释,会很方便。有时候,没有碍事的编译器,或者不用为静态类型担心,可能也是不错的。或者,你可能对 Node.js 编程模型感兴趣,它也可以和 Java 一起使用,在本文的末尾我们会看到。另外,还有个情况不得不提一下,与Java 相比,使用JavaScript 进行JavaFX 开发会快很多。

Shell 脚本

Nashorn 引擎可以使用 jjs 命令从命令行调用。你可以不带任何参数调用它,这会将你带入一个交互模式,或者你可以传递一个希望执行的 JavaScript 文件名,或者你可以用它作为 shell 脚本的替代,像这样:

#!/usr/bin/env jjs 

var name = $ARG[0]; 
print(name ? "Hello, ${name}!" : "Hello, world!");

向 jjs 传递程序参数,需要加“—”前缀。因此举例来说,你可以这样调用:

./hello-script.js – Joe

如果没有“—”前缀,参数会被解释为文件名。

向 Java 传递数据或者从 Java 传出数据

正如上文所说的那样,你可以从 Java 代码直接调用 JavaScript;只需获取一个引擎对象并调用它的“eval”方法。你可以将数据作为字符串显式传递……

ScriptEngineManager scriptEngineManager = 
      new ScriptEngineManager(); 
ScriptEngine nashorn = 
      scriptEngineManager.getEngineByName("nashorn"); 
String name = "Olli"; 
nashorn.eval("print('" + name + "')");

……或者你可以在 Java 中传递绑定,它们是可以从 JavaScript 引擎内部访问的全局变量:

int valueIn = 10; 
SimpleBindings simpleBindings = new SimpleBindings(); 
simpleBindings.put("globalValue", valueIn); 
nashorn.eval("print (globalValue)", simpleBindings);

JavaScript eval 的求值结果将会从引擎的“eval”方法返回:

Integer result = (Integer) nashorn.eval("1 + 2"); 
assert(result == 3);

在 Nashorn 中使用 Java 类

前面已经提到,Nashorn 最强大的功能之一源于在 JavaScript 中调用 Java 类。你不仅能够访问类并创建实例,你还可以继承他们,调用他们的静态方法,几乎可以做任何你能在 Java 中做的事。

作为一个例子,让我们看下来龙去脉。JavaScript 没有任何语言特性是面向并发的,所有常见的运行时环境都是单线程的,或者至少没有任何共享状态。有趣的是,在 Nashorn 环境中,JavaScript 确实可以并发运行,并且有共享状态,就像在 Java 中一样:

// 访问 Java 类 Thread 
var Thread = Java.type("java.lang.Thread"); 

// 带有 run 方法的子类 
var MyThread = Java.extend(Thread, { 
    run: function() { 
        print("Run in separate thread"); 
    } 
}); 
var th = new MyThread(); 
th.start(); 
th.join();

请注意,从 Nashorn 访问类的规范做法是使用 Java.type,并且可以使用 Java.extend 扩展一个类。

令人高兴的函数式

从各方面来说,随着 JDK 8 的发布,Java——至少在某种程度上——已经变成一种函数式语言。开发人员可以在集合上使用高阶函数,比如,遍历所有的元素。高阶函数是把另一个函数当作参数的函数,它可以用这个函数参数做些有意义的事情。请看下面Java 中高阶函数的示例:

List<Integer> list = Arrays.asList(3, 4, 1, 2); 
list.forEach(new Consumer() { 

    @Override 
    public void accept(Object o) { 
        System.out.println(o); 
    } 
});

对于这个例子,我们的传统实现方式是使用一个“外部”循环遍历元素,但现在,我们没有那样做,而是将一个“Consumer”函数传递给了“forEach”操作,一个高阶的“内部循环”操作会将集合中的每个元素一个一个地传递给 Consumer 的“accept”方法并执行它。

如上所述,对于这样的高阶函数,函数式语言的做法是接收一个函数参数,而不是一个对象。虽然在传统上讲,传递函数引用本身超出了 Java 的范围,但现在,JDK 8 有一些语法糖,使它可以使用 Lambda 表达式(又称为“闭包”)来实现那种表示方式。例如:

List<Integer> list = Arrays.asList(3, 4, 1, 2); 
list.forEach(el -> System.out.println(el));

在这种情况下,“forEach”的参数是这样一个函数引用的形式。这是可行的,因为 Customer 是一个函数式接口(有时称为“单一抽象方法(Single Abstract Method)”类型或“SAM”)。

那么,我们为什么要在讨论 Nashorn 时谈论 Lambda 表达式呢?因为在 JavaScript 中,开发人员也可以这样编写代码,而在这种情况下,Nashorn 可以特别好地缩小 Java 和 JavaScript 之间的差距。尤其是,它甚至允许开发人员将纯 JavaScript 函数作为函数式接口(SAM 类型)的实现来传递。

让我们来看一些纯 JavaScript 代码,它们与上述 Java 代码实现一样的功能。注意,在 JavaScript 中没有内置的列表类型,只有数组;不过这些数组的大小是动态分配的,而且有与 Java 列表类似的方法。因此,在这个例子中,我们调用一个 JavaScript 数组的“for Each”方法:

var jsArray = [4,1,3,2]; 
jsArray.forEach(function(el) { print(el) } );

相似之处显而易见;但那还不是全部。开发人员还可以将这样一个 JavaScript 数组转换成一个 Java 列表:

var list = java.util.Arrays.asList(jsArray);

看见了吗?是的,这就是在 Nashorn 中运行的 JavaScript。既然它现在是一个 Java 列表,那么开发人员就可以调用其“forEach”方法。注意,这个“forEach”方法不同于我们在 JavaScript 数组上调用的那个,它是定义在 java 集合上的“forEach”方法。这里,我们仍然传递一个纯 JavaScript 函数:

list.forEach(function(el) { print(el) } );

Nashorn 允许开发人员在需要使用函数式接口(SAM 类型)的地方提供纯 JavaScript 函数引用。这不仅适应于 Java,也适应于 JavaScript。

ECMAScript 的下一个版本——预计是今年的最后一个版本——将包含函数的短语法,允许开发人员将函数写成近似 Java Lambda 表达式的形式,只不过它使用双箭头 =>。这进一步增强了一致性。

Nashorn JavaScript 特有的方言

正如简介部分所提到的那样,Nashorn 支持的 JavaScript 实现了 ECMAScript 5.1 版本及一些扩展。我并不建议使用这些扩展,因为它们既不是 Java,也不是 JavaScript,两类开发人员都会觉得它不正常。另一方面,有两个扩展在整个 Oracle 文档中被大量使用,因此,我们应该了解它们。首先,让我们为了解第一个扩展做些准备。正如前文所述,开发人员可以使用 Java.extend 从 JavaScript 中扩展一个 Java 类。如果需要继承一个抽象 Java 类或者实现一个接口,那么可以使用一种更简便的语法。在这种情况下,开发人员实际上可以调用抽象类或接口的构造函数,并传入一个描述方法实现的 JavaScript 对象常量。这种常量不过是 name/value 对,你可能了解 JSON 格式,这与那个类似。这使我们可以像下面这样实现 Runnable 接口:

var r = new java.lang.Runnable({
    run: function() {
        print("running...\n");
    }
});

在这个例子中,一个对象常量指定了 run 方法的实现,我们实际上是用它调用了 Runnable 的构造函数。注意,这是 Nashorn 的实现提供给我们的一种方式,否则,我们无法在 JavaScript 这样做。

示例代码已经与我们在 Java 中以匿名内部类实现接口的方式类似了,但还不完全一样。这将我们带到了第一个扩展,它允许开发人员在调用构造函数时在右括号“)”后面传递最后一个参数。这种做法的代码如下:

var r = new java.lang.Runnable() {
    run: function() {
       print("running...\n");
    }
};

……它实现了完全相同的功能,但更像 Java。

第二个常用的扩展一种函数的简便写法,它允许删除单行函数方法体中的两个花括号以及 return 语句。这样,上一节中的例子:

list.forEach(function(el) { print(el) } );

可以表达的更简洁一些:

list.forEach(function(el) print(el));

Avatar.js

我们已经看到,有了 Nashorn,我们就有了一个嵌入到 Java 的优秀的 JavaScript 引擎。我们也已经看到,我们可以从 Nashorn 访问任意 Java 类。 Avatar.js 更进一步,它“为 Java 平台带来了 Node 编程模型、API 和模块生态系统”。要了解这意味着什么以及它为什么令人振奋,我们首先必须了解 Node 是什么。从根本上说,Node 是将 Chrome 的 V8 JavaScript 引擎剥离出来,使它可以从命令行运行,而不再需要浏览器。这样,JavaScript 就不是只能在浏览器中运行了,而且可以在服务器端运行。在服务器端以任何有意义的方式运行 JavaScript 都至少需要访问文件系统和网络。为了做到这一点,Node 内嵌了一个名为 libnv 的库,以异步方式实现该项功能。实际上,这意味着操作系统调用永远不会阻塞,即使它过一段时间才能返回。开发人员需要提供一个回调函数代替阻塞。该函数会在调用完成时立即触发,如果有任何结果就返回。

有若干公司都在重要的应用程序中使用了 Node,其中包括 Walmart Paypal

让我们来看一个 JavaScript 的小例子,它是我根据 Node 网站上的例子改写而来:

// 加载“http”模块(这是阻塞的)来处理 http 请求 
var http = require('http'); 

// 当有请求时,返回“Hello,World\n”
function handleRequest(req, res) { 
  res.writeHead(200, {'Content-Type': 'text/plain'}); 
  res.end('Hello, World\n'); 
} 

// 监听 localhost,端口 1337
// 并提供回调函数 handleRequest
// 这里体现了其非阻塞 / 异步特性 
http.createServer(handleRequest).listen(1337, '127.0.0.1'); 

// 记录到控制台,确保我们在沿着正确的方向前进 
console.log('Get your hello at http://127.0.0.1:1337/');

要运行这段代码,需要安装 Node,然后将上述 JavaScript 代码保存到一个文件中。最后,将该文件作为一个参数调用 Node。

将 libuv 绑定到 Java 类,并使 JavaScript 可以访问它们,Avatar.js 旨在以这种方式提供与 Node 相同的核心 API。虽然这可能听上去很繁琐,但这种方法很有效。Avatar.js 支持许多 Node 模块。对 Node 主流 Web 框架“ express ”的支持表明,这种方式确实适用于许多现有的项目。

令人遗憾的是,在写这篇文章的时候,还没有一个 Avatar.js 的二进制分发包。有一个自述文件说明了如何从源代码进行构建,但是如果真没有那么多时间从头开始构建,那么也可以从这里下载二进制文件而不是自行构建。两种方式都可以,但为了更快的得到结果,我建议选择第二种方式。

一旦创建了二进制文件并放进了 lib 文件夹,就可以使用下面这样的语句调用 Avatar.js 框架:

java -Djava.library.path=lib -jar lib/avatar-js.jar helloWorld.js

假设演示服务器(上述代码)保存到了一个名为“helloWorld.js”的文件中。

让我们再问一次,这为什么有用?Oracle 的专家(幻灯片10 )指出了该库的几个适用场景。我对其中的两点持大致相同的看法,即:

  1. 有一个 Node 应用程序,并希望使用某个 Java 库作为 Node API 的补充
  2. 希望切换到 JavaScript 和 Node API,但需要将遗留的 Java 代码部分或全部嵌入

两个应用场景都可以通过使用 Avatar.js 并从 JavaScript 代码中调用任何需要的 Java 类来实现。我们已经看到,Nashorn 支持这种做法。

下面我将举一个第一个应用场景的例子。JavaScript 目前只有一种表示数值的类型,名为“number”。这相当于 Java 的“double”精度,并且有同样的限制。JavaScript 的 number,像 Java 的 double 一样,并不能表示任意的范围和精度,比如在计量货币时。

在 Java 中,我们可以使用 BigDecimal,它正是用于此类情况。但 JavaScript 没有内置与此等效的类型,因此,我们就可以直接从 JavaScript 代码中访问 BigDecimal 类,安全地处理货币值。

让我们看一个 Web 服务示例,它计算某个数量的百分之几是多少。首先,需要有一个函数执行实际的计算:

var BigDecimal = Java.type('java.math.BigDecimal'); 

function calculatePercentage(amount, percentage) { 
    var result = new BigDecimal(amount).multiply( 
     new BigDecimal(percentage)).divide( 
           new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_EVEN); 
    return result.toPlainString(); 
}

JavaScript 没有类型声明,除此之外,上述代码与我针对该任务编写的 Java 代码非常像:

public static String calculate(String amount, String percentage) { 
    BigDecimal result = new BigDecimal(amount).multiply( 
     new BigDecimal(percentage)).divide( 
          new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_EVEN); 
    return result.toPlainString(); 
}

我们只需要替换上文 Node 示例中的 handleRequest 函数就可以完成代码。替换后的代码如下:

// 加载工具模块“url”来解析 url
var url = require('url'); 

function handleRequest(req, res) { 
    // '/calculate' Web 服务地址 
    if (url.parse(req.url).pathname === '/calculate') { 
        var query = url.parse(req.url, true).query;  
       // 数量和百分比作为查询参数传入 
        var result = calculatePercentage(query.amount,
                                          query.percentage); 
        res.writeHead(200, {'Content-Type': 'text/plain'}); 
        res.end(result + '\n'); 
    } 
}

我们又使用了 Node 核心模块来处理请求 URL,从中解析出查询参数 amount 和 percentage。

当启动服务器(如前所述)并使用浏览器发出下面这样一个请求时,

http://localhost:1337/calculate?
amount=99700000000000000086958613&percentage=7.59

就会得到正确的结果“7567230000000000006600158.73”。这在单纯使用 JavaScript 的“number”类型时是不可能。

当你决定将现有的 JEE 应用程序迁移到 JavaScript 和 Node 时,第二个应用场景就有意义了。在这种情况下,你很容易就可以从 JavaScript 代码内访问现有的所有服务。另一个相关的应用场景是,在使用 JavaScript 和 Node 构建新的服务器功能时,仍然可以受益于现有的 JEE 服务。

此外,基于 Avatar.js 的 Avatar 项目也朝着相同的方向发展。该项目的详细信息超出了本文的讨论范围,但读者可以阅读这份Oracle 公告做一个粗略的了解。该项目的基本思想是,用JavaScript 编写应用程序,并访问JEE 服务。Avatar 项目包含Avatar.js 的一个二进制分发包,但它需要Glassfish 用于安装和开发。

小结

Nashorn 项目增强了 JDK 6 中原有的 Rhino 实现,极大地提升了运行时间较长的应用程序的性能,例如用在 Web 服务器中的时候。Nashorn 将 Java 与 JavaScript 集成,甚至还考虑了 JDK 8 的新 Lambda 表达式。Avatar.js 带来了真正的创新,它基于这些特性构建,并提供了企业级 Java 与 JavaScript 代码的集成,同时在很大程度上与 JavaScript 服务器端编程事实上的标准兼容。

完整实例以及用于 Mac OS X 的 Avatar.js 二进制文件可以从 Github 上下载。

关于作者

Oliver Zeigermann是一名来自德国汉堡的自由软件架构师 / 开发人员、顾问和教练。目前,他致力于在企业应用程序中使用 JavaScript。

查看英文原文: Nashorn - The Combined Power of Java and JavaScript in JDK 8

评论

发布