Java-脚本编程教程-集成-Groovy-和-JS-三-

84 阅读23分钟

Java 脚本编程教程:集成 Groovy 和 JS(三)

协议:CC BY-NC-SA 4.0

八、实现脚本引擎

在本章中,您将学习:

  • 实现新脚本引擎时需要开发的脚本引擎组件
  • 如何实现一个简单脚本引擎的不同组件,该引擎将对两个数执行加、减、乘、除运算
  • 如何封装脚本引擎的代码
  • 如何部署和测试脚本引擎

介绍

实现一个成熟的脚本引擎不是一件简单的任务,这超出了本书的范围。本章旨在为您提供实现脚本引擎所需的设置的简要但完整的概述。在本节中,您将实现一个简单的脚本引擎,称为JKScript引擎。它将使用以下规则计算算术表达式:

  • 它将计算由两个操作数和一个运算符组成的算术表达式
  • 表达式可能有两个数字文字、两个变量,或者一个数字文字和一个变量作为操作数。数字文字必须是十进制格式。不支持十六进制、八进制和二进制数字文本
  • 表达式中的算术运算仅限于加、减、乘和除
  • 它会将+-*/识别为算术运算符
  • 引擎将返回一个Double对象作为表达式的结果
  • 可以使用引擎的全局范围或引擎范围绑定将表达式中的操作数传递给引擎
  • 它应该允许从一个String对象和一个java.io.Reader对象执行脚本。然而,一个Reader应该只有一个表达式作为它的内容
  • 它不会实现InvocableCompilable接口

使用这些规则,脚本引擎的一些有效表达式如下:

  • 10 + 90
  • 10.7 + 89.0
  • +10 + +90
  • num1 + num2
  • num1 * num2
  • 78.0 / 7.5

在实现脚本引擎时,您需要为以下两个接口提供实现:

  • javax.script.ScriptEngineFactory
  • javax.script.ScriptEngine

作为JKScript脚本引擎实现的一部分,你将开发表 8-1 中列出的三个类。在随后的部分中,您将开发这些类。

表 8-1。

The List of Classes to be Developed for the JKScript Script Engine

班级描述
ExpressionExpression类是脚本引擎的核心。它执行解析和评估算术表达式的工作。它在JKScriptEngine类的eval()方法中使用。
JKScriptEngine接口的一个实现。它扩展了实现ScriptEngine接口的AbstractScriptEngine类。AbstractScriptEngine类为ScriptEngine接口的eval()方法的几个版本提供了标准实现。您需要实现下面两个版本的eval()方法:Object eval(String, ScriptContext) Object eval(Reader, ScriptContext)
JKScriptEngineFactory接口的一个实现。

表达式类

Expression类包含解析和评估算术表达式的主要逻辑。清单 8-1 包含了Expression类的完整代码。

清单 8-1。分析和计算算术表达式的表达式类

// Expression.java

package com.jdojo.script;

import java.util.regex.Matcher;

import java.util.regex.Pattern;

import javax.script.ScriptContext;

public class Expression {

private String exp;

private ScriptContext context;

private String op1;

private char op1Sign = '+';

private String op2;

private char op2Sign = '+';

private char operation;

private boolean parsed;

public Expression(String exp, ScriptContext context) {

if (exp == null || exp.trim().equals("")) {

throw new IllegalArgumentException(this.getErrorString());

}

this.exp = exp.trim();

if (context == null) {

throw new IllegalArgumentException(        "ScriptContext cannot be null.");

}

this.context = context;

}

public String getExpression() {

return exp;

}

public ScriptContext getScriptContext() {

return context;

}

public Double eval() {

// Parse the expression

if (!parsed) {

this.parse();

this.parsed = true;

}

// Extract the values for the operand

double op1Value = getOperandValue(op1Sign, op1);

double op2Value = getOperandValue(op2Sign, op2);

// Evaluate the expression

Double result = null;

switch (operation) {

case '+':

result = op1Value + op2Value;

break;

case '-':

result = op1Value - op2Value;

break;

case '*':

result = op1Value * op2Value;

break;

case '/':

result = op1Value / op2Value;

break;

default:

throw new RuntimeException(        "Invalid operation:" + operation);

}

return result;

}

private double getOperandValue(char sign, String operand) {

// Check if operand is a double

double value;

try {

value = Double.parseDouble(operand);

return sign == '-' ? -value : value;

}

catch (NumberFormatException e) {

// Ignore it. Operand is not in a format that can be

// converted to a double value.

}

// Check if operand is a bind variable

Object bindValue = context.getAttribute(operand);

if (bindValue == null) {

throw new RuntimeException(operand +         " is not found in the script context.");

}

if (bindValue instanceof Number) {

value = ((Number) bindValue).doubleValue();

return sign == '-' ? -value : value;

}

else {

throw new RuntimeException(operand +         " must be bound to a number.");

}

}

public void parse() {

// Supported expressions are of the form v1 op v2, // where v1 and v2 are variable names or numbers,

// and op could be +, -, *, or /

// Prepare the pattern for the expected expression

String operandSignPattern = "([+-]?)";

String operandPattern = "([\\p{Alnum}\\p{Sc}_.]+)";

String whileSpacePattern = "([\\s]*)";

String operationPattern = "([+*/-])";

String pattern = "^" + operandSignPattern + operandPattern +

whileSpacePattern + operationPattern + whileSpacePattern +

operandSignPattern + operandPattern + "$";

Pattern p = Pattern.compile(pattern);

Matcher m = p.matcher(exp);

if (!m.matches()) {

// The expression is not in the expected format

throw new IllegalArgumentException(this.getErrorString());

}

// Get operand-1

String temp = m.group(1);

if (temp != null && !temp.equals("")) {

this.op1Sign = temp.charAt(0);

}

this.op1 = m.group(2);

// Get operation

temp = m.group(4);

if (temp != null && !temp.equals("")) {

this.operation = temp.charAt(0);

}

// Get operand-2

temp = m.group(6);

if (temp != null && !temp.equals("")) {

this.op2Sign = temp.charAt(0);

}

this.op2 = m.group(7);

}

private String getErrorString() {

return "Invalid expression[" + exp + "]" +

"\nSupported expression syntax is: op1 operation op2" +

"\n where op1 and op2 can be a number or a bind variable" +

" , and operation can be +, -, *, and /.";

}

@Override

public String toString() {

return "Expression: " + this.exp + ", op1 Sign = " +

op1Sign + ", op1 = " + op1 + ", op2 Sign = " +

op2Sign + ", op2 = " + op2 + ", operation = " + operation;

}

}

Expression类被设计用来解析和评估以下形式的算术表达式:

op1 operation op2

这里,op1op2是两个操作数,可以是十进制格式的数字或变量,operation可以是+-*/

建议使用的Expression类是:

Expression exp = new Expression(expression, scriptContext);

Double value = exp.eval();

让我们详细讨论一下Expression类的重要组件。

实例变量

名为expcontext的实例变量分别是表达式和对表达式求值的ScriptContext。它们被传递给该类的构造函数。

名为op1op2的实例变量分别代表表达式中的第一个和第二个操作数。实例变量op1Signop2Sign分别代表表达式中第一个和第二个操作数的符号,可以是“+”或“-”。当使用parse()方法解析表达式时,操作数及其符号被填充。

名为operation的实例变量表示要对操作数执行的算术运算(+-*/)。

名为parsed的实例变量用于跟踪表达式是否被解析。parse()方法将其设置为true

构造函数

Expression类的构造函数接受一个表达式和一个ScriptContext。它确保它们不是null,并将它们存储在实例变量中。在将表达式存储到名为exp的实例变量中之前,它会从表达式中删除前导空格和尾随空格。

parse()方法

parse()方法将表达式解析成操作数和操作。它使用正则表达式来解析表达式文本。正则表达式要求表达式文本采用以下形式:

  • 第一个操作数的可选符号+-
  • 第一个操作数,可以由字母数字、货币符号、下划线和小数点的组合组成
  • 任意数量的空格
  • 可能是+-*/的操作标志
  • 第二个操作数的可选符号+-
  • 第二个操作数,可以由字母数字、货币符号、下划线和小数点的组合组成

正则表达式([+-]?)将匹配操作数的可选符号。正则表达式([\\p{Alnum}\\p{Sc}_.]+)会匹配一个操作数,可能是十进制数,也可能是名字。正则表达式([\\s]*)将匹配任意数量的空格。正则表达式([+*/-])将匹配一个操作符。所有正则表达式都用括号括起来形成组,这样就可以捕获表达式的匹配部分。

如果一个表达式匹配正则表达式,parse()方法将匹配部分存储到各自的实例变量中。

注意,匹配操作数的正则表达式并不完美。它将允许几种无效的情况,比如一个操作数有多个小数点,等等。但是,对于本演示来说,这就足够了。

getOperandValue()方法

