12.2 在Nashorn中执行JavaScript代码

本节介绍 Nashorn 环境,还会讨论两种执行 JavaScript 代码的方式(这两种方式使用的工具都在 $JAVA_HOME 的子目录 bin 中)。

  • jrunscript

这是一个简单的脚本运行程序,执行 .js 格式的 JavaScript 文件。

  • jjs

这是一个功能更完整的 shell,既能运行脚本,也能作为交互式读取 - 求值 - 输出循环(Read-Eval-Print-Loop,REPL)环境使用,用于探索 Nashorn 及其功能。

我们先介绍基本的运行程序 jrunscript,它适用于大多数简单的 JavaScript 应用。

12.2.1 在命令行中运行

若想在 Nashorn 中执行名为 my_script.js 的 JavaScript 文件,使用 jrunscript 命令即可:

  1. jrunscript my_script.js

除了 Nashorn,jrunscript 还能使用其他脚本引擎(12.3 节会详细介绍脚本引擎)。如果需要使用其他引擎,可以通过 -l 选项指定:

  1. jrunscript l nashorn my_script.js

12.2 在Nashorn中执行JavaScript代码 - 图1 如果有合适的脚本引擎,使用这个选项还能让 jrunscript 运行使用其他语言编写的脚本。

这个基本的运行程序特别适合在简单的应用场景中使用,不过它有一定的局限性,因此,在重要场合下,我们需要使用功能更强的执行环境——jjs,也就是 Nashorn shell。

12.2.2 使用Nashorn shell

启动 Nashorn shell 的命令是 jjs。Nashorn shell 既可以交互式使用,也可以非交互式使用,能直接替代 jrunscript

最简单的 JavaScript 示例当然是经典的“Hello World”,我们看一下如何在交互式 shell 中编写这个示例:

  1. $ jjs
  2. jjs> print("Hello World!");
  3. Hello World!
  4. jjs>

