抖音技术能力大揭密!钜惠大礼、深度体验,尽在火山引擎增长沙龙,就等你来! 立即报名>> 了解详情
写点什么

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

2014 年 12 月 29 日

从 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

2014 年 12 月 29 日 05:5440010
用户头像

发布了 256 篇内容, 共 69.3 次阅读, 收获喜欢 6 次。

关注

评论

发布
暂无评论
发现更多内容

13年Java开发经验精华总结!29大核心知识模块,带你直达架构师!

Java架构追梦

Java 阿里巴巴 架构 全栈知识点

量化合约机器人APP开发|量化合约机器人软件系统开发

开發I852946OIIO

系统开发

十大经典系统架构设计面试题

有理想的coder

架构 面试 架构设计 架构面试

量化合约交易机器人系统开发|量化合约交易机器人APP软件开发

开發I852946OIIO

系统开发

浅谈BSS3.0产品“守成”之策上 • 架构提升篇

鲸品堂

运维 性能 架构·

技术团队内部管理思考

6:00 am

技术管理

Linux df 命令

一个大红包

linux命令 4月日更

HTTPS双向认证

上海派拉基础研发

https HTTP ssl SSL 连接

5分钟教你学会GaussDB数据分布策略设计

华为云开发者社区

数据库 分布式数据库 GaussDB GaussDB(for openGauss) 数据分布

数字货币自动交易机器人APP开发|数字货币自动交易机器人软件系统开发

开發I852946OIIO

系统开发

Rust从0到1-枚举-match控制流

rust 枚举 match

这份阿里P8大佬手写的 “Java核心面试精选” 疯传阿里内网

码农之家

Java 编程 程序员 互联网 面试

Kafka源码阅读笔记(1)

InfoQ_Springup

kafka

【详解文件IO系列】讲讲 MQ 消息中间件 (Kafka,RocketMQ等)与 MMAP、PageCache 的故事

Linux服务器开发

网络编程 Linux服务器开发 底层实现原理 网络io C++后端开发

牛客网官推!3天访问量直接破20W的Java高频面试汇总笔记太香了!

程序员小毕

Java 程序员 架构 面试 分布式

lakin跟投社区APP开发|lakin跟投社区软件系统开发

开發I852946OIIO

系统开发

我们真的可以使世界成为无密码的地方吗?

龙归科技

网络 安全性

【LeetCode】删除排序链表中的重复元素Java题解

HQ数字卡

算法 LeetCode 4月日更

Google Analytics

曦语

数据分析

翻译:《实用的Python编程》09_01_Packages

codists

Python

人生向前

shun123456789

MySQL性能监控与调优

Sakura

四月日更

合约量化交易机器人系统开发|合约量化交易机器人APP软件开发

开發I852946OIIO

合约跟单交易系统开发量化策略

薇電13242772558

数字货币

与同事组队,用 3s 把工作节点打通,建立信赖与协作关系。

叶小鍵

微擎的日志文件保存在哪里?如何查看。

微擎应用商城

如何利用ipad随时随地开发代码

有理想的coder

ipad 编程 远程

1分钟get什么是训练数据

澳鹏Appen

人工智能 机器学习 数据集 人工智能大数据

从能耗大户“变身”智能绿色办公,只需一步到位!

IoT云工坊

物联网 API sdk 办公空间 智能转型

百度联合清华,全球首个十亿像素数据集来了!

百度大脑

人工智能 百度

11 个非常实用的 Python 和 Shell 拿来就用脚本实例!

JackTian

Python Shell linux运维 脚本语言 程序员编程

Study Go: From Zero to Hero

Study Go: From Zero to Hero

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