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 命令即可:
jrunscript my_script.js
除了 Nashorn,jrunscript 还能使用其他脚本引擎(12.3 节会详细介绍脚本引擎)。如果需要使用其他引擎,可以通过 -l 选项指定:
jrunscript –l nashorn my_script.js
如果有合适的脚本引擎,使用这个选项还能让
jrunscript运行使用其他语言编写的脚本。
这个基本的运行程序特别适合在简单的应用场景中使用,不过它有一定的局限性,因此,在重要场合下,我们需要使用功能更强的执行环境——jjs,也就是 Nashorn shell。
12.2.2 使用Nashorn shell
启动 Nashorn shell 的命令是 jjs。Nashorn shell 既可以交互式使用,也可以非交互式使用,能直接替代 jrunscript。
最简单的 JavaScript 示例当然是经典的“Hello World”,我们看一下如何在交互式 shell 中编写这个示例:
$ jjsjjs> print("Hello World!");Hello World!jjs>
在 shell 中可以轻易处理 Nashorn 和 Java 之间的相互操作,12.4.1 节会详细介绍,不过现在先举个例子。若想在 JavaScript 中直接访问 Java 类和方法,使用完全限定的类名即可。下面这个实例获取 Java 原生的正则表达式功能:
jjs> var pattern = java.util.regex.Pattern.compile("\\d+");jjs> var myNums = pattern.split("a1b2c3d4e5f6");jjs> print(myNums);[Ljava.lang.String;@10b48321jjs> print(myNums[0]);a
在 REPL 中打印 JavaScript 变量
myNums时,得到的结果是[Ljava.lang.String;@10b48321。这表明,虽然myNums是 JavaScript 代码中的变量,但其实它是 Java 中的字符串数组。
稍后会详细说明 Nashorn 和 Java 之间的相互操作,现在先介绍 jjs 的其他功能。jjs 命令的通用格式是:
jjs [<options>] <files> [-- <arguments>]
能传给 jjs 的选项有很多,其中最常使用的如下所示。
-cp或-classpath:指定在哪个位置寻找额外的 Java 类(稍后会发现,通过Java.type机制实现)。-doe或-dump-on-error:如果强制退出 Nashorn,转储完整的错误信息。-J:把选项传给 JVM。例如,如果想增加 JVM 可用的最大内存,可以这么做:
$ jjs -J-Xmx4gjjs> java.lang.Runtime.getRuntime().maxMemory()3817799680
-strict:在 JavaScript 严格模式中执行所有脚本和函数。这是 ECMAScript 5 引入的 JavaScript 特性,目的是减少缺陷和错误。所有新编写的 JavaScript 代码都推荐使用严格模式,如果你对这个特性还不了解,应该找些资料看看。-D:让开发者把键值对表示的系统属性传给 Nashorn,这和 JVM 的通常做法一样。例如:
$ jjs –DmyKey=myValuejjs> java.lang.System.getProperty("myKey");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:
$ jjs -scripting my_script.js
这样调用,可以使用增强的 jjs 功能,其中包含一些有用的扩展。很多扩展都能让脚本程序员通过更熟悉的方式使用 Nashorn。
1. 脚本中的注释
在传统的 Unix 脚本中,# 符号表示注释,一直到行尾结束。而 JavaScript 使用 C/C++ 风格的注释,使用 // 表示注释,一直到行尾结束。Nashorn 支持这种注释方式,不过在脚本模式中也能使用 Unix 脚本的注释方式,因此,下述代码完全合法:
#!/usr/bin/jjs# 在脚本模式中,这样写注释完全合法print("After the comment");
2. 行内执行命令
资深 Unix 程序员通常把这个功能称为“反引号”。在 bash 脚本中,我们可以编写如下的代码,使用 Unix 的 curl 命令下载谷歌首页的内容:
echo "Google says: " `curl http://www.google.co.uk`
类似地,在 Nashorn 脚本中,我们也可以使用反引号(`)执行 Unix shell 命令。如下所示:
print("Google says: "+ `curl http://www.google.co.uk`);
3. 字符串插值
字符串插值是一种特殊的句法,无需连接字符串,就能直接插入变量的内容。在 Nashorn 脚本中,我们可以使用 ${ 把变量的值插入字符串。例如,前面下载网页内容的示例,使用插值后可以改写成:
var url = "www.google.co.uk";var pageContents = `curl http://${url}`;print("Google says: ${pageContents}");
4. 特殊变量
Nashorn 还提供了几个特殊的全局变量和函数,编写脚本时特别有用,而且普通的 JavaScript 中没有。例如,传入脚本的参数可以通过 $ARG 变量获取。参数必须使用约定的 — 方式传入,像下面这样:
jjs test1.jjs -- aa bbb cccc
获取参数的方式如下所示:
print($ARG);for(var i=0; i < $ARG.length; i++) {print("${i}: "+ $ARG[i]);}
![]()
$ARG变量是一个 JavaScript 数组(观察传给print()方法后的表现可以看出来),而且要当成数组处理。学过其他语言的程序员可能觉得这种句法有点让人困惑,因为有些语言使用$符号表示标量变量。
我们能遇到的另一个特殊的全局变量是 $ENV,这个变量用于获取当前的环境变量。例如,下述代码打印当前用户的家目录:
print("HOME = "+ $ENV.HOME); # 在我的电脑中打印的是/home/ben
Nashorn 还提供了一个特殊的全局函数,$EXEC()。这个函数的作用和前面介绍的反引号类似,如下述示例所示:
var execOutput = $EXEC("echo Print this on stdout");print(execOutput);
你可能注意到了,使用反引号或 $EXEC() 函数时,不会打印所执行命令的输出,而是返回函数的返回值。这是为了避免命令的输出扰乱主脚本的输出。
Nashorn 提供了另外两个特殊的变量,有助于程序员处理脚本中所执行命令的输出:$OUT 和 $ERR。这两个变量分别用于捕获脚本中所执行命令的输出和错误消息。例如:
$EXEC("echo Print this on stdout");// 没有修改标准输出的代码var saveOut = $OUT;print("- - - - - - -");print(saveOut);
$OUT 和 $ERR 中的内容一直存在,除非主脚本中的后续代码修改这些内容(例如执行其他命令)。
5. 行内文档
JavaScript 和 Java 一样,不支持把包围字符串的两个引号放在不同的行(这种字符串叫多行字符串)。可是,在脚本模式中,Nashorn 通过扩展对此提供了支持。这种功能也叫行内文档或 heredoc,是脚本语言的通用特性。
heredoc 以 < 开头,从下一行开始,直到结束符号(可以使用任何字符串,不过经常全部大写,经常使用的字符串有 END、END_DOC、END_STR、EOF 和 EOSTR),中间都是多行字符串的内容。在结束符号之后,脚本恢复正常。我们看一个示例:
var hw = "Hello World!";var output = <<EOSTR;This is a multiline stringIt can interpolate too - ${hw}EOSTRprint(output);
6. Nashorn提供的辅助函数
Nashorn 还提供了一些辅助函数,让开发者能轻易实现 shell 脚本经常要执行的常见任务。
print()/echo()
前面的示例都用到了 print() 函数。这两个函数的表现和预期完全一样,把传入的字符串打印出来,后面还会加一个换行符。
quit()/exit()
这两个函数的作用完全一样——退出脚本。可以把一个整数参数传给这两个函数,作为脚本进程的返回码。如果没传入参数,返回码默认为 0——这是 Unix 进程的习惯做法。
readLine()
从标准输入(通常是键盘)读取一行输入。默认情况下,这个函数会把读取的内容打印到标准输出,不过,如果把 readLine() 函数的返回值赋值给变量,输入的数据就在此结束,如下述示例所示:
print("Please enter your name: ");var name = readLine();print("Please enter your age: ");var age = readLine();print(<<EOREC);Student Record-+-+-+-+-+-+-+-Name: ${name}Age: ${age}EOREC
readFully()
readFully() 函数不从标准输入读取数据,而是加载一个文件中的全部内容。和 readLine() 函数一样,加载的内容不是打印到标准输出,就是赋值给变量:
var contents = readFully("input.txt");
load()
这个函数用于加载并执行脚本(使用 JavaScript 的 eval 函数执行)。脚本可以从本地路径或 URL 中加载。除此之外,还可以使用 JavaScript 的脚本对象表示法把脚本文件定义成一个字符串。
使用
load()函数执行其他脚本可能出现意料之外的错误。JavaScript 支持使用try-catch块处理异常,所以加载代码时要使用这种方式。下面举个简单的例子,这个例子在 Nashorn 中加载图形可视化库 D3:
try {load(“http://d3js.org/d3.v3.min.js”);} catch (e) {print(“Something went wrong, probably that we're not a web browser”);}
loadWithNewGlobal()
load() 函数会在当前 JavaScript 上下文中执行加载的脚本。而有时我们想把代码放入属于它自己的纯净上下文中。此时,可以使用 loadWithNewGlobal() 函数,在全新的全局上下文中执行脚本。
7. shebang句法
本节介绍的所有功能都是为了方便编写 shell 脚本,让 jjs 能替代 bash、Perl 或其他脚本语言。为了完善这种支持,最后还需要一个功能——“shebang”句法,用来启动使用 Nashorn 编写的脚本。
如果可执行脚本的第一行以
#!开头,而且后面是一个可执行文件的路径,那么 Unix 操作系统会假定这个路径指向一个解释器,而且这个解释器能处理这种脚本。执行脚本时,操作系统会启动指定的解释器,并把脚本文件传给解释器处理。
对 Nashorn 来说,最好创建一个符号链接(可能需要 sudo 访问),把 /usr/bin/jjs(或 /usr/local/bin/jjs)指向 jjs 的真正位置(通常是 $JAVA_HOME/bin/jjs)。然后,可以像下面这样编写 Nashorn shell 脚本:
#!/usr/bin/jjs# ……脚本中的其他内容
对更高级的应用场景来说(例如长时间运行的守护进程),Nashorn 甚至提供了对 Node.js 的支持。这个功能由 Avatar 项目中的 Avatar.js 提供,详情参见 12.5 节的“Avatar 项目”。
本节介绍的工具便于直接在命令行中执行 JavaScript 代码,不过多数情况下,我们希望使用另一种方式执行 JavaScript 代码——在 Java 程序中调用 Nashorn,执行 JavaScript 代码。实现这种执行方式的 API 包含在 Java 包 javax.script 中,所以,接下来我们要介绍这个包,并讨论 Java 如何与解释脚本语言的引擎交互。
如果有合适的脚本引擎,使用这个选项还能让 