在表达式被解析后,在表达式求值期间使用getOperandValue()方法。如果操作数是一个double数,它通过应用操作数的符号返回值。否则,它会在ScriptContext中查找操作数的名称。如果在ScriptContext中没有找到操作数的名称,它抛出一个RuntimeException。如果在ScriptContext中找到操作数的名称,它将检查该值是否为数字。如果该值是一个数字,则在将符号应用于该值后返回该值;否则抛出一个RuntimeException

方法不支持十六进制、八进制和二进制格式的操作数。例如,像“0x2A + 0b1011”这样的表达式将不会被视为具有两个带int文字的操作数的表达式。读者可以增强这种方法,以支持十六进制、八进制和二进制格式的数字文字。

eval()方法

eval()方法计算表达式并返回一个double值。首先,如果表达式还没有被解析,它就解析它。注意,多次调用eval()只会解析表达式一次。

它获取两个操作数的值,执行运算,并返回表达式的值。

JKScriptEngine 类

清单 8-2 包含了JKScript脚本引擎的实现。其eval(String, ScriptContext)方法包含主逻辑,如图所示:

Expression exp = new Expression(script, context);

Object result = exp.eval();

它创建了一个Expression类的对象。它调用评估表达式并返回结果的Expression对象的eval()方法。

eval(ReaderScriptContext)方法从Reader中读取所有行,将它们连接起来,并将结果String传递给eval(String, ScriptContext)方法来计算表达式。注意一个Reader必须只有一个表达式。一个表达式可以拆分成多行。Reader中的空白被忽略。

清单 8-2。JKScript 脚本引擎的实现

// JKScriptEngine.java

package com.jdojo.script;

import java.io.BufferedReader;

import java.io.IOException;

import java.io.Reader;

import javax.script.AbstractScriptEngine;

import javax.script.Bindings;

import javax.script.ScriptContext;

import javax.script.ScriptEngineFactory;

import javax.script.ScriptException;

import javax.script.SimpleBindings;

public class JKScriptEngine extends AbstractScriptEngine {

private ScriptEngineFactory factory;

public JKScriptEngine(ScriptEngineFactory factory) {

this.factory = factory;

}

@Override

public Object eval(String script, ScriptContext context)

throws ScriptException {

try {

Expression exp = new Expression(script, context);

Object result = exp.eval();

return result;

}

catch (Exception e) {

throw new ScriptException(e.getMessage());

}

}

@Override

public Object eval(Reader reader, ScriptContext context) throws ScriptException {

// Read all lines from the Reader

BufferedReader br = new BufferedReader(reader);

String script = "";

String str = null;

try {

while ((str = br.readLine()) != null) {

script = script + str;

}

}

catch (IOException e) {

throw new ScriptException(e);

}

// Use the String version of eval()

return eval(script, context);

}

@Override

public Bindings createBindings() {

return new SimpleBindings();

}

@Override

public ScriptEngineFactory getFactory() {

return factory;

}

}

JKScriptEngineFactory 类

清单 8-3 包含了JKScript引擎的ScriptEngineFactory接口的实现。它的一些方法返回一个"Not Implemented"字符串,因为你不支持这些方法公开的特性。JKScriptEngineFactory类中的代码是不言自明的。使用ScriptEngineManager可以获得一个JKScript引擎的实例,其名称为jksJKScriptjkscript,如getNames()方法中编码的那样。

清单 8-3。JKScript 脚本引擎的 ScriptEngineFactory 实现

// JKScriptEngineFactory.java

package com.jdojo.script;

import java.util.ArrayList;

import java.util.Arrays;

import java.util.Collections;

import java.util.List;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineFactory;

public class JKScriptEngineFactory implements ScriptEngineFactory {

@Override

public String getEngineName() {

return "JKScript Engine";

}

@Override

public String getEngineVersion() {

return "1.0";

}

@Override

public List<String> getExtensions() {

return Collections.unmodifiableList(Arrays.asList("jks"));

}

@Override

public List<String> getMimeTypes() {

return Collections.unmodifiableList(Arrays.asList("text/jkscript") );

}

@Override

public List<String> getNames() {

List<String> names = new ArrayList<>();

names.add("jks");

names.add("JKScript");

names.add("jkscript");

return Collections.unmodifiableList(names);

}

@Override

public String getLanguageName() {

return "JKScript";

}

@Override

public String getLanguageVersion() {

return "1.0";

}

@Override

public Object getParameter(String key) {

switch (key) {

case ScriptEngine.ENGINE:

return getEngineName();

case ScriptEngine.ENGINE_VERSION:

return getEngineVersion();

case ScriptEngine.NAME:

return getEngineName();

case ScriptEngine.LANGUAGE:

return getLanguageName();

case ScriptEngine.LANGUAGE_VERSION:

return getLanguageVersion();

case "THREADING":

return "MULTITHREADED";

default:

return null;

}

}

@Override

public String getMethodCallSyntax(String obj, String m, String[] p) {

return "Not implemented";

}

@Override

public String getOutputStatement(String toDisplay) {

return "Not implemented";

}

@Override

public String getProgram(String[] statements) {

return "Not implemented";

}

@Override

public ScriptEngine getScriptEngine() {

return new JKScriptEngine(this);

}

}

准备部署

在为JKScript脚本引擎打包类之前,您需要再执行一个步骤:创建一个名为META-INF的目录。在META-INF目录下,创建一个名为services的子目录。在services目录下,创建一个名为javax.script.ScriptEngineFactory的文本文件。请注意,文件名必须是所提到的名称,并且不应该有任何扩展名,例如.txt

编辑javax.script.ScriptEngineFactory文件并输入如清单 8-4 所示的内容。文件中的第一行是以#号开头的注释。第二行是JKScript脚本引擎工厂类的完全限定名。

清单 8-4。名为 javax . script . scriptenginefactory 的文件的内容

#The factory class for the JKScript engine

com.jdojo.script.JKScriptEngineFactory

为什么一定要执行这一步?您将把javax.script.ScriptEngineFactory文件和JKScript引擎的类文件打包在一个 JAR 文件中。脚本引擎的发现机制在类路径的所有 JAR 文件的META-INF/services目录中搜索这个文件。如果找到该文件,将读取其内容,并且该文件中的所有脚本工厂类都将被实例化并包含在脚本引擎工厂列表中。因此,这一步对于让你的JKScript引擎被ScriptEngineManager自动发现是必要的。

打包 jscript 文件

您需要将JKScript脚本引擎的所有文件打包到一个名为jkscript.jar的 JAR 文件中。您也可以将该文件命名为任何其他名称。以下是文件及其目录的列表。请注意,在这种情况下,一个空的manifest.mf文件将起作用:

  • com\jdojo\script\Expression.class
  • com\jdojo\script\JKScriptEngine.class
  • com\jdojo\script\JKScriptEngineFactory.class
  • META-INF\manifest.mf
  • META-INF\services\javax.script.ScriptEngineFactory

您可以手动创建jkscript.jar文件,方法是将除了manifest.mf文件之外的所有这些文件复制到一个目录中,比如 Windows 上的C:\build,然后从C:\build目录执行以下命令:

C:\build> jar cf jkscript.jar com\jdojo\script\*.class META-INF\services\*.*

在本书的可下载包的src\JavaScripts目录中可以找到jkscript.jar文件。可下载的源代码包含一个 NetBeans 8.0 项目,并且jkscript.jar文件被添加到项目的类路径中。如果从附带的 NetBeans IDE 中运行引擎,则不需要执行打包和部署步骤来使用JKScript引擎。

使用 jscript 脚本引擎

是时候测试你的JKScript脚本引擎了。第一步也是最重要的一步是将您在上一节中创建的jkscript.jar包含到应用类路径中。一旦在应用类路径中包含了jkscript.jar文件,使用JKScript与使用任何其他脚本引擎没有什么不同。

下面的代码片段创建了一个使用JKScript作为名称的JKScript脚本引擎的实例。您也可以使用它的其他名称,如jksjkscript:

// Create the JKScript engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JKScript");

if (engine == null) {

System.out.println("JKScript engine is not available. " +

"Add jkscript.jar to CLASSPATH.");

}

else {

// Evaluate your JKScript

}

清单 8-5 包含一个程序,它使用JKScript脚本引擎来评估不同类型的表达式。执行存储在String对象和文件中的表达式。一些表达式使用数值和一些绑定变量,它们的值在引擎范围和引擎的默认ScriptContext的全局范围内的绑定中传递。注意,这个程序期望在当前目录中有一个名为jkscript.txt的文件,其中包含一个可以被JKScript脚本引擎理解的算术表达式。如果脚本文件不存在,程序将在标准输出中打印一条消息,其中包含预期脚本文件的路径。您可能会在最后一行得到不同的输出。

清单 8-5。使用 JKScript 脚本引擎

// JKScriptTest.java

package com.jdojo.script;

import java.io.FileNotFoundException;

import java.io.IOException;