在 shell 中可以轻易处理 Nashorn 和 Java 之间的相互操作,12.4.1 节会详细介绍,不过现在先举个例子。若想在 JavaScript 中直接访问 Java 类和方法,使用完全限定的类名即可。下面这个实例获取 Java 原生的正则表达式功能:

  1. jjs> var pattern = java.util.regex.Pattern.compile("\\d+");
  2. jjs> var myNums = pattern.split("a1b2c3d4e5f6");
  3. jjs> print(myNums);
  4. [Ljava.lang.String;@10b48321
  5. jjs> print(myNums[0]);
  6. a

12.2 在Nashorn中执行JavaScript代码 - 图2 在 REPL 中打印 JavaScript 变量 myNums 时,得到的结果是 [Ljava.lang.String;@10b48321。这表明,虽然 myNums 是 JavaScript 代码中的变量,但其实它是 Java 中的字符串数组。

稍后会详细说明 Nashorn 和 Java 之间的相互操作,现在先介绍 jjs 的其他功能。jjs 命令的通用格式是:

  1. jjs [<options>] <files> [-- <arguments>]

能传给 jjs 的选项有很多,其中最常使用的如下所示。

  • -cp-classpath:指定在哪个位置寻找额外的 Java 类(稍后会发现,通过 Java.type 机制实现)。

  • -doe-dump-on-error:如果强制退出 Nashorn,转储完整的错误信息。

  • -J:把选项传给 JVM。例如,如果想增加 JVM 可用的最大内存,可以这么做:

  1. $ jjs -J-Xmx4g
  2. jjs> java.lang.Runtime.getRuntime().maxMemory()
  3. 3817799680
  • -strict:在 JavaScript 严格模式中执行所有脚本和函数。这是 ECMAScript 5 引入的 JavaScript 特性,目的是减少缺陷和错误。所有新编写的 JavaScript 代码都推荐使用严格模式,如果你对这个特性还不了解,应该找些资料看看。

  • -D:让开发者把键值对表示的系统属性传给 Nashorn,这和 JVM 的通常做法一样。例如:

  1. $ jjs DmyKey=myValue
  2. jjs> java.lang.System.getProperty("myKey");
  3. myValue
  • -v-version:打印标准的 Nashorn 版本字符串。

  • -fv-fullversion:打印完整的 Nashorn 版本字符串。

  • -fx:把脚本当成 JavaFX GUI 应用执行。JavaFX 程序员使用 Nashorn 可以少编写一些样板代码。2

  • -h:显示帮助信息。

  • -scripting:启用 Nashorn 专用的脚本扩展。下一节会介绍这个功能。

2JavaFX 是开发 GUI 应用的标准 Java 技术,不过超出了本书范畴。

12.2.3 在jjs中编写脚本

jjs shell 可用于测试一些基本的 JavaScript 代码,或者使用交互式方式试验不熟悉的 JavaScript 包(例如,学习使用包时)。不过,jjs 有个限制,不能输入多行代码,也没提供其他大量使用 REPL 的语言常用的高级功能。

其实,jjs 非常适合在非交互式场合下使用,例如启动使用 JavaScript 编写的守护进程。对于这种情况,可以使用下述方式调用 jjs

  1. $ jjs -scripting my_script.js

这样调用,可以使用增强的 jjs 功能,其中包含一些有用的扩展。很多扩展都能让脚本程序员通过更熟悉的方式使用 Nashorn。

1. 脚本中的注释

在传统的 Unix 脚本中,# 符号表示注释,一直到行尾结束。而 JavaScript 使用 C/C++ 风格的注释,使用 // 表示注释,一直到行尾结束。Nashorn 支持这种注释方式,不过在脚本模式中也能使用 Unix 脚本的注释方式,因此,下述代码完全合法:

  1. #!/usr/bin/jjs
  2. # 在脚本模式中,这样写注释完全合法
  3. print("After the comment");

2. 行内执行命令

资深 Unix 程序员通常把这个功能称为“反引号”。在 bash 脚本中,我们可以编写如下的代码,使用 Unix 的 curl 命令下载谷歌首页的内容:

  1. echo "Google says: " `curl http://www.google.co.uk`

类似地,在 Nashorn 脚本中,我们也可以使用反引号(`)执行 Unix shell 命令。如下所示:

  1. print("Google says: "+ `curl http://www.google.co.uk`);

3. 字符串插值

字符串插值是一种特殊的句法,无需连接字符串,就能直接插入变量的内容。在 Nashorn 脚本中,我们可以使用 ${} 把变量的值插入字符串。例如,前面下载网页内容的示例,使用插值后可以改写成:

  1. var url = "www.google.co.uk";
  2. var pageContents = `curl http://${url}`;
  3. print("Google says: ${pageContents}");

4. 特殊变量

Nashorn 还提供了几个特殊的全局变量和函数,编写脚本时特别有用,而且普通的 JavaScript 中没有。例如,传入脚本的参数可以通过 $ARG 变量获取。参数必须使用约定的 — 方式传入,像下面这样:

  1. jjs test1.jjs -- aa bbb cccc

获取参数的方式如下所示:

  1. print($ARG);
  2. for(var i=0; i < $ARG.length; i++) {
  3. print("${i}: "+ $ARG[i]);
  4. }

12.2 在Nashorn中执行JavaScript代码 - 图3 $ARG 变量是一个 JavaScript 数组(观察传给 print() 方法后的表现可以看出来),而且要当成数组处理。学过其他语言的程序员可能觉得这种句法有点让人困惑,因为有些语言使用 $ 符号表示标量变量。

我们能遇到的另一个特殊的全局变量是 $ENV,这个变量用于获取当前的环境变量。例如,下述代码打印当前用户的家目录:

  1. print("HOME = "+ $ENV.HOME); # 在我的电脑中打印的是/home/ben

Nashorn 还提供了一个特殊的全局函数,$EXEC()。这个函数的作用和前面介绍的反引号类似,如下述示例所示:

  1. var execOutput = $EXEC("echo Print this on stdout");
  2. print(execOutput);

你可能注意到了,使用反引号或 $EXEC() 函数时,不会打印所执行命令的输出,而是返回函数的返回值。这是为了避免命令的输出扰乱主脚本的输出。

Nashorn 提供了另外两个特殊的变量,有助于程序员处理脚本中所执行命令的输出:$OUT$ERR。这两个变量分别用于捕获脚本中所执行命令的输出和错误消息。例如:

  1. $EXEC("echo Print this on stdout");
  2. // 没有修改标准输出的代码
  3. var saveOut = $OUT;
  4. print("- - - - - - -");
  5. print(saveOut);

$OUT$ERR 中的内容一直存在,除非主脚本中的后续代码修改这些内容(例如执行其他命令)。

5. 行内文档

JavaScript 和 Java 一样,不支持把包围字符串的两个引号放在不同的行(这种字符串叫多行字符串)。可是,在脚本模式中,Nashorn 通过扩展对此提供了支持。这种功能也叫行内文档或 heredoc,是脚本语言的通用特性。

heredoc 以 < 开头,从下一行开始,直到结束符号(可以使用任何字符串,不过经常全部大写,经常使用的字符串有 ENDEND_DOCEND_STREOFEOSTR),中间都是多行字符串的内容。在结束符号之后,脚本恢复正常。我们看一个示例:

  1. var hw = "Hello World!";
  2. var output = <<EOSTR;
  3. This is a multiline string
  4. It can interpolate too - ${hw}
  5. EOSTR
  6. print(output);

6. Nashorn提供的辅助函数

Nashorn 还提供了一些辅助函数,让开发者能轻易实现 shell 脚本经常要执行的常见任务。

  • print()/echo()

前面的示例都用到了 print() 函数。这两个函数的表现和预期完全一样,把传入的字符串打印出来,后面还会加一个换行符。

  • quit()/exit()

这两个函数的作用完全一样——退出脚本。可以把一个整数参数传给这两个函数,作为脚本进程的返回码。如果没传入参数,返回码默认为 0——这是 Unix 进程的习惯做法。

  • readLine()

从标准输入(通常是键盘)读取一行输入。默认情况下,这个函数会把读取的内容打印到标准输出,不过,如果把 readLine() 函数的返回值赋值给变量,输入的数据就在此结束,如下述示例所示:

  1. print("Please enter your name: ");
  2. var name = readLine();
  3. print("Please enter your age: ");
  4. var age = readLine();
  5.  
  6. print(<<EOREC);
  7. Student Record
  8. -+-+-+-+-+-+-+-
  9. Name: ${name}
  10. Age: ${age}
  11. EOREC
  • readFully()

readFully() 函数不从标准输入读取数据,而是加载一个文件中的全部内容。和 readLine() 函数一样,加载的内容不是打印到标准输出,就是赋值给变量:

  1. var contents = readFully("input.txt");
  • load()

这个函数用于加载并执行脚本(使用 JavaScript 的 eval 函数执行)。脚本可以从本地路径或 URL 中加载。除此之外,还可以使用 JavaScript 的脚本对象表示法把脚本文件定义成一个字符串。

12.2 在Nashorn中执行JavaScript代码 - 图4

使用 load() 函数执行其他脚本可能出现意料之外的错误。JavaScript 支持使用 try-catch 块处理异常,所以加载代码时要使用这种方式。

下面举个简单的例子,这个例子在 Nashorn 中加载图形可视化库 D3:

  1. try {
  2. load(“http://d3js.org/d3.v3.min.js”);
  3. } catch (e) {
  4. print(“Something went wrong, probably that we're not a web browser”);
  5. }
  • loadWithNewGlobal()

load() 函数会在当前 JavaScript 上下文中执行加载的脚本。而有时我们想把代码放入属于它自己的纯净上下文中。此时,可以使用 loadWithNewGlobal() 函数,在全新的全局上下文中执行脚本。

7. shebang句法

本节介绍的所有功能都是为了方便编写 shell 脚本,让 jjs 能替代 bash、Perl 或其他脚本语言。为了完善这种支持,最后还需要一个功能——“shebang”句法,用来启动使用 Nashorn 编写的脚本。

12.2 在Nashorn中执行JavaScript代码 - 图5 如果可执行脚本的第一行以 #! 开头,而且后面是一个可执行文件的路径,那么 Unix 操作系统会假定这个路径指向一个解释器,而且这个解释器能处理这种脚本。执行脚本时,操作系统会启动指定的解释器,并把脚本文件传给解释器处理。

对 Nashorn 来说,最好创建一个符号链接(可能需要 sudo 访问),把 /usr/bin/jjs(或 /usr/local/bin/jjs)指向 jjs 的真正位置(通常是 $JAVA_HOME/bin/jjs)。然后,可以像下面这样编写 Nashorn shell 脚本:

  1. #!/usr/bin/jjs
  2. # ……脚本中的其他内容

对更高级的应用场景来说(例如长时间运行的守护进程),Nashorn 甚至提供了对 Node.js 的支持。这个功能由 Avatar 项目中的 Avatar.js 提供,详情参见 12.5 节的“Avatar 项目”。

本节介绍的工具便于直接在命令行中执行 JavaScript 代码,不过多数情况下,我们希望使用另一种方式执行 JavaScript 代码——在 Java 程序中调用 Nashorn,执行 JavaScript 代码。实现这种执行方式的 API 包含在 Java 包 javax.script 中,所以,接下来我们要介绍这个包,并讨论 Java 如何与解释脚本语言的引擎交互。