import java.io.Reader;

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class JKScriptTest {

public static void main(String[] args) throws FileNotFoundException, IOException {

// Create JKScript engine

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("JKScript");

if (engine == null) {

System.out.println("JKScript engine is not available. " +

"Add jkscript.jar to CLASSPATH.");

return;

}

// Test scripts as String

testString(manager, engine);

// Test scripts as a Reader

testReader(manager, engine);

}

public static void testString(ScriptEngineManager manager, ScriptEngine engine) {

try {

// Use simple expressions with numeric literals

String script = "12.8 + 15.2";

Object result = engine.eval(script);

System.out.println(script + " = " + result);

script = "-90.0 - -10.5";

result = engine.eval(script);

System.out.println(script + " = " + result);

script = "5 * 12";

result = engine.eval(script);

System.out.println(script + " = " + result);

script = "56.0 / -7.0";

result = engine.eval(script);

System.out.println(script + " = " + result);

// Use global scope bindings variables

manager.put("num1", 10.0);

manager.put("num2", 20.0);

script = "num1 + num2";

result = engine.eval(script);

System.out.println(script + " = " + result);

// Use global and engine scopes bindings. num1 from

// engine scope and num2 from global scope will be used.

engine.put("num1", 70.0);

script = "num1 + num2";

result = engine.eval(script);

System.out.println(script + " = " + result);

// Try mixture of number literal and bindings. num1 // from the engine scope bindings will be used

script = "10 + num1";

result = engine.eval(script);

System.out.println(script + " = " + result);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

public static void testReader(ScriptEngineManager manager, ScriptEngine engine) {

try {

Path scriptPath = Paths.get("jkscript.txt").toAbsolutePath();

if (!Files.exists(scriptPath)) {

System.out.println(scriptPath +

" script file does not exist.");

return;

}

try(Reader reader = Files.newBufferedReader(scriptPath);) {

Object result = engine.eval(reader);

System.out.println("Result of " +

scriptPath + " = " + result);

}

}

catch(ScriptException | IOException e) {

e.printStackTrace();

}

}

}

12.8 + 15.2 = 28.0

-90.0 - -10.5 = -79.5

5 * 12 = 60.0

56.0 / -7.0 = -8.0

num1 + num2 = 30.0

num1 + num2 = 90.0

10 + num1 = 80. 0

Result of C:\jkscript.txt = 190.0

摘要

您可以使用 Java Script API 实现脚本引擎。您需要为ScriptEngineScriptEngineFactory接口提供实现。您需要以某种方式打包您的脚本引擎代码,这样引擎就可以在运行时被ScriptManager发现。引擎的 JAR 文件应该包含一个名为META-INF\services\javax.script.ScriptEngineFactory的文件,该文件应该包含所有脚本引擎工厂类的全限定名称;Java Script API 会自动发现这些脚本引擎工厂。一旦打包并部署了脚本引擎代码,就可以像访问 Nashorn 和其他脚本引擎一样访问它。

九、jrunscript命令行 Shell

在本章中,您将学习:

  • 什么是jrunscript命令行 shell
  • 如何调用jrunscript命令行 shell
  • 调用jrunscript命令行外壳的不同模式
  • 如何用jrunscript命令行 shell 列出可用的脚本引擎
  • 如何向jrunscript命令行外壳添加脚本引擎
  • 如何向jrunscript命令行 shell 传递参数
  • jrunscript命令行 shell 提供的全局函数

JDK 包括一个名为jrunscript的命令行脚本外壳。它独立于脚本引擎,可以用来评估任何脚本,包括你在第八章的中开发的JKScript。您可以在JAVA_HOME\bin目录中找到这个 shell,其中JAVA_HOME是您安装 JDK 的目录。在这一节中,我将讨论如何使用jrunscript shell 来评估使用不同脚本引擎的脚本。

语法

使用jrunscript shell 的语法是:

jrunscript [options] [arguments]

[options][arguments]都是可选的。但是,如果两者都指定了,[options]必须在[arguments]之前。表 9-1 列出了jrunscript外壳的所有可用选项。

表 9-1。

The List of Options for the jrunscript Shell

[计]选项描述
-classpath <path>用于指定类路径。
-cp <path>-classpath选项相同。
-D<name>=<value>为 Java 运行时设置系统属性。
-J<flag>将指定的<flag>传递给运行jrunscript的 JVM。
-l <language>允许您指定一种您想与jrunscript一起使用的脚本语言。默认情况下,Rhino JavaScript 用于 JDK 6 和 JDK 7。在 JDK 8 中,Nashorn 是默认设置。如果您想使用 JavaScript 之外的语言,比如说JKScript,您将需要使用-cp-classpath选项来包含包含脚本引擎的 JAR 文件。
-e <script>执行指定的脚本。通常,它用于执行一行脚本。
-encoding <encoding>指定读取脚本文件时使用的字符编码。
-f <script-file>以批处理模式评估指定的script-file
-f -允许您在交互模式下评估脚本。它从标准输入中读取脚本并执行它。
-help输出帮助消息并退出。
-?输出帮助消息并退出。
-q列出所有可用的脚本引擎和出口。请注意,除了 JavaScript 之外的脚本引擎只有在您使用-cp-classpath选项包含它们的 JAR 文件时才可用。

命令的[arguments]部分是一个参数列表,根据是否使用了-e-f选项来解释。传递给脚本的参数在脚本中作为一个名为arguments的对象存在。关于在脚本中使用arguments对象的更多细节,请参考第四章。表 9-2 列出了与-e-f选项一起使用时参数的解释。

表 9-2。

Interpretation of [arguments] in Combination of the -e or -f Option

-e 或-f 选项争论解释
YesYes如果指定了-e-f选项,所有参数都将作为脚本参数传递给脚本。
NoYes如果参数没有指定-e-f选项,第一个参数被认为是要运行的脚本文件。其余的参数(如果有)作为脚本参数传递给脚本。
NoNo如果缺少参数和-e-f选项,shell 将在交互模式下工作,以交互方式执行在标准输入中输入的脚本。

外壳的执行模式

您可以在以下三种模式下使用jrunscript shell:

  • 单行模式
  • 成批处理方式
  • 对话方式

单行模式

-e选项允许您在一行模式下使用 shell。它执行一行脚本。以下命令使用 Nashorn 引擎在标准输出上打印一条消息:

C:\>jrunscript -e "print('Hello Nashorn!');"

Hello Nashorn!

C:\>

在单行模式下,整个脚本必须在一行中输入。但是,一行脚本可能包含多条语句。

成批处理方式

-f选项允许您在批处理模式下使用 shell。它执行一个脚本文件。考虑一个名为jrunscripttest.js的脚本文件,如清单 9-1 所示。

清单 9-1。用 Nashorn JavaScript 编写的 jrunscripttest.js 脚本文件

// jrunscripttest.js

// Print a message

print("Hello Nashorn!");

// Add two integers and print the value

var x = 10;

var y = 20;

var z = x + y;

printf("x + y = z", x, y, z);

以下命令以批处理模式运行jrunscripttest.js文件中的脚本。如果jrunscripttest.js文件不在当前目录中,您可能需要指定它的完整路径。

C:\>jrunscript -f jrunscripttest.js

Hello Nashorn!

10 + 20 = 30

C:\>

对话方式

在交互模式下,shell 读取并评估在标准输入上输入的脚本。有两种方法可以在交互模式下使用 shell:

  • 不使用-e-f选项,也不使用参数
  • 使用“-f -”选项

以下命令不使用任何选项和参数来进入交互模式。按 Enter 键会让 shell 评估输入的脚本。请注意,您需要执行exit()quit()功能来退出交互模式:

c:\>jrunscript

nashorn> print("Hello Interactive mode!");

Hello Interactive mode!

nashorn> var num = 190;

nashorn> print("num is " + num);

num is 190

nashorn> exit();

C:\>

列出可用的脚本引擎

jrunscript shell 是一个脚本语言中立的 shell。您可以使用它来运行脚本引擎 JAR 文件可用的任何脚本语言的脚本。缺省情况下,Nashorn JavaScript 引擎是可用的。要列出所有可用的脚本引擎,请使用如下所示的-q选项:

c:\>jrunscript -q

Language ECMAScript ECMA - 262 Edition 5.1 implementation "Oracle Nashorn" 1.8.0_05

请参考下一节如何将脚本引擎添加到 shell 中。

向外壳添加脚本引擎

如何让脚本引擎而不是 Nashorn 引擎对 shell 可用?要使脚本引擎对 shell 可用,您需要使用-classpath-cp选项为脚本引擎提供 JAR 文件列表。以下命令通过为JythonJKScript引擎提供 JAR 文件列表,使JKScriptjython脚本引擎对 shell 可用。请注意,默认情况下,Nashorn 引擎始终可用。该命令使用-q选项列出所有可用的脚本引擎:

c:\> jrunscript -cp C:\jython-standalone-2.5.3.jar;C:\jkscript.jar -q

Language python 2.5 implementation "jython" 2.5.3

Language ECMAScript ECMA - 262 Edition 5.1 implementation "Oracle Nashorn" 1.8.0_05

Language JKScript 1.0 implementation "JKScript Engine" 1.0

Tip

使用-cp-classpath选项设置的类路径仅对使用该选项的命令有效。如果在交互模式下运行 shell,则类路径对整个交互会话都有效。

使用其他脚本引擎

您可以通过使用-l选项指定脚本引擎名称来使用其他脚本引擎。您必须使用-cp-classpath选项为脚本引擎指定 JAR 文件,这样 shell 就可以访问引擎。以下命令在交互模式下使用JKScript引擎:

C:\>jrunscript -cp C:\jkscript.jar -l JKScript

jks> 10 + 30

40.0

jks> +89.7 + -9.7

80.0

jks>

向脚本传递参数

jrunscript shell 允许向脚本传递参数。这些参数在一个名为arguments的类似数组的对象中对脚本可用。您可以用特定于语言的方式访问脚本中的arguments数组。以下命令传递三个参数 10、20 和 30,并打印第一个参数的值:

C:\>jrunscript -e "print('First argument is ' + arguments[0])" 10 20 30

First argument is 10

考虑清单 9-2 所示的 Nashorn JavaScript 文件nashornargstest.js,它打印了传递给脚本的参数数量及其值。

清单 9-2。用 Nashorn JavaScript 编写的 nashornargstest.js 文件,用于打印命令行参数

// nashornargstest.js

print("Number of arguments:" + arguments.length);

print("Arguments are ") ;

for(var i = 0; i < arguments.length; i++) {

print(arguments[i]);

}

以下命令使用jrunscript shell 运行nashornargstest.js文件:

C:\>jrunscript nashornargstest.js

Number of arguments:0

Arguments are

C:\>jrunscript nashornargstest.js 10 20 30

Number of arguments:3

Arguments are

10

20

30

如果您想从 Java 应用运行nashornargstest.js文件,您需要向引擎传递一个名为arguments的参数。名为arguments的参数由 shell 自动传递给脚本,而不是由 Java 应用传递。

全局函数

jrunscript命令行 shell 使几个全局函数可供使用,如表 9-3 中所列。

表 9-3。

The List of Global Objects Loaded by the jrunscript Command-Line Shell

功能描述
cat(path, pattern)显示由path指定的文件、URL 或 InputStream 的内容。或者,您可以指定pattern只显示匹配的内容。
cd(target)将当前工作目录更改为target目录。
cp(from, to)将文件、URL 或流复制到另一个文件或流。
date()使用当前区域设置打印当前日期。
del(pathname)rm命令的同义词。
dir(d, filter)ls命令的同义词。
dirname(pathname)返回指定pathname的目录部分。
echo(str)回显指定的字符串参数。
exec(cmd)启动子进程,执行指定的命令,等待完成,并返回退出代码。
exit(code)用指定的code作为退出代码退出 shell 程序。
find(dir, pattern, callback)dir中查找文件名与指定的pattern匹配的文件。当找到匹配时,调用callback函数传递找到的文件。搜索在所有子目录中递归执行。您可以将此表中列出的一些函数作为callback进行传递。如果没有指定callback,默认是打印找到的文件路径。如果未指定pattern,则打印所有文件。
grep(pattern, files)类似 Unix 的grep,但是接受 JavaScript regex 模式。
ip(name)打印给定域名的 IP 地址。
load(path)从流、文件或 URL 加载并计算 JavaScript 代码。
ls(dir, filter)列出dir中与filter正则表达式匹配的文件。
mkdir(dir)创建一个名为dir的新目录。
mkdirs(dir)创建一个名为dir的目录,包括任何必要但不存在的父目录。
mv(from, to)将文件移动到另一个目录。
printf(format, args)一个类似 C 的 printf。
pwd()打印工作目录。
quit(code)exit(code)的同义词。
read(prompt, multiline)打印指定的prompt后,从标准输入中读取并返回一行或多行。默认提示是一个>。如果multiline为 0,则读取一行。如果multiline不为 0,则读取多行。你需要按下Enter来停止输入文本。
ren(from, to)mv的同义词。
rm(filePath)删除带有指定filePath的文件。
rmdir(dirPath)用指定的dirPath删除目录。
which(cmd)基于 path 环境变量打印指定的cmd命令的路径。
XMLDocument(input)将可以是文件路径或Readerinput转换为 DOM 文档对象。如果没有指定input,则返回一个空的 DOM 文档。
XMLResult(input)将任意流或文件转换为XMLResult。如果inputjavax.xml.transform.Result的实例,则返回input;如果inputorg.w3c.dom.Document的实例,则返回一个javax.xml.transform.dom.DOMResult;否则,返回一个javax.xml.transform.stream.StreamResult
XMLSource(input)将任意流、文件、URL 转换为XMLSource。如果inputjavax.xml.transform.Source的实例,则返回input;如果inputorg.w3c.dom.Document的实例,则返回一个javax.xml.transform.dom.DOMSource;否则,返回一个javax.xml.transform.stream.StreamSource
XSLTransform(input, style, output)执行 XSLT 转换;input是输入的 XMLstyle是 XML 样式表;output是输出 XML。Inputstyle可以是URLFileInputStream;输出可以是一个File或一个OutputStream

以下是使用jrunscript提供的一些实用函数的输出:

C:\>jrunscript

nashorn> cat("http://jdojo.com/about

68      : <p>You can contact Kishori Sharan by email at <a href="``mailto:ksharan@jdojo.com``">``ksharan@jdojo.com

nashorn> var addr = read("Please enter your address: ", 1);

Please enter your address: 9999 Main St.

Please enter your address: Dreamland, HH 11111

Please enter your address:

nashorn> print(addr)

9999 Main St.

Dreamland, HH 11111

nashorn> which("jrunscript.exe");

c:\JAVA8\BIN\jrunscript.exe

nashorn>pwd()

C:\

nashorn>

这些实用函数中的大部分都是利用 Java 类库作为 Nashorn 脚本编写的。了解这些函数如何工作的最好方法是阅读源代码。您可以通过在nashorn命令提示符下输入函数名来打印非本地函数的源代码。以下命令序列向您展示了如何打印exec(cmd)函数的源代码。输出显示该函数在内部使用 Java Runtime类来运行命令:

c:\>jrunscript

nashorn> exec

function exec(cmd) {

var process = java.lang.Runtime.getRuntime().exec(cmd);

var inp = new DataInputStream(process.getInputStream());

var line = null;

while ((line = inp.readLine()) != null) {

println(line);

}

process.waitFor();

$exit = process.exitValue();

}

nashorn> exit()

c:\>

还有另外三个由jrunscript提供的全局函数值得一提。这些函数可以用作函数和构造函数:

  • jlist(javaList)
  • jmap(javaMap)
  • JSInvoker(object)

jlist()函数接受java.util.List的一个实例,并返回一个 JavaScript 对象,您可以用它来访问List,就像它是一个数组一样。您可以使用带索引的括号符号来访问List的元素。返回的对象包含一个length属性,它给出了List的大小。清单 9-3 包含了显示如何使用jlist()函数的代码。

清单 9-3。使用 jlist()函数

// jlisttest.js

// Create an ArrayList and add two elements to it

var ArrayList = Java.type("java.util.ArrayList");

var list = new ArrayList();

list.add("Ken");

list.add("Li");

// Convert the ArrayList into a Nashorn array

var names = jlist(list);

print("Accessing an ArrayList as a Nashorn array...");

for(var i = 0; i < names.length; i++) {

printf("names[%d] = %s", i, names[i]);

}

下面的命令使用jrunscript命令行 shell 执行清单 9-3 中的代码:

C:\>jrunscript -f jlisttest.js

Accessing an ArrayList as a Nashorn array...

names[0] = Ken

names[1] = Li

jmap()函数接受java.util.Map的一个实例,并返回一个 JavaScript 对象,您可以用它来访问MapMap中的键成为 JavaScript 对象的属性。清单 9-4 包含了显示如何使用jmap()函数的代码。

清单 9-4。使用 jmap()函数

// jmaptest.js

// Create an HashMap and add two elements to it

var HashMap = Java.type("java.util.HashMap");

var map = new HashMap();

map.put("Ken", "(999) 777-3331");

map.put("Li", "(888) 444-1111");

// Convert the HashMap into a Nashorn object

var phoneDir = jmap(map);

print("Accessing a HashMap as a Nashorn object...");

for(var prop in phoneDir) {

printf("phoneDir['%s'] = %s", prop, phoneDir[prop]);

}

// Use dot notation to access the proeprty

var kenPhone = phoneDir.Ken; // Same as phoneDir["Ken"]

printf("phoneDir.Ken = %s", kenPhone)

下面的命令使用jrunscript命令行 shell 执行清单 9-4 中的代码:

C:\>jrunscript -f jmaptest.js

Accessing a HashMap as a Nashorn object...

phoneDir['Ken'] = (999) 777-3331

phoneDir['Li'] = (888) 444-1111

phoneDir.Ken = (999) 777-3331

JSInvoker()函数接受一个委托对象作为参数。当在JSInvoker对象上调用一个函数时,在委托对象上调用invoke(name, args)方法。被调用的函数名作为第一个参数传递给invoke()方法;传递给函数调用的参数作为第二个参数传递给invoke()方法。清单 9-5 显示了如何使用JSInvoker对象。

清单 9-5。使用 JSInvoker 对象

// jsinvokertest.js

var calcDelegate = { invoke: function(name, args) {

if (args.length !== 2) {

throw new Error("Must pass 2 arguments to " + name);

}

var value = 0;

if (name === "add")

value = args[0] + args[1];

else if (name === "subtract")

value = args[0] - args[1];

else if (name === "multiply")

value = args[0] * args[1];

else if (name === "divide")

value = args[0] / args[1];

else

throw new Error("Operation " + name + " not supported.");

return value;

}

};

var calc = new JSInvoker(calcDelegate);

var x = 20.44, y = 30.56;

var addResult = calc.add(x, y); // Will call calcDelegate.invoke("add", [x, y])

var subResult = calc.subtract(x, y);

var mulResult = calc.multiply(x, y);

var divResult = calc.divide(x, y);

printf("calc.add(%.2f, %.2f) = %.2f%n", x, y, addResult);

printf("calc.sub(%.2f, %.2f) = %.2f%n", x, y, subResult);

printf("calc.mul(%.2f, %.2f) = %.2f%n", x, y, mulResult);

printf("calc.div(%.2f, %.2f) = %.2f", x, y, divResult);

代码创建了一个名为calcDelegate的对象,它包含一个invoke()方法。JSInvoker对象包装了calcDelegate对象。当在calc对象上调用一个函数时,calcDelegate对象的invoke()方法被调用,函数名作为第一个参数,函数参数作为第二个参数以数组的形式传递。invoke()函数对参数执行加、减、乘、除操作。以下命令显示了如何执行代码:

c:\>jrunscript -f jsinvokertest.js

calc.add(20.44, 30.56) = 51.00

calc.sub(20.44, 30.56) = -10.12

calc.mul(20.44, 30.56) = 624.65

calc.div(20.44, 30.56) = 0.67

JSInvoker对象可以在 Java 7 中工作,但是当您运行这个例子时,会在 Java 8 中产生以下错误。好像是 Java 8 引入的 bug:

c:\>jrunscript -f jsinvokertest.js

script error in file jsinvoker.js : TypeError: [object JSAdapter] has no such function "add" in jsinvoker.js at line number 25

jrunscript shell 还为几个 Java 类创建了别名,比如java.io.File、j ava.io.Reader、j ava.net.URL等等,所以您可以通过它们的简单名称来引用它们。其他几个对象也被jrunscript曝光为全局对象。您可以使用以下命令在命令行上打印全局对象及其类型的完整列表。仅显示了部分输出。注意,输出还将包括一个名为p的属性,这是在for循环中声明的变量名。

c:\>jrunscript

nashorn> for(var p in this) print(p, typeof this[p]);

engine object

JSInvoker function

jmap function

jlist function

inStream function

outStream function

streamClose function

javaByteArray function

pwd function

...

nashorn>exit()

c:\

摘要

JDK 包括一个名为jrunscript的独立于脚本引擎的命令行 shell。它可用于评估在命令行或从文件中输入的脚本。您可以在JAVA_HOME\bin目录中找到这个 shell,其中JAVA_HOME是您安装 JDK 的目录。

jrunscript命令行 shell 可以运行用 Java 支持的任何脚本语言编写的脚本。默认情况下,它运行 Nashorn 脚本。要使用 Nashorn 之外的脚本语言,您需要使用–cp–classpath选项将该语言的 JAR 文件包含在jrunscript中。–l选项让您选择想要使用的脚本语言。

您可以在一行程序模式、批处理模式和交互模式下使用jrunscript。单行模式允许您执行一行脚本。使用–e选项调用一行程序模式。批处理模式允许您执行存储在文件中的脚本。使用–f选项调用批处理模式。交互模式允许您以交互方式执行在命令行上输入的脚本。不使用–e–f选项,或者使用–f –选项(注意–f后的)调用交互模式。

您可以使用–q选项用jrunscript列出所有可用的脚本引擎。注意,您必须包含除 Nashorn 之外的语言的脚本引擎的 JAR 文件,以使它们在jrunscript中可用。jrunscript shell 提供了几个有用的全局函数和对象。例如,cat()函数可以用来打印一个文件或 URL 的内容,可选地应用一个过滤器。

十、jjs命令行工具

在本章中,您将学习:

  • 什么是jjs命令行工具
  • 如何调用jjs命令行工具
  • 如何向jjs命令行工具传递参数
  • 如何在交互和脚本模式下使用jjs命令行工具

为了配合 Nashorn 脚本引擎,JDK 8 包含了一个名为jjs的新命令行工具。如果你想知道jjs代表什么,它代表 Java JavaScript。该命令位于JDK_HOME\bin目录下。该命令可用于运行文件中的脚本或以交互模式在命令行上输入的脚本。它还可以用于执行 shell 脚本。

语法

调用该命令的语法是:

jjs [options] [script-files] [-- arguments]

这里:

  • [options]jjs命令的选项。多个选项由空格分隔
  • [script-files]是由 Nashorn 引擎解释的脚本文件列表
  • [-- arguments]是作为参数传递给脚本或交互式 shell 的参数列表。参数在双连字符后指定,可以使用脚本中的arguments属性访问它们

选项

jjs工具支持几个选项。表 10-1 列出了jjs工具的所有选项。一些选项有一个以同样方式工作的变体;例如,选项-classpath-cp是同义词。两者都用于设置类路径。请注意,有些选项以两个连字符开头。要打印所有选项的列表,在命令提示符下运行带有–xhelp选项的工具:

jjs –xhelp

表 10-1。

Options for the jjs Comand-Line Tool

[计]选项描述
-D<name>=<value>为 Java 运行时设置系统属性。可以重复使用该选项来设置多个运行时属性值。
-ccs=<size> --class-cache-size=<size>设置类缓存大小(以字节为单位)。默认情况下,类缓存大小设置为 50 字节。您可以在 size 中使用 k/K、m/M 和 g/G 来表示 KB、MB 和 GB 大小。选项-css=200-css=2M分别将类缓存大小设置为 200 字节和 2 MB。
-classpath <path> -cp <path>指定类路径。
-co --compile-only编译脚本而不运行。默认情况下,它是禁用的,这意味着脚本被编译和运行。
--const-as-var用关键字var替换脚本中的关键字const。如果脚本使用了 Nashorn 无法识别的const关键词,则此选项可用。默认情况下,它是禁用的。
-d=<path> --dump-debug-dir=<path>指定为脚本转储类文件的目标目录。
--debug-lines.class文件中生成行号表。默认情况下,它是启用的。指定--debug-lines=false禁用该功能。
--debug-locals.class文件中生成一个本地变量表。默认设置为false
-doe ––dump-on-error如果指定了此选项,将打印错误的完整堆栈跟踪。默认情况下,会打印一条简短的错误消息。
--early-lvalue-error解析代码时,将无效的lvalue表达式报告为早期错误。默认设置为true。如果设置为 false,则在执行代码时会报告无效的左值表达式。
--empty-statements在 Java 抽象语法树(AST)中保留空语句。默认设置为false
-fv –fullversion打印 Nashorn 引擎的完整版本。
--function-statement-error当函数声明用作语句时,打印错误信息。默认设置为false
--function-statement-warning当函数声明用作语句时,打印警告消息。默认设置为false
-fx将脚本作为 JavaFX 应用启动。
--global-per-engine每个脚本引擎实例使用一个全局实例。默认为false
-help -h输出帮助消息并退出。
-J<flag>将指定的<flag>传递给 JVM。
-language指定 ECMAScript 语言版本。有效值为es5es6。默认是es5
--lazy-compilation通过不一次性编译整个脚本来启用延迟代码生成策略。默认为true
--loader-per-compile每次编译创建一个新的类装入器。默认为true
-l --locale设置脚本执行的区域设置。默认为en-US
--log=subsystem:lebel为给定数量的子系统启用给定级别的日志记录,例如--log=fields:finest,codegen:info。多个子系统的日志记录用逗号分隔。
-nj --no-java禁用 Java 支持。缺省值是false,意味着允许在脚本中使用 Java 类。
-nse --no-syntax-extensions不允许非标准语法扩展。默认为false
-nta --no-typed-arrays禁用类型化数组支持。默认为false
--optimistic-types使用乐观类型假设,并取消优化重新编译。默认为true
--parse-only分析代码而不编译。默认为false
-pcc --persistent-code-cache为编译的脚本启用磁盘缓存。默认为false
--print-ast打印抽象语法树。默认为false
-pc --print-code将生成的字节码打印到标准错误或指定的目录。您可以指定打印字节码的函数名。指定目录和功能的语法是:-pc=dir:<output-directory-path>,function:<function-name>
--print-lower-ast打印降低的抽象语法树。默认为false
-plp --print-lower-parse打印降低的解析树。默认为false
--print-mem-usage打印每个编译阶段后指令寄存器(IR)的内存使用情况。默认为false
--print-no-newlineprint()函数在打印其参数后不会打印换行符。默认为false
-pp --print-parse打印解析树。默认为false
--print-symbols打印符号表。默认为false
-pcs --profile-callsites转储调用点配置文件数据。默认为false
-scripting启用外壳脚本功能。默认为false
--stderr=<filename&#124;stream&#124;tty>stderr重定向到指定的文件名、流或文本终端。
--stdout=<filename&#124;stream&#124;tty>stdout重定向到指定的文件名、流或文本终端。
-strict启用使用 ECMAScript 版标准执行脚本的严格模式。默认为false
-t=<timezone> –timezone=<timezone>设置脚本执行的时区。默认时区是芝加哥/美国。
-tcs=<option> --trace-callsites=<option>启用调用点跟踪模式。有效选项有miss(跟踪调用点未命中)、enterexit(跟踪调用点进入/退出)和objects(打印对象属性)。指定多个选项,用逗号分隔:-tcs=miss,enterexit,objects
--verify-code在运行之前验证字节码。默认为false
-v –version打印 Nashorn 引擎的版本。默认为false
-xhelp打印扩展帮助。默认为false

在交互模式下使用 jjs

如果在没有指定任何选项或脚本文件的情况下运行jjs,它将以交互模式运行。当您输入脚本时,它会被解释。回想一下,Nashorn 中的字符串可以用单引号或双引号括起来。

以下是在交互模式下使用jjs工具的一些例子。假设您已经在机器的 path 环境变量中包含了jjs工具的路径。如果您没有这样做,您可以在下面的命令中用JDK_HOME\bin\jjs替换jjs。记得执行quit()exit()功能退出jjs工具:

c:\>jjs

jjs> "Hello Nashorn"

Hello Nashorn

jjs> "Hello".toLowerCase();

hello

jjs> var list = [1, 2, 3, 4, 5]

jjs> var sum = 0;

jjs> for each (x in list) { sum = sum + x};

15

jjs> quit()

c:\>

向 jjs 传递参数

下面是一个向jjs工具传递参数的例子。前五个自然数作为参数传递给jjs工具,稍后使用arguments属性访问它们。请注意,您必须在两个连字符和第一个参数之间添加一个空格:

c:\>jjs -- 1 2 3 4 5

jjs> for each (x in``arguments

1

2

3

4

5

jjs> quit()

c:\>

考虑清单 10-1 中的脚本。该脚本已保存在名为stream.js的文件中。该脚本处理整数列表。该列表可以作为命令行参数传递给脚本。如果列表没有作为参数传递,它使用前五个自然数作为列表。它计算列表中奇数整数的平方和。它打印列表和总数。

清单 10-1。计算列表中奇数的平方和的脚本

// stream.js

var list;

if (arguments.length == 0) {

list = [1, 2, 3, 4, 5];

}

else {

list = arguments;

}

print("List of numbers: " + list);

var sumOfSquaredOdds = list.filter(function(n) {return n % 2 == 1;})

.map(function(n) {return n * n;})

.reduce(function(sum, n) {return sum + n;}, 0);

print("Sum of the squares of odd numbers: " + sumOfSquaredOdds);

使用jjs工具,您可以如下运行stream.js文件中的脚本。假设stream.js文件在当前目录中。否则,您需要指定文件的完整路径:

c:\>jjs stream.js

List of numbers: 1,2,3,4,5

Sum of the squares of odd numbers: 35

c:\>jjs stream.js -- 10 11 12 13 14 15

List of numbers: 10,11,12,13,14,15

Sum of the squares of odd numbers: 515

c:\>

在脚本模式下使用 jjs

可以在脚本模式下调用jjs工具,这允许您运行 shell 命令。您可以使用–scripting选项在脚本模式下启动jjs工具。shell 命令用反引号括起来,而不是单引号/双引号。以下是在脚本模式下使用jjs工具使用datels shell 命令的示例:

c:\>jjs -scripting

jjs> date``

Wed Oct 15 15:27:07 CDT 2014

jjs> ls -l``

total 3102

drwxr-xr-x  4 ksharan Administrators       0 Jan 11  2014 $AVG

drwxr-xr-x  5 ksharan Administrators       0 Jan 22  2014 $Recycle.Bin

-rw-r--r--  1 ksharan Administrators       1 Jun 18  2013 BOOTNXT

-rw-r--r--  1 ksharan Administrators      94 May 23  2013 DBAR_Ver.txt

More output goes here...

jjs> exit()

c:\>

Nashorn 在脚本模式下定义了几个全局对象和函数,如表 10-2 所列。

表 10-2。

Global Objects and Functions Available in Scripting Mode

全局对象描述
$ARG存储传递给脚本的参数。与arguments的工作方式相同。
$ENV将所有环境变量映射到一个对象。
$EXEC(cmd, input)用于在新进程中运行命令的全局函数,将input传递给cmd。两个参数都可以是命令,在这种情况下,input的输出将作为输入 Io cmd传递。
$OUT存储流程的最新标准输出。例如,执行$EXEC()的结果保存在$OUT中。
$ERR存储流程的最新标准输出。
$EXIT存储进程的退出代码。非零值表示进程失败。
echo(arg1, arg2,...)echo()函数的工作原理与print()函数相同,但它仅在脚本模式下可用。
readLine(prompt)从标准输入中读取一行输入。指定的参数显示为提示。默认情况下,读取输入显示在标准输出上。该函数返回读取的输入。
readFully(filePath)读取指定文件的全部内容。默认情况下,内容显示在标准输出中。您可以将函数的返回值赋给变量。

以下脚本显示了如何使用$ARG全局对象:

c:\>jjs -scripting -- 10 20 30

jjs> for each(var arg in $ARG) print(arg);

10

20

30

jjs>

以下脚本显示了如何使用$ENV全局对象。它在 Windows 上打印 OS 环境变量的值,并列出所有环境变量:

jjs> print($ENV.OS);

Windows_NT

jjs> for(var x in $ENV) print(x);

LOCALAPPDATA

PROCESSOR_LEVEL

FP_NO_HOST_CHECK

USERDOMAIN

LOGONSERVER

PROMPT

OS

...

以下脚本使用$EXEC()全局函数列出所有扩展名为txt的文件,其中包含ksharan:

jjs> $EXEC("grep -l ksharan *.txt");

test.txt

您可以在变量中捕获 shell 命令的输出。脚本模式允许在双引号括起来的字符串中进行表达式替换。请注意,表达式替换功能在单引号中的字符串中不可用。表达式被指定为${expression}。以下命令捕获变量中的date shell 命令的值,并使用表达式替换将日期值嵌入到字符串中。请注意,在示例中,当字符串用单引号括起来时,表达式替换不起作用:

c:\ >jjs -scripting

jjs> var today = date``

jjs> "Today is ${today}"

Today is Mon Jul 14 22:48:26 CDT 2014

jjs> 'Today is ${today}'

Today is ${today}

jjs> quit()

c:\>

您还可以使用脚本模式执行存储在文件中的 shell 脚本:

C:\> jjs –scripting myscript.js

jjs工具支持可以在脚本模式下运行的脚本文件中的 heredocs。heredoc 也称为 here 文档、here 字符串或 here 脚本。这是一个多行字符串,其中保留了空格。一个 heredoc 以一个双尖括号(< <)和一个定界标识符开始。通常,EOFEND被用作定界标识符。但是,您可以使用脚本中其他地方没有用作标识符的任何其他标识符。多行字符串从最后一行开始。字符串以相同的定界标识符结尾。以下是在 Nashorn 中使用 heredoc 的示例:

var str = <<EOF

This is a multi-line string using the heredoc syntax.

Bye Heredoc!

EOF

清单 10-2 包含了在 Nashorn 中使用 heredoc 的脚本。$ARG属性仅在脚本模式下定义,其值是使用jjs工具传递给脚本的参数:

清单 10-2。使用 heredoc 样式的 heredoc.js 文件的内容是一个多行字符串

// heredoc.js

var str = <<EOF

This is a multiline string.

Number of arguments passed to this

script is ${$ARG.length}

Arguments are ${$ARG}

Bye Heredoc!

EOF

print(str);

您可以执行如下所示的heredoc.js脚本文件:

c:\> jjs -scripting heredoc.js

This is a multi-line string.

Number of arguments passed to this

script is 0

Arguments are

Bye Heredoc!

c:\> jjs -scripting heredoc.js -- Kishori Sharan

This is a multi-line string.

Number of arguments passed to this

script is 2

Arguments are Kishori,Sharan

Bye Heredoc!

除了 Nashorn 支持的两种注释样式(///* */)之外,jjs工具还支持一种额外的以数字符号(#)开头的单行注释样式。如果jjs工具运行的脚本文件以符号#开头,jjs工具会自动启用脚本模式,并在脚本模式下执行整个脚本。考虑清单 10-3 所示的脚本。

清单 10-3。jjs 工具的特殊注释。内容存储在 jjscomments.js 文件中

# This script will run in scripting mode by the jjs tool

# because it starts with a number sign

// Set the current directory to C:\kishori

$ENV.PWD = "C:\\kishori";

// Get the list of files and directories in the current directory

var str = ls -F;

print(str);

以下命令运行jjscomments.js文件中的脚本。脚本以#符号开始,因此jjs工具将自动启用脚本模式:

c:\>jjs jjscomments.js

books/

ejb/

hello.txt

important/

programs/

rmi.log

rmi.policy

scripts/

c:\>

使用jjs工具将带有第一个#符号的脚本文件解释为 shell 可执行文件,您可以在脚本文件的开头使用 shebang ( #!)来将其作为脚本可执行文件运行。注意,类 Unix 操作系统直接支持 shebang。您需要在 shebang 中包含 jjs 工具的路径,这样脚本将由jjs工具执行。脚本文件将被传递给 jjs 工具来执行。因为脚本文件以#符号开始(?? 的一部分),jjs工具将自动启用脚本模式。以下是使用 shebang 的脚本示例,假设jjs工具位于/usr/bin目录:

#!/usr/bin/jjs

var str = ls -F;

print(str);

摘要

Java 8 提供了一个名为jjs的命令行工具。它位于JDK_HOME\bin目录中。它用于在命令行上运行 Nashorn 脚本。jjs工具支持许多选项。可以调用它在文件中、在交互模式下和在脚本模式下运行脚本。

如果不指定任何选项或脚本文件,jjs以交互模式运行。在交互模式下,脚本在输入时被解释。

如果您使用–scripting选项调用jjs,它将在脚本模式下运行,允许您使用任何特定于操作系统的 shell 命令。Shell 命令用反引号括起来。在脚本模式下,jjs工具支持脚本文件中的 heredocs。如果脚本文件以#符号开头,运行脚本文件会自动启用脚本模式。这支持执行包含 shebang ( #!在脚本的开头)的脚本。

十一、在 Nashorn 中使用 JavaFX

在本章中,您将学习:

  • jjs命令行工具中的 JavaFX 支持
  • 在脚本中提供 JavaFX Application类的init()start()stop()方法的实现
  • 如何使用预定义的脚本加载和使用 JavaFX 包和类
  • 使用 Nashorn 脚本创建和启动简单的 JavaFX 应用

在本章中,我们假设您已经有了 JavaFX 8 的初级经验。如果您没有 JavaFX 的经验,请在阅读本章之前学习 JavaFX。

jjs 中的 JavaFX 支持

jjs命令行工具允许您启动在 Nashorn 脚本中创建的 JavaFX 应用。您需要使用带有jjs工具的–fx选项来作为 JavaFX 应用运行脚本。以下命令将存储在myfxapp.js文件中的脚本作为 JavaFX 应用运行:

jjs –fx myfxapp.js

脚本中 JavaFX 应用的结构

在 JavaFX 应用中,您可以覆盖下面三个Application类的方法来管理应用的生命周期:

  • init()
  • start()
  • stop()

在 Nashorn 脚本中,您可以像在 Java 中一样管理 JavaFX 应用的生命周期。脚本中可以有三个名为init()start()stop()的函数。注意,在 Nashorn 脚本中,这三个函数都是可选的。这些函数对应于 Java 类中的三个方法,它们的调用顺序如下:

The init() function is called. You can initialize the application in this function.   The start() function is called. As is the case in a Java application, the start() function is passed the reference to the primary stage of the application. You need to populate the scene, add the scene to the primary stage, and show the stage.   The stop() function is called when the JavaFX application exits.   Tip

如果 JavaFX 应用的脚本中没有start()函数,那么全局范围内的整个脚本都被认为是start()函数的代码。除了这三个函数之外,还可以有其他函数。它们将被视为函数,没有任何特殊的含义。这些函数不会被自动调用;您需要在脚本中调用它们。

清单 11-1 包含了一个简单的 JavaFX 应用的 Java 代码。它显示一个窗口,在一个StackPane中有一个Text节点。HelloFX类包含了init()start()stop()方法。图 11-1 显示了HelloFX应用显示的窗口。当您退出应用时,stop()方法会在标准输出中显示一条消息。

清单 11-1。Java 中的 HelloFX 应用

// HelloFX.java

package com.jdojo.script;

import javafx.application.Application;

import javafx.scene.Scene;

import javafx.scene.layout.StackPane;

import javafx.scene.text.Font;

import javafx.scene.text.Text;

import javafx.stage.Stage;

public class HelloFX extends Application {

private Text msg;

private StackPane sp;

public static void main(String[] args) {

Application.launch(HelloFX.class);

}

@Override

public void init() {

msg = new Text("Hello JavaFX from Nashorn!");

msg.setFont(Font.font("Helvetica", 18));

sp = new StackPane(msg);

}

@Override

public void start(Stage stage) throws Exception {

stage.setTitle("Hello FX");

stage.setScene(new Scene(sp, 300, 100));

stage.sizeToScene();

stage.show();

}

@Override

public void stop() {

System.out.println("Hello FX application is stopped.");

}

}

Hello FX application is stopped.

A978-1-4842-0713-0_11_Fig1_HTML.jpg

图 11-1。

The Windows Displayed by the HelloFX Application

清单 11-2 包含了用于HelloFX应用的 Nashorn 脚本。它是清单 11-1 中 Java 代码的一对一翻译。请注意,用 Nashorn 编写的相同应用的代码要短得多。脚本存储在hellofx.js文件中。您可以使用如下命令提示符运行该脚本,它将显示如图 11-1 所示的相同窗口:

jjs –fx hellofx.js

清单 11-2。存储在 hellofx.js 中的 HelloFX 应用的 Nashorn 脚本

// hellofx.js

var msg;

var sp;

function init() {

msg = new javafx.scene.control.Label("Hello JavaFX from Nashorn!");

msg.font = javafx.scene.text.Font.font("Helvetica", 18);

sp = new javafx.scene.layout.StackPane(msg);

}

function start(stage) {

stage.setTitle("Hello FX");

stage.setScene(new javafx.scene.Scene(sp, 300, 100));

stage.sizeToScene();

stage.show();

}

function stop() {

java.lang.System.out.println("Hello FX application is stopped.");

}

您不需要在脚本中拥有任何init()start()stop()函数。清单 11-3 包含了另一个版本的HelloFX应用。不包括init()stop()功能。来自init()函数的代码已经被移动到全局范围。stop()方法已经被移除,所以当应用退出时,您将不会在标准输出上看到消息。Nashorn 会先执行全局范围内的代码,然后调用start()方法。脚本存储在hellofx2.js文件中。运行它显示如图 11-1 所示的相同窗口。您可以运行该脚本:

jjs –fx hellofx2.js

清单 11-3。HelloFX 应用的另一个版本,没有 init()和 stop()方法

// hellofx2.js

var msg = new javafx.scene.control.Label("Hello JavaFX from Nashorn!");

msg.font = javafx.scene.text.Font.font("Helvetica", 18);

var sp = new javafx.scene.layout.StackPane(msg);

function start(stage) {

stage.setTitle("Hello FX");

stage.setScene(new javafx.scene.Scene(sp, 300, 100));

stage.sizeToScene();

stage.show();

}

您可以进一步简化 HelloFX 应用的脚本。您可以从脚本中删除start()函数。JavaFX 运行时创建初级阶段的引用并将其传递给start()函数。如果没有start()功能,如何获取初级阶段的引用?Nashorn 创建了一个名为$STAGE的全局对象,它是对初级阶段的引用。您可以使用此全局对象来处理主要阶段。你甚至不需要展示初级阶段;Nashorn 会自动显示给你看。

清单 11-3 包含的脚本是同一个 HelloFX 应用的另一个版本。它使用全局对象$STAGE来引用初级阶段。我已经去掉了init()功能。这一次,你甚至没有调用初级阶段的show()方法。你在让 Nashorn 自动为你展示初级阶段。脚本保存在hellofx3.js文件中。您可以运行该脚本:

jjs –fx hellofx3.js

清单 11-4。HelloFX 应用的另一个版本,没有 init()、start()和 stop()函数

// hellofx3.js

var msg = new javafx.scene.control.Label("Hello JavaFX from Nashorn!");

msg.font = javafx.scene.text.Font.font("Helvetica", 18);

var sp = new javafx.scene.layout.StackPane(msg);

$STAGE.setTitle("Hello FX");

$STAGE.setScene(new javafx.scene.Scene(sp, 300, 100));

$STAGE.sizeToScene();

// $STAGE.show(); // No need to show the primary stage. Nashorn will // automatically show it.

让我们再尝试一种功能组合。您将提供init()函数,但不提供start()函数。清单 11-5 包含了同一个 HelloFX 应用的代码。它包含了创建控件的init()方法,但是删除了start()方法。

清单 11-5。Nashorn 脚本中 JavaFX 应用的不正确实现

// incorrectfxapp.js

var msg;

var sp;

function init() {

msg = new javafx.scene.control.Label("Hello JavaFX from Nashorn!");

msg.font = javafx.scene.text.Font.font("Helvetica", 18);

sp = new javafx.scene.layout.StackPane(msg);

}

$STAGE.setTitle("Hello FX");

$STAGE.setScene(new javafx.scene.Scene(sp, 300, 100));

$STAGE.sizeToScene();

当您运行清单 11-5 中的脚本时,它抛出一个异常,如下所示:

jjs –fx incorrectfxapp.js

Exception in Application start method

Exception in thread "main" java.lang.RuntimeException: Exception in Application start method

at com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:875)

at com.sun.javafx.application.LauncherImpl.lambda$launchApplication$149(LauncherImpl.java:157)

at com.sun.javafx.application.LauncherImpl$$Lambda$1/23211803.run(Unknown Source)

at java.lang.Thread.run(Thread.java:745)

Caused by: java.lang.ClassCastException: Cannot cast jdk.nashorn.internal.runtime.Undefined to javafx.scene.Parent

at java.lang.invoke.MethodHandleImpl.newClassCastException(MethodHandleImpl.java:364)

...

当您试图使用全局变量sp创建场景时抛出异常,该变量是对StackPane的引用。与预期相反,在全局范围内运行代码之前,不会调用init()方法。在自动调用init()函数之前,先调用全局范围内的代码。在脚本中,init()方法创建了要添加到场景中的控件。当创建场景时,变量sp仍然是undefined,这导致了异常。如果您在脚本中显示了主要阶段,那么在主要阶段已经显示之后会调用init()函数。如果您让 Nashorn 为您显示初级阶段,则在初级阶段之前调用init()函数。

Tip

如果在 JavaFX 脚本中没有提供start()函数,那么提供init()函数几乎没有任何用处,因为这样的init()函数将在主阶段构建完成后被调用。如果您想使用init()函数来初始化您的 JavaFX 应用,您应该同时提供init()start()方法,以便它们被按顺序调用。

最后,我将向您展示最简单的 JavaFX 应用,它只用一行脚本就可以编写完成。它会在一个窗口中显示一条消息。但是,该窗口没有标题文本。清单 11-6 包含了一行脚本。它展示了 Nashorn 的妙处,将 10 到 15 行 Java 代码压缩成 1 行脚本!以下命令运行显示窗口的脚本,如图 11-2 所示:

jjs –fx simplestfxapp.js

清单 11-6。Nashorn 中最简单的 JavaFX 应用

// simplestfxapp.js

$STAGE.scene = new javafx.scene.Scene(new javafx.scene.control.Label("Hello JavaFX Scripting"));

A978-1-4842-0713-0_11_Fig2_HTML.jpg

图 11-2。

The simplest JavaFX application using Nashorn script

在 Nashorn 中,您只需为最简单的 JavaFX 应用编写一行代码,这是一种保守的说法。正确的说法是,您甚至不需要在 Nashorn 中编写一行代码来显示一个窗口。创建一个名为empty.js的脚本文件,不要在其中写任何代码。您可以将该文件命名为任何其他名称。使用以下命令运行empty.js文件:

jjs –fx empty.js

该命令将显示如图 11-3 所示的窗口。Nashorn 是如何在你不写任何代码的情况下显示一个窗口的?回想一下,Nashorn 创建了初级阶段和一个全局对象$STAGE来表示初级阶段。如果它看到你没有展示初级阶段,它就为你展示。这就是本案中发生的情况。脚本文件为空,Nashorn 自动显示空的初级阶段。

A978-1-4842-0713-0_11_Fig3_HTML.jpg

图 11-3。

The simplest JavaFX application using a Nashorn script without writing even one line of code

导入 JavaFX 类型

您可以使用 JavaFX 类的完全限定名,或者使用Java.type()函数导入它们。在上一节中,您使用了所有 JavaFX 类的完全限定名。下面的代码片段展示了在 JavaFX 中创建Label的两种方法:

// Using the``fully qualified name

var msg = new javafx.scene.control.Label("Hello JavaFX!");

// Using Java.type() function

var Label = Java.type("javafx.scene.control.Label");

var msg = new Label("Hello JavaFX!");

键入所有 JavaFX 类的完全限定名可能很麻烦。脚本不是应该比 Java 代码短吗?Nashorn 有一种方法可以缩短 JavaFX 脚本。它包括几个脚本文件,这些文件将 JavaFX 类型作为它们的简单名称导入。在脚本中使用 JavaFX 类的简单名称之前,您需要使用load()方法加载这些脚本文件。例如,Nashorn 包含一个fx:controls.js脚本文件,它将所有 JavaFX 控件类作为它们的简单类名导入。表 11-1 包含脚本文件和它们导入的类/包的列表。

表 11-1。

The List of Nashorn Script Files and the Classes/Packages That They Import

Nashorn 脚本文件导入的类/包
fx:base.jsjavafx.stage.Stage javafx.scene.Scene javafx.scene.Group javafx/beans javafx/collections
fx:graphics.jsjavafx/animation javafx/application javafx/concurrent javafx/css javafx/geometry javafx/print javafx/scene javafx/stage
fx:controls.jsjavafx/scene/chartjavafx/scene/control
fx:fxml.jsjavafx/fxml
fx:web.jsjavafx/scene/web
fx:media.jsjavafx/scene/media
fx:swing.jsjavafx/embed/swing
fx:swt.jsjavafx/embed/swt

下面的代码片段显示了如何加载这个脚本文件并使用简单的名称javafx.scene.control.Label类:

// Import all JavaFX control class names

load("fx:controls.js")

// Use the simple name of the Label control

var msg = new Label("Hello JavaFX!");

清单 11-7 包含 JavaFX 欢迎应用的代码,它保存在一个名为greeter.js的文件中。您可以按如下方式运行脚本:

jjs –fx greeter.js

清单 11-7。使用 Nashorn 脚本的 JavaFX 应用

// greeter.js

// Load Nashorn predefined scripts to import JavaFX specific classes // and packages

load("fx:base.js");

load("fx:controls.js");

load("fx:graphics.js");

// Define the start() method of the JavaFX application class

function start(stage) {

var nameLbl = new Label("Enter your name:");

var nameFld = new TextField();

var msg = new Label();

msg.style = "-fx-text-fill: blue;";

// Create buttons

var sayHelloBtn = new Button("Say Hello");

var exitBtn = new Button("Exit");

// Add the event handler for the Say Hello button

sayHelloBtn.onAction = sayHello;

// Call the same fucntion sayHello() when the user

nameFld.onAction = sayHello;

// Add the event handler for the Exit button

exitBtn.onAction = function() {

Platform.exit();

};

// Create the root node

var root = new VBox();

root.style = "-fx-padding: 10;" +

"-fx-border-style: solid inside;" +

"-fx-border-width: 2;" +

"-fx-border-insets: 5;" +

"-fx-border-radius: 5;" +

"-fx-border-color: blue;";

// Set the vertical spacing between children to 5px

root.spacing = 5;

// Add children to the root node

root.children.addAll(msg, new HBox(nameLbl, nameFld, sayHelloBtn, exitBtn));

// Set the scene and title for the stage

stage.scene = new Scene(root);

stage.title = "Greeter";

// Show the stage

stage.show();

// A nested function to say hello based on the entered name

function sayHello(evt) {

var name = nameFld.getText();

if (name.trim().length() > 0) {

msg.text = "Hello " + name;

}

else {

msg.text = "Hello there";

}

}

}

欢迎应用显示一个窗口,如图 11-4 所示。输入名称并按下Enter键或点击Say Hello按钮。将显示带有问候语的讯息。

A978-1-4842-0713-0_11_Fig4_HTML.jpg

图 11-4。

The greeter JavaFX aplication in action

在 Nashorn 中开发 JavaFX 应用要容易得多。在脚本中,您可以使用属性调用 Java 对象的 getters 和 setters。可以直接访问所有 Java 对象的属性,而不仅仅是 JavaFX 对象的属性。比如不用 Java 写root.setSpacing(5),可以用 Nashorn 写root.spacing = 5

为按钮添加事件处理程序也更容易。您可以设置一个匿名函数作为按钮的事件处理程序。请注意,您可以使用onAction属性来设置事件处理程序,而不是调用Button类的setOnAction()方法。下面的代码片段展示了如何使用函数引用sayHello来设置按钮的ActionEvent处理程序:

// Add the event handler for the Say Hello button

sayHelloBtn.onAction = sayHello

注意,在这个例子中,您在start()函数中使用了一个嵌套函数sayHello()。对函数的引用被用作事件处理程序。一个事件处理器接受一个参数,sayHello()函数中的evt形参就是那个事件对象。

摘要

Nashorn 中的jjs命令行工具支持启动用脚本编写的 JavaFX 应用。–fx选项与jjs一起使用来启动 JavaFX 应用。JavaFX 应用的脚本可以有与 JavaFX Application类的init()start()stop()方法相对应的init()start()stop()函数。Nashorn 调用这些函数的顺序与在 JavaFX 应用中调用它们的顺序相同。

start()函数传递对初级阶段的引用。如果你不提供一个start()函数,整个脚本被认为是start()函数。Nashorn 提供了一个名为$STAGE的全局对象,它是对初级阶段的引用。如果你没有提供start()函数,你需要使用$STAGE全局变量来访问初级阶段。如果您没有提供start()方法,也没有显示初级阶段,Nashorn 将为您调用$STAGE.show()方法。