Java8-API-入门手册-八-

93 阅读39分钟

Java8 API 入门手册(八)

原文:Beginning Java 8 APIs

协议:CC BY-NC-SA 4.0

十、使用 Java 编写脚本

在本章中,您将学习

  • 什么是 Java 脚本
  • 如何从 Java 执行脚本以及如何向脚本传递参数
  • 如何在执行脚本时使用ScriptContext
  • 如何在脚本中使用 Java 编程语言
  • 如何实现脚本引擎
  • 如何使用jrunscriptjjs命令行工具来执行脚本

Java 中的脚本是什么?

有人认为 Java 虚拟机(JVM)可以执行只用 Java 编程语言编写的程序。然而,事实并非如此。JVM 执行语言无关的字节码。如果程序可以被编译成 Java 字节码,它可以执行用任何编程语言编写的程序。

是一种编程语言,它为您提供了编写脚本的能力,这些脚本由称为的运行时环境(或解释器)进行评估(或解释)。脚本是使用脚本语言的语法编写的字符序列,用作由解释器执行的程序的源。解释器解析脚本,产生中间代码,中间代码是程序的内部表示,并执行中间代码。解释器将脚本中使用的变量存储在名为。

通常,与编译编程语言不同,脚本语言中的源代码(称为脚本)不是编译的,而是在运行时解释的。然而,用一些脚本语言编写的脚本可以被编译成 Java 字节码,由 JVM 运行。

Java 6 为 Java 平台增加了脚本支持,允许 Java 应用执行用 Rhino JavaScript、Groovy、Jython、JRuby、Nashorn JavaScript 等脚本语言编写的脚本。支持双向通信。它还允许脚本访问由宿主应用创建的 Java 对象。Java 运行时和脚本语言运行时可以相互通信并利用彼此的特性。

Java 对脚本语言的支持来自 Java 脚本 API。Java 脚本 API 中的所有类和接口都在javax.script包中。

在 Java 应用中使用脚本语言有几个好处:

  • 大多数脚本语言都是动态类型的,这使得编写程序更加简单。
  • 它们为开发和测试小型应用提供了一种更快捷的方式。
  • 最终用户可以进行定制。
  • 脚本语言可以提供 Java 中没有的特定领域的特性。

脚本语言也有一些缺点。例如,动态类型有利于编写更简单的代码;然而,当一个类型被错误地解释时,它就变成了一个缺点,你必须花很多时间去调试它。

Java 中的脚本支持让您可以利用两个世界的优势:它允许您使用 Java 编程语言来开发应用的静态类型、可伸缩和高性能部分,并使用适合特定领域需求的脚本语言来开发其他部分。

我将在本章中频繁使用术语脚本引擎。是一种执行用特定脚本语言编写的程序的软件组件。通常,但不一定,脚本引擎是脚本语言的解释器的实现。Java 已经实现了几种脚本语言的解释器。它们公开了编程接口,因此 Java 程序可以与它们进行交互。

JDK 7 和一个叫做 Rhino JavaScript 的脚本引擎捆绑在一起。JDK 8 用一个轻量级、更快的脚本引擎 Nashorn JavaScript 取代了 Rhino JavaScript 引擎。本章讨论的是 Nashorn JavaScript,而不是 Rhino JavaScript。请访问 www.mozilla.org/rhino 了解更多关于 Rhino JavaScript 文档的详细信息。如果你想把用 Rhino JavaScript 编写的程序迁移到 Nashorn,请访问 https://wiki.openjdk.java.net/display/Nashorn/Rhino+Migration+Guide 的 Rhino 迁移指南。如果你有兴趣在 JDK 8 中使用 Rhino JavaScript,请访问页面 https://wiki.openjdk.java.net/display/Nashorn/Using+Rhino+JSR-223+engine+with+JDK8

Java 包含一个名为jrunscript的命令行 shell,可以用来以交互模式或批处理模式运行脚本。jrunscript shell 是脚本语言中立的;JDK 7 的默认语言是 Rhino JavaScript,JDK 8 的默认语言是 Nashorn。我将在本章后面详细讨论jrunscript外壳。JDK 8 包括另一个名为jjs的命令行工具,它调用 Nashorn 引擎并提供特定于 Nashorn 的命令行选项。如果你正在使用 Nashorn,你应该使用jjs命令行工具而不是jrunscript。我将在本章后面讨论jjs命令行工具。

Java 可以执行任何为脚本引擎提供实现的脚本语言的脚本。比如 Java 可以执行 Nashorn JavaScript、Rhino JavaScript、Groovy、Jython、JRuby 等编写的脚本。本章中的示例使用 Nashorn JavaScript 语言。

在本章中,术语“Nashorn”、“Nashorn 引擎”、“Nashorn JavaScript”、“Nashorn JavaScript 引擎”、“Nashorn 脚本语言”和“JavaScript”作为同义词使用。

执行您的第一个脚本

在本节中,您将使用 Nashorn 在标准输出上打印一条消息。使用任何其他脚本语言都可以使用相同的步骤来打印消息,只有一点不同:您需要使用特定于脚本语言的代码来打印消息。要用 Java 运行脚本,您需要执行以下三个步骤:

  • 创建脚本引擎管理器。
  • 从脚本引擎管理器获取脚本引擎的实例。
  • 调用脚本引擎的eval()方法执行脚本。

脚本引擎管理器是类的一个实例。

// Create an script engine manager

ScriptEngineManager manager = new ScriptEngineManager();

接口的一个实例代表了 Java 程序中的一个脚本引擎。一个ScriptEngineManagergetEngineByName(String engineShortName)方法用于获取一个脚本引擎的实例。要获得 Nashorn 引擎的实例,使用JavaScript作为引擎的简称,如下所示:

// Get the reference of a Nashorn engine

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

Tip

脚本引擎的简称区分大小写。有时一个脚本引擎有多个简称。Nashorn 发动机有以下简称:nashornNashornjsJSJavaScriptjavascriptECMAScriptecmascript。您可以使用引擎的任何简称,通过使用ScriptEngineManager类的getEngineByName()方法来获得它的实例。

在 Nashorn 中,函数在标准输出上打印一条消息,字符串是用单引号或双引号括起来的字符序列。以下代码片段在一个String对象中存储了一个脚本,该脚本在标准输出中打印Hello Scripting!:

// Store a Nashorn script in a string

String script = "print('Hello Scripting!')";

如果要在 Nashorn 中使用双引号将字符串括起来,该语句将如下所示:

// Store a Nashorn script in a string

String script = "print(\"Hello Scripting!\")";

要执行脚本,需要将其传递给脚本引擎的eval()方法。脚本引擎在运行脚本时可能会抛出一个ScriptException。因此,当您调用ScriptEngineeval()方法时,您需要处理这个异常。以下代码片段执行存储在script变量中的脚本:

try {

engine.eval(script);

}

catch (ScriptException e) {

e.printStackTrace();

}

清单 10-1 包含了在标准输出上打印消息的完整代码。

清单 10-1。使用 Nashorn 在标准输出上打印信息

// HelloScripting.java

package com.jdojo.script;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class HelloScripting {

public static void main(String[] args) {

// Create a script engine manager

ScriptEngineManager manager = new ScriptEngineManager();

// Obtain a Nashorn script engine from the manager

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

// Store the script in a String

String script = "print('Hello Scripting!')";

try {

// Execute the script

engine.eval(script);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

}

Hello Scripting!

使用其他脚本语言

在 Java 程序中使用除 Nashorn 之外的脚本语言非常简单。在使用脚本引擎之前,您只需要执行一项任务:在应用类路径中包含特定脚本引擎的 JAR 文件。脚本引擎的实现者提供这些 JAR 文件。

Java 使用一种发现机制来列出其 JAR 文件已经包含在应用类路径中的所有脚本引擎。接口的一个实例用于创建和描述一个脚本引擎。脚本引擎的提供者为ScriptEngineFactory接口提供了一个实现。ScriptEngineManager的方法返回一个所有可用脚本引擎工厂的List<ScriptEngineFactory>ScriptEngineFactorygetScriptEngine()方法返回ScriptEngine的一个实例。工厂的其他几个方法返回关于引擎的元数据。

清单 10-2 显示了如何打印所有可用脚本引擎的细节。输出显示 Groovy、Jython 和 JRuby 的脚本引擎可用。它们之所以可用,是因为我已经将它们引擎的 JAR 文件添加到了我机器上的类路径中。当您在类路径中包含了脚本引擎的 JAR 文件,并且想知道脚本引擎的简称时,这个程序会很有帮助。运行该程序时,您可能会得到不同的输出。

清单 10-2。列出所有可用的脚本引擎

// ListingAllEngines.java

package com.jdojo.script;

import java.util.List;

import javax.script.ScriptEngineFactory;

import javax.script.ScriptEngineManager;

public class ListingAllEngines {

public static void main(String[] args) {

ScriptEngineManager manager = new ScriptEngineManager();

// Get the list of all available engines

List<ScriptEngineFactory> list = manager.getEngineFactories();

// Print the details of each engine

for (ScriptEngineFactory f : list) {

System.out.println("Engine Name:" + f.getEngineName());

System.out.println("Engine Version:" +

f.getEngineVersion());

System.out.println("Language Name:" + f.getLanguageName());

System.out.println("Language Version:" +

f.getLanguageVersion());

System.out.println("Engine Short Names:" + f.getNames());

System.out.println("Mime Types:" + f.getMimeTypes());

System.out.println("----------------------------");

}

}

}

Engine Name:

Engine Version:2.5.3

Language Name:python

Language Version:2.5

Engine Short Names:[python, jython]

Mime Types:[text/python, application/python, text/x-python, application/x-python]

----------------------------

Engine Name:JSR 223 JRuby Engine

Engine Version:1.7.0.preview1

Language Name:ruby

Language Version:jruby 1.7.0.preview1

Engine Short Names:[ruby, jruby]

Mime Types:[application/x-ruby]

----------------------------

Engine Name:Groovy Scripting Engine

Engine Version:2.0

Language Name:Groovy

Language Version:2.0.0-rc-2

Engine Short Names:[groovy, Groovy]

Mime Types:[application/x-groovy]

----------------------------

Engine Name:Oracle Nashorn

Engine Version:1.8.0_05

Language Name:ECMAScript

Language Version:ECMA - 262 Edition 5.1

Engine Short Names:[nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]

Mime Types:[application/javascript, application/ecmascript, text/javascript, text/ecmascript]

----------------------------

表 10-1 列出了在 Java 应用中使用脚本引擎之前,如何安装脚本引擎的详细信息。网站列表和说明在撰写本文时仍然有效;它们可能在阅读时失效。但是,它们向您展示了脚本语言的脚本引擎是如何安装的。如果你对使用 Nashorn 感兴趣,你不需要在你的机器上安装任何东西。Nashorn 在 JDK 8 中可用。

表 10-1。

Installation Details for Installing Some Script Engines

| 脚本引擎 | 版本 | 网站(全球资讯网的主机站) | 安装说明 | | --- | --- | --- | --- | | 绝妙的 | Two point three | [`groovy.codehaus.org`](http://groovy.codehaus.org) | 下载 Groovy 的安装文件;这是一个压缩文件。拉开拉链。在`embeddable`文件夹中查找名为`groovy-all-2.0.0-rc-2.jar`的 JAR 文件。将这个 JAR 文件添加到类路径中。 | | 脚本语言 | 2.5.3 | [`www.jython.org`](http://www.jython.org/) | 下载 Jython 安装程序文件,它是一个 JAR 文件。提取`jython.jar`文件并将其添加到类路径中。 | | JRuby | 1.7.13 | [`www.jruby.org`](http://www.jruby.org/) | 下载 JRuby 安装文件。您可以选择下载一个 ZIP 文件。拉开拉链。在`lib`文件夹中,您将找到一个需要包含在类路径中的`jruby.jar`文件。 |

清单 10-3 展示了如何使用 JavaScript、Groovy、Jython 和 JRuby 在标准输出上打印消息。如果脚本引擎不可用,程序会打印一条消息说明这一点。

清单 10-3。使用不同的脚本语言在标准输出上打印消息

// HelloEngines.java

package com.jdojo.script;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class HelloEngines {

public static void main(String[] args) {

// Get the script engine manager

ScriptEngineManager manager = new ScriptEngineManager();

// Try executing scripts in Nashorn, Groovy, Jython, and JRuby

execute(manager, "JavaScript", "print('Hello JavaScript')");

execute(manager, "Groovy", "println('Hello Groovy')");

execute(manager, "jython", "print 'Hello Jython'");

execute(manager, "jruby", "puts('Hello JRuby')");

}

public static void execute(ScriptEngineManager manager,

String engineName,

String script) {

// Try getting the engine

ScriptEngine engine = manager.getEngineByName(engineName);

if (engine == null) {

System.out.println(engineName + " is not available.");

return;

}

// If we get here, it means we have the engine installed.

// So, run the script

try {

engine.eval(script);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

}

Hello JavaScript

Hello Groovy

Hello Jython

Hello JRuby

有时,您可能只是为了好玩而想使用脚本语言,并且您不知道用于在标准输出中打印消息的语法。ScriptEngineFactory类包含一个名为getOutputStatement(String toDisplay)的方法,您可以用它来查找在标准输出中打印文本的语法。以下代码片段显示了如何获取 Nashorn 的语法:

// Get the script engine factory for Nashorn

ScriptEngineManager manager = new ScriptEngineManager();

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

ScriptEngineFactory factory = engine.getFactory();

// Get the script

String script = factory.getOutputStatement("\"Hello JavaScript\"");

System.out.println("Syntax: " + script);

// Evaluate the script

engine.eval(script);

Syntax: print("Hello JavaScript")

Hello JavaScript

对于其他脚本语言,使用它们的引擎工厂来获得语法。

探索 javax.script 包

Java 中的 Java 脚本 API 由少量的类和接口组成。它们在javax.script包里。本章包含了对这个包中的类和接口的简要描述。我将在后面的章节中讨论它们的用法。

ScriptEngine 和 ScriptEngineFactory 接口

接口是 Java 脚本 API 的主接口,它的实例有助于执行用特定脚本语言编写的脚本。

ScriptEngine接口的实现者也提供了ScriptEngineFactory接口的实现。一辆ScriptEngineFactory执行两项任务:

  • 它创建脚本引擎的实例。
  • 它提供关于脚本引擎的信息,如引擎名称、版本、语言等。

AbstractScriptEngine 类

AbstractScriptEngine类是一个抽象类。它为ScriptEngine接口提供了部分实现。除非实现脚本引擎,否则不能直接使用该类。

ScriptEngineManager 类

类为脚本引擎提供了发现和实例化机制。它还维护一个键-值对的映射,作为存储状态的Bindings接口的实例,由它创建的所有脚本引擎共享。

可编译接口和 CompiledScript 类

该接口可以可选地由脚本引擎实现,该脚本引擎允许编译脚本以便重复执行,而无需重新编译。

该类是一个抽象类。它是由脚本引擎的提供者扩展的。它以编译形式存储脚本,无需重新编译即可重复执行。请注意,使用ScriptEngine重复执行脚本会导致脚本每次都要重新编译,从而降低性能。

支持脚本编译不需要脚本引擎。如果支持脚本编译,它必须实现Compilable接口。

可调用的接口

该接口可以可选地由脚本引擎实现,该脚本引擎可以允许调用先前已经编译过的脚本中的过程、函数和方法。

绑定接口和简单绑定类

实现接口的类的一个实例是一个键-值对的映射,有一个限制,即一个键必须是非空的,非空的String。它扩展了java.util.Map接口。SimpleBindings类是Bindings接口的一个实现。

ScriptContext 接口和 SimpleScriptContext 类

接口的实例充当 Java 主机应用和脚本引擎之间的桥梁。它用于将 Java 主机应用的执行上下文传递给脚本引擎。脚本引擎可以在执行脚本时使用上下文信息。脚本引擎可以将其状态存储在实现ScriptContext接口的类的实例中,Java 主机应用可以访问该接口。

SimpleScriptContext类是ScriptContext接口的一个实现。

ScriptException 类

该类是一个异常类。如果在脚本的执行、编译或调用过程中出现错误,脚本引擎会抛出一个ScriptException。该类包含三个有用的方法,分别叫做getLineNumber()getColumnNumber()getFileName()。这些方法报告发生错误的脚本的行号、列号和文件名。ScriptException类覆盖了Throwable类的getMessage()方法,并在它返回的消息中包含行号、列号和文件名。

发现和实例化脚本引擎

您可以使用ScriptEngineFactoryScriptEngineManager创建脚本引擎。谁真正负责创建一个脚本引擎:ScriptEngineFactoryScriptEngineManager,或者两者都有?简单的回答是,ScriptEngineFactory总是负责创建脚本引擎的实例。下一个问题是“a ScriptEngineManager的作用是什么?”

A ScriptEngineManager使用服务提供者机制来定位所有可用的脚本引擎工厂。它搜索类路径和其他标准目录中的所有 JAR 文件。它寻找一个资源文件,这是一个名为javax.script.ScriptEngineFactory的文本文件,位于名为META-INF/的目录下。资源文件由实现ScriptEngineFactory接口的类的完全限定名组成。每个类名在单独的一行中指定。该文件可能包含以#字符开头的注释。示例资源文件可能包含以下内容,其中包括两个脚本引擎工厂的类名:

#Java Kishori Script Engine Factory class

com.jdojo.script.JKScriptEngineFactory

#Another factory class

com.jdojo.script.FunScriptFactory

一个ScriptEngineManager定位并实例化所有可用的ScriptEngineFactory类。您可以使用ScriptEngineManager类的方法获得所有工厂类的实例列表。当您调用管理器的一个方法来根据一个标准获得一个脚本引擎时,例如通过名称获得一个引擎的getEngineByName(String shortName)方法,管理器搜索该标准的所有工厂并返回匹配的脚本引擎引用。如果没有工厂能够提供匹配的引擎,经理返回null。请参考清单 10-2,了解更多关于列出所有可用工厂和描述它们可以创建的脚本引擎的细节。

现在你知道了ScriptEngineManager并不创建脚本引擎的实例。相反,它查询所有可用的工厂,并将工厂创建的脚本引擎的引用传递回调用者。

为了使讨论完整,让我们添加一个创建脚本引擎的方法。您可以通过三种方式创建脚本引擎的实例:

  • 直接实例化脚本引擎类。
  • 直接实例化脚本引擎工厂类,调用其getScriptEngine()方法。
  • 使用ScriptEngineManager类的getEngineByXxx()方法之一。

建议使用ScriptEngineManager类来获取脚本引擎的实例。这个方法允许由同一个管理器创建的所有引擎共享一个状态,这个状态是作为Bindings接口的一个实例存储的一组键-值对。ScriptEngineManager实例存储这个状态。

Tip

一个应用中可能有多个ScriptEngineManager类的实例。在这种情况下,每个ScriptEngineManager实例维护一个它创建的所有引擎共有的状态。也就是说,如果两个引擎是由ScriptEngineManager类的两个不同实例获得的,那么这些引擎将不会共享由它们的管理器维护的一个公共状态,除非您以编程方式实现。

执行脚本

一个ScriptEngine可以执行一个String和一个Reader中的脚本。使用Reader,您可以执行存储在网络或文件中的脚本。ScriptEngine接口的方法的以下版本之一用于执行脚本:

  • Object eval(String script)
  • Object eval(Reader reader)
  • Object eval(String script, Bindings bindings)
  • Object eval(Reader reader, Bindings bindings)
  • Object eval(String script, ScriptContext context)
  • Object eval(Reader reader, ScriptContext context)

eval()方法的第一个参数是脚本的源。第二个参数允许您将信息从宿主应用传递到脚本引擎,这些信息可以在脚本执行期间使用。

在清单 10-1 中,您看到了如何使用第一个版本的eval()方法使用一个String对象来执行一个脚本。在本节中,您将把您的脚本存储在一个文件中,并使用一个Reader对象作为脚本的源,它将使用第二个版本的eval()方法。下一节将讨论eval()方法的其他四个版本。通常,脚本文件会被赋予一个.js扩展名。

清单 10-4 显示了一个名为helloscript.js的文件的内容。它在 Nashorn 中只包含一个在标准输出中打印消息的语句。

清单 10-4。helloscript.js 文件的内容

// Print a message

print('Hello from JavaScript!');

清单 10-5 中的 Java 程序执行存储在helloscript.js文件中的脚本,该文件应该存储在当前目录中。如果没有找到脚本文件,程序会在需要的地方打印出helloscript.js文件的完整路径。如果您在执行脚本文件时遇到问题,请尝试使用main()方法中的绝对路径,比如 Windows 上的C:\scripts\helloscript.js,假设helloscript.js文件保存在C:\scripts目录中。

清单 10-5。执行存储在文件中的脚本

// ReaderAsSource.java

package com.jdojo.script;

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 ReaderAsSource {

public static void main(String[] args) {

// Construct the script file path

String scriptFileName = "helloscript.js";

Path scriptPath = Paths.get(scriptFileName);

// Make sure the script file exists. If not, print the full path of

// the script file and terminate the program.

if (! Files.exists(scriptPath) ) {

System.out.println(scriptPath.toAbsolutePath() +

" does not exist.");

return;

}

// Get the Nashorn script engine

ScriptEngineManager manager = new ScriptEngineManager();

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

try {

// Get a Reader for the script file

Reader scriptReader = Files.newBufferedReader(scriptPath);

// Execute the script in the file

engine.eval(scriptReader);

}

catch (IOException | ScriptException e) {

e.printStackTrace();

}

}

}

Hello from JavaScript!

在实际应用中,您应该将所有脚本存储在允许修改脚本而无需修改和重新编译 Java 代码的文件中。在本章的大部分例子中,你不会遵循这个规则;您将把您的脚本存储在String对象中,以保持代码简短。

传递参数

Java 脚本 API 允许您将参数从主机环境(Java 应用)传递到脚本引擎,反之亦然。在本节中,您将看到宿主应用和脚本引擎之间的参数传递机制的技术细节。

从 Java 代码向脚本传递参数

Java 程序可以向脚本传递参数。Java 程序也可以在脚本执行后访问脚本中声明的全局变量。让我们讨论一个简单的例子,Java 程序向脚本传递一个参数。考虑清单 10-6 中的程序,它将一个参数传递给一个脚本。

清单 10-6。从 Java 程序向脚本传递参数

// PassingParam.java

package com.jdojo.script;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class PassingParam {

public static void main(String[] args) {

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

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

// Store the script in a String. Here, msg is a variable

// that we have not declared in the script

String script = "print(msg)";

try {

// Store a parameter named msg in the engine

engine.put("msg", "Hello from Java program");

// Execute the script

engine.eval(script);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

}

Hello from Java program

程序将一个脚本存储在一个String对象中,如下所示:

// Store a Nashorn script in a String object

String script = "print(msg)";

在语句中,脚本是

print(msg)

注意msg是在print()函数调用中使用的变量。脚本没有声明msg变量,也没有给它赋值。如果你试图在不告诉引擎什么是msg变量的情况下执行上述脚本,引擎将抛出一个异常,声明它不理解变量msg的含义。这就是将参数从 Java 程序传递到脚本引擎的概念发挥作用的地方。

可以通过几种方式将参数传递给脚本引擎。最简单的方法是使用脚本引擎的put(String paramName, Object paramValue)方法,它接受两个参数:

  • 第一个参数是参数的名称,它需要与脚本中变量的名称相匹配。
  • 第二个参数是参数的值。

在您的例子中,您希望将一个名为msg的参数传递给脚本引擎,它的值是一个String。对方法的调用是

// Store the value of the msg parameter in the engine

engine.put("msg", "Hello from Java program");

注意,在调用eval()方法之前,必须先调用引擎的put()方法。在您的例子中,当引擎试图执行print(msg)时,它将使用您传递给引擎的msg参数的值。

大多数脚本引擎允许您使用传递给它的参数名作为脚本中的变量名。当您传递名为msg的参数值并在清单 10-6 的脚本中将它用作变量名时,您看到了这种例子。脚本引擎可能要求在脚本中声明变量,例如,在 PHP 中变量名必须以$前缀开头,而在 JRuby 中全局变量名包含$前缀。如果您想将名为msg的参数传递给 JRuby 中的脚本,您的代码应该如下所示:

// Get the JRuby script engine

ScriptEngineManager manager = new ScriptEngineManager();

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

// Must use the $ prefix in JRuby script

String script = "puts($msg)";

// No $ prefix used in passing the msg parameter to the JRuby engine

engine.put("msg", "Hello from Java");

// Execute the script

engine.eval(script);

传递给脚本的 Java 对象的属性和方法可以在脚本中访问,就像在 Java 代码中访问一样。不同的脚本语言使用不同的语法来访问脚本中的 Java 对象。例如,你可以在清单 10-6 所示的例子中使用表达式msg.toString(),输出将是相同的。在这种情况下,您正在调用变量msgtoString()方法。将清单 10-6 中赋值给script变量的语句改为如下并运行程序,这将产生相同的输出:

String script = "println(msg.toString())";

从脚本向 Java 代码传递参数

脚本引擎可以使变量在其全局范围内对 Java 代码可用。ScriptEngineget(String variableName)方法用于访问 Java 代码中的那些变量。它返回一个 Java Object。全局变量的声明依赖于脚本语言。以下代码片段声明了一个全局变量,并在 JavaScript 中为其赋值:

// Declare a variable named year in Nashorn

var year = 1969;

清单 10-7 包含了一个程序,展示了如何从 Java 代码中访问 Nashorn 中的一个全局变量。

清单 10-7。在 Java 代码中访问脚本全局变量

// AccessingScriptVariable.java

package com.jdojo.script;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class AccessingScriptVariable {

public static void main(String[] args) {

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

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

// Write a script that declares a global variable named year and

// assign it a value of 1969.

String script = "var year = 1969";

try {

// Execute the script

engine.eval(script);

// Get the year global variable from the engine

Object year = engine.get("year");

// Print the class name and the value of the variable year

System.out.println("year's class:" +

year.getClass().getName());

System.out.println("year's value:" + year);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

}

year's class:java.lang.Integer

year's value:1969

程序在脚本中声明了一个全局变量year,并给它赋值 1969,如下所示:

String script = "var num = 1969";

当脚本执行时,引擎将year变量添加到它的状态中。在 Java 代码中,引擎的get()方法用于检索year变量的值,如下所示:

Object year = engine.get("year");

当脚本中声明了year变量时,您没有指定它的数据类型。脚本变量值到适当 Java 对象的转换是自动执行的。如果您在 Java 7 中运行程序,您的输出将显示java.lang.Double作为类名,1960.0 作为year变量的值。这是因为 Java 7 使用 Rhino 脚本引擎将 1969 年解释为Double,而 Java 8 使用 Nashorn 脚本引擎将其解释为Integer

高级参数传递技术

为了理解参数传递机制的细节,必须清楚地理解三个术语:绑定、范围和上下文。这些术语起初令人困惑。本节使用以下步骤解释参数传递机制:

  • 首先,它定义了这些术语。
  • 其次,它定义了这些术语之间的关系。
  • 第三,它解释了如何在 Java 代码中使用它们。

粘合剂

是一组键-值对,其中所有键都必须是非空的非空字符串。在 Java 代码中,BindingsBindings接口的一个实例。SimpleBindings类是Bindings接口的一个实现。脚本引擎可以提供自己的Bindings接口实现。

Tip

如果你熟悉java.util.Map界面,就很容易理解BindingsBindings接口继承了Map<String,Object>接口。因此,Bindings只是一个Map,它的键必须是非空的非空字符串。

清单 10-8 显示了如何使用一个Bindings。它创建一个SimpleBindings的实例,添加一些键值对,检索键值,删除键值对,等等。Bindings接口的get()方法返回null,如果键不存在或者键存在且其值为null。如果你想测试一个键是否存在,你需要调用它的contains()方法。

清单 10-8。使用绑定对象

// BindingsTest.java

package com.jdojo.script;

import javax.script.Bindings;

import javax.script.SimpleBindings;

public class BindingsTest {

public static void main(String[] args) {

// Create a Bindings instance

Bindings params = new SimpleBindings();

// Add some key-value pairs

params.put("msg", "Hello");

params.put("year", 1969);

// Get values

Object msg = params.get("msg");

Object year = params.get("year");

System.out.println("msg = " + msg);

System.out.println("year = " + year);

// Remove year from Bindings

params.remove("year");

year = params.get("year");

boolean containsYear = params.containsKey("year");

System.out.println("year = " + year);

System.out.println("params contains year = " + containsYear);

}

}

msg = Hello

year = 1969

year = null

params contains year = false

你不能单独使用一个Bindings。通常,您会使用它将参数从 Java 代码传递到脚本引擎。ScriptEngine接口包含一个返回Bindings接口实例的方法。这个方法给脚本引擎一个机会来返回Bindings接口的特定实现的实例。您可以使用如下所示的方法:

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

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

// Instead of instantiating the SimpleBindings class, use the

// createBindings() method of the engine

Bindings params = engine.createBindings();

// Work with params as usual

范围

让我们转到下一个术语,即范围。一个作用域用于一个BindingsBindings的范围决定了它的键值对的可见性。您可以让多个Bindings出现在多个作用域中。但是,一个Bindings只能出现在一个作用域中。你如何指定一个Bindings的范围?我很快会谈到这一点。

使用Bindings的作用域可以让您按照层次顺序为脚本引擎定义参数变量。如果在引擎状态下搜索变量名,首先搜索优先级较高的Bindings,然后是优先级较低的Bindings。返回找到的第一个变量值。

Java 脚本 API 定义了两个范围。它们在ScriptContext接口中被定义为两个int常量。他们是

  • ScriptContext.ENGINE_SCOPE
  • ScriptContext.GLOBAL_SCOPE

引擎范围的优先级高于全局范围。如果将两个具有相同键的键-值对添加到两个Bindings(一个在引擎范围内,一个在全局范围内),每当必须解析与键同名的变量时,将使用引擎范围内的键-值对。

理解作用域对于一个Bindings的作用是如此重要,以至于我将通过另一个类比来解释它。考虑一个有两组变量的 Java 类:一组包含类中的所有实例变量,另一组包含方法中的所有局部变量。这两组变量及其值是两个BindingsBindings中变量的类型定义了作用域。为了便于讨论,我将定义两个范围:实例范围和本地范围。当执行一个方法时,首先在局部范围Bindings中查找变量名,因为局部变量优先于实例变量。如果在本地作用域Bindings中没有找到变量名,就在实例作用域Bindings中查找。当一个脚本被执行时,Bindings和它们的作用域扮演着相似的角色。

定义脚本上下文

脚本引擎在上下文中执行脚本。您可以将上下文视为脚本执行的环境。Java 宿主应用为脚本引擎提供了两样东西:脚本和脚本需要执行的上下文。接口的一个实例代表一个脚本的上下文。SimpleScriptContext类是ScriptContext接口的一个实现。脚本上下文由四部分组成:

  • 一组Bindings,其中每个Bindings与一个不同的作用域相关联
  • 脚本引擎用来读取输入的Reader
  • 脚本引擎用来写输出的一个Writer
  • 脚本引擎用来写入错误输出的错误Writer

上下文中的一组用于将参数传递给脚本。上下文中的读取器和写入器分别控制脚本的输入源和输出目的地。例如,通过将文件编写器设置为编写器,可以将脚本的所有输出发送到文件。

每个脚本引擎都维护一个默认的脚本上下文,用于执行脚本。到目前为止,您已经在没有提供脚本上下文的情况下执行了几个脚本。在这些情况下,脚本引擎使用它们的默认脚本上下文来执行脚本。在这一节中,我将介绍如何单独使用ScriptContext接口的实例。在下一节中,我将介绍如何在脚本执行期间将一个ScriptContext接口的实例传递给一个ScriptEngine

您可以使用该类创建一个ScriptContext接口的实例,如下所示:

// Create a script context

ScriptContext ctx = new SimpleScriptContext();

一个SimpleScriptContext类的实例维护两个Bindings实例:一个用于引擎范围,一个用于全局范围。当您创建SimpleScriptContext的实例时,就会创建引擎范围内的Bindings。要使用全局范围Bindings,您需要创建一个Bindings接口的实例。

默认情况下,SimpleScriptContext类将上下文的输入读取器、输出写入器和错误写入器分别初始化为标准输入System.in、标准输出System.out和标准错误输出System.err。您可以使用ScriptContext接口的getReader()getWriter()getErrorWriter()方法分别从ScriptContext中获取阅读器、编写器和错误编写器的引用。还提供了 Setter 方法来设置读取器和编写器。下面的代码片段显示了如何获取阅读器和编写器。它还展示了如何将 writer 设置为FileWriter以将脚本输出写入文件。

// Get the reader and writers from the script context

Reader inputReader = ctx.getReader();

Writer outputWriter = ctx.getWriter();

Writer errWriter = ctx.getErrorWriter();

// Write all script outputs to an out.txt file

Writer fileWriter = new FileWriter("out.txt");

ctx.setWriter(fileWriter);

在创建了SimpleScriptContext之后,您可以开始在引擎范围Bindings中存储键值对,因为当您创建SimpleScriptContext对象时,在引擎范围中创建了一个空的Bindings。该方法用于向一个Bindings添加一个键值对。您必须为Bindings提供键名、值和范围。下面的代码片段添加了三个键值对。

// Add three key-value pairs to the engine scope bindings

ctx.setAttribute("year", 1969, ScriptContext.ENGINE_SCOPE);

ctx.setAttribute("month", 9, ScriptContext.ENGINE_SCOPE);

ctx.setAttribute("day", 19, ScriptContext.ENGINE_SCOPE);

如果您想在全局范围内将键值对添加到一个Bindings中,您将需要首先创建并设置Bindings,如下所示:

// Add a global scope Bindings to the context

Bindings globalBindings = new SimpleBindings();

ctx.setBindings(globalBindings, ScriptContext.GLOBAL_SCOPE);

现在,您可以使用方法在全局范围内向Bindings添加键值对,如下所示:

// Add two key-value pairs to the global scope bindings

ctx.setAttribute("year", 1982, ScriptContext.GLOBAL_SCOPE);

ctx.setAttribute("name", "Boni", ScriptContext.GLOBAL_SCOPE);

此时,您可以看到ScriptContext实例的状态,如图 10-1 所示。

A978-1-4302-6662-4_10_Fig1_HTML.jpg

图 10-1。

A pictorial view of an instance of the SimpleScriptContext class

您可以在ScriptContext上执行多项操作。您可以使用setAttribute(String name, Object value, int scope)方法为已存储的密钥设置不同的值。对于指定的键和范围,可以使用removeAttribute(String name, int scope)方法移除键-值对。您可以使用getAttribute(String nameint scope)方法获取指定范围内的键值。

使用ScriptContext可以做的最有趣的事情是检索一个键值,而不用使用它的getAttribute(String name)方法指定它的作用域。一个ScriptContext首先在引擎范围Bindings中搜索关键字。如果在引擎范围内没有找到,则在全局范围内搜索Bindings。如果在这些范围中找到该键,则返回首先找到该键的范围中的相应值。如果两个范围都不包含该键,则返回null

在您的示例中,您已经在引擎范围和全局范围中存储了名为year的键。当首先搜索引擎范围时,下面的代码片段从引擎范围返回关键字year的 1969。getAttribute()方法的返回类型是Object

// Get the value of the key year without specifying the scope.

// It returns 1969 from the Bindings in the engine scope.

int yearValue = (Integer)ctx.getAttribute("year");

您只在全局范围内存储了名为name的键。如果尝试检索其值,将首先搜索引擎范围,这不会返回匹配项。随后,搜索全局范围并返回值"Boni",如下所示:

// Get the value of the key named name without specifying the scope.

// It returns "Boni" from the Bindings in the global scope.

String nameValue = (String)ctx.getAttribute("name");

您还可以检索特定范围内的键值。以下代码片段从引擎范围和全局范围中检索关键字"year"的值:

// Assigns 1969 to engineScopeYear and 1982 to globalScopeYear

int engineScopeYear = (Integer)ctx.getAttribute("year", ScriptContext.ENGINE_SCOPE);

int globalScopeYear = (Integer)ctx.getAttribute("year", ScriptContext.GLOBAL_SCOPE);

Tip

Java 脚本 API 只定义了两个作用域:引擎和全局。ScriptContext接口的子接口可以定义额外的作用域。ScriptContext接口的方法以List<Integer>的形式返回受支持范围的列表。请注意,作用域表示为整数。ScriptContext界面中的两个常量ENGINE_SCOPEGLOBAL_SCOPE分别被赋值为 100 和 200。当在出现在多个范围中的多个Bindings中搜索一个键时,首先搜索具有较小整数值的范围。因为引擎范围的值 100 小于全局范围的值 200,所以当您不指定范围时,首先在引擎范围中搜索一个键。

清单 10-9 展示了如何使用一个实现了ScriptContext接口的类的实例。请注意,您不能在应用中单独使用ScriptContext。它由脚本引擎在脚本执行期间使用。最常见的是,你通过一个ScriptEngine和一个ScriptEngineManager间接地操纵一个ScriptContext,这将在下一节详细讨论。

清单 10-9。使用 ScriptContext 接口的实例

// ScriptContextTest.java

package com.jdojo.script;

import java.util.List;

import javax.script.Bindings;

import javax.script.ScriptContext;

import javax.script.SimpleBindings;

import javax.script.SimpleScriptContext;

import static javax.script.ScriptContext.ENGINE_SCOPE;

import static javax.script.ScriptContext.GLOBAL_SCOPE;

public class ScriptContextTest {

public static void main(String[] args) {

// Create a script context

ScriptContext ctx = new SimpleScriptContext();

// Get the list of scopes supported by the script context

List<Integer> scopes = ctx.getScopes();

System.out.println("Supported Scopes: " + scopes);

// Add three key-value pairs to the engine scope bindings

ctx.setAttribute("year", 1969, ENGINE_SCOPE);

ctx.setAttribute("month", 9, ENGINE_SCOPE);

ctx.setAttribute("day", 19, ENGINE_SCOPE);

// Add a global scope Bindings to the context

Bindings globalBindings = new SimpleBindings();

ctx.setBindings(globalBindings, GLOBAL_SCOPE);

// Add two key-value pairs to the global scope bindings

ctx.setAttribute("year", 1982, GLOBAL_SCOPE);

ctx.setAttribute("name", "Boni", GLOBAL_SCOPE);

// Get the value of year without specifying the scope

int yearValue = (Integer)ctx.getAttribute("year");

System.out.println("yearValue = " + yearValue);

// Get the value of name

String nameValue = (String)ctx.getAttribute("name");

System.out.println("nameValue = " + nameValue);

// Get the value of year from engine and global scopes

int engineScopeYear = (Integer)ctx.getAttribute("year", ENGINE_SCOPE);

int globalScopeYear = (Integer)ctx.getAttribute("year", GLOBAL_SCOPE);

System.out.println("engineScopeYear = " + engineScopeYear);

System.out.println("globalScopeYear = " + globalScopeYear);

}

}

Supported Scopes: [100, 200]

yearValue = 1969

nameValue = Boni

engineScopeYear = 1969

globalScopeYear =

把它们放在一起

在这一节中,我将向您展示Bindings的实例及其作用域、ScriptContextScriptEngineScriptEngineManager和宿主应用是如何协同工作的。重点将是如何使用一个ScriptEngine和一个ScriptEngineManager在不同的范围内操作存储在Bindings中的键值对。

一个ScriptEngineManager在一个Bindings中维护一组键值对。它允许您使用以下四种方法操作这些键值对:

  • void put(String key, Object value)
  • Object get(String key)
  • void setBindings(Bindings bindings)
  • Bindings getBindings()

该方法向Bindings添加一个键值对。get()方法返回指定键的值;如果没有找到密钥,它将返回null。使用setBindings()方法可以替换发动机管理器的BindingsgetBindings()方法返回ScriptEngineManagerBindings的引用。

默认情况下,每个ScriptEngine都有一个被称为默认上下文的ScriptContext。回想一下,除了读者和作者,一个ScriptContext有两个Bindings:一个在引擎范围内,一个在全局范围内。当一个ScriptEngine被创建时,它的引擎作用域Bindings为空,它的全局作用域Bindings引用创建它的ScriptEngineManagerBindings

默认情况下,由ScriptEngineManager创建的ScriptEngine的所有实例共享ScriptEngineManagerBindings。在同一个 Java 应用中可能有多个ScriptEngineManager实例。在这种情况下,由同一个ScriptEngineManager创建的ScriptEngine的所有实例共享ScriptEngineManagerBindings作为它们默认上下文的全局作用域Bindings

下面的代码片段创建了一个ScriptEngineManager,用于创建ScriptEngine的三个实例:

// Create a ScriptEngineManager

ScriptEngineManager manager = new ScriptEngineManager();

// Create three ScriptEngines using the same ScriptEngineManager

ScriptEngine engine1 = manager.getEngineByName("JavaScript");

ScriptEngine engine2 = manager.getEngineByName("JavaScript");

ScriptEngine engine3 = manager.getEngineByName("JavaScript");

现在,让我们给ScriptEngineManagerBindings添加三个键值对,给每个ScriptEngine的引擎范围Bindings添加两个键值对。

// Add three key-value pairs to the Bindings of the

manager.put("K1", "V1");

manager.put("K2", "V2");

manager.put("K3", "V3");

// Add two key-value pairs to each engine

engine1.put("KE11", "VE11");

engine1.put("KE12", "VE12");

engine2.put("KE21", "VE21");

engine2.put("KE22", "VE22");

engine3.put("KE31", "VE31");

engine3.put("KE32", "VE32");

图 10-2 显示了代码片段执行后ScriptEngineManager和三个ScriptEngine的状态。从图中可以明显看出,所有ScriptEngine的默认上下文共享ScriptEngineManagerBindings作为它们的全局作用域Bindings

A978-1-4302-6662-4_10_Fig2_HTML.jpg

图 10-2。

A pictorial view of three ScriptEngines created by a ScriptEngineManager

ScriptEngineManager中的Bindings可以通过以下方式修改:

  • 通过使用ScriptEngineManagerput()方法
  • 通过使用ScriptEngineManagergetBindings()方法获取Bindings的参考,然后在Bindings上使用put()remove()方法
  • 通过使用ScriptEngine的方法在默认上下文的全局范围内获取Bindings的引用,然后在Bindings上使用put()remove()方法

当一个ScriptEngineManager中的Bindings被修改时,由这个ScriptEngineManager创建的所有ScriptEngine的默认上下文中的全局作用域Bindings被修改,因为它们共享同一个Bindings

每个ScriptEngine的默认上下文分别维护一个引擎范围Bindings。要将一个键-值对添加到一个ScriptEngine的引擎作用域Bindings,使用它的方法如下所示:

ScriptEngine engine1 = null; // get an engine

// Add an "engineName" key with its value as "Engine-1" to the

// engine scope Bindings of the default context of engine1

engine1.put("engineName", "Engine-1");

ScriptEngine的方法从其引擎作用域Bindings返回指定key的值。以下语句返回“Engine-1”,这是engineName键的值。

String eName = (String)engine1.get("engineName");

在默认的ScriptEngine上下文中,获得全局作用域Bindings的键值对需要两个步骤。首先,您需要使用如下所示的方法获取全局作用域Bindings的引用:

Bindings e1Global = engine1.getBindings(ScriptContext.GLOBAL_SCOPE);

现在,您可以使用e1Global引用来修改引擎的全局范围Bindings。下面的语句向e1Global Bindings添加了一个键值对:

e1Global.put("id", 89999);

因为所有的ScriptEngine共享一个ScriptEngine的全局作用域Bindings,上面的代码片段将把关键字id及其值添加到所有脚本引擎的默认上下文的全局作用域Bindings,这些脚本引擎是由创建engine1的同一个ScriptEngineManager创建的。不建议使用如上所示的代码修改ScriptEngineManager中的Bindings。您应该使用ScriptEngineManager引用来修改Bindings,这使得代码的读者对逻辑更加清楚。

清单 10-10 展示了本节讨论的概念。A ScriptEngineManager向它的Bindings添加两个键-值对,键为n1n2。创建了两个脚本引擎;他们在引擎范围Bindings中添加了一个名为engineName的键。当脚本被执行时,脚本中的engineName变量的值从ScriptEngine的引擎范围中被使用。脚本中变量n1n2的值是从ScriptEngine的全局作用域Bindings中获取的。在第一次执行该脚本后,每个ScriptEngine向它们的引擎范围Bindings添加一个名为n2的键,该键具有不同的值。当您第二次执行脚本时,变量n1的值从引擎的全局作用域Bindings中检索,而变量n2的值从引擎作用域Bindings中检索,如输出所示。

清单 10-10。使用由同一个 ScriptEngineManager 创建的引擎的全局和引擎范围绑定

// GlobalBindings.java

package com.jdojo.script;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class GlobalBindings {

public static void main(String[] args) {

ScriptEngineManager manager = new ScriptEngineManager();

// Add two numbers to the Bindings of the manager that will be

// shared by all its engines

manager.put("n1", 100);

manager.put("n2", 200);

// Create two JavaScript engines and add the name of the engine

// in the engine scope of the default context of the engines

ScriptEngine engine1 = manager.getEngineByName("JavaScript");

engine1.put("engineName", "Engine-1");

ScriptEngine engine2 = manager.getEngineByName("JavaScript");

engine2.put("engineName", "Engine-2");

// Execute a script that adds two numbers and prints the result

String script = "var sum = n1 + n2; "

+ "print(engineName + ' - Sum = ' + sum)";

try {

// Execute the script in two engines

engine1.eval(script);

engine2.eval(script);

// Now add a different value for n2 for each engine

engine1.put("n2", 1000);

engine2.put("n2", 2000);

// Execute the script in two engines again

engine1.eval(script);

engine2.eval(script);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

}

Engine-1 - Sum = 300

Engine-2 - Sum = 300

Engine-1 - Sum = 1100

Engine-2 - Sum = 2100

由一个ScriptEngineManager创建的所有 ScriptEngines 共享的全局作用域Bindings的故事还没有结束。这是最复杂、最令人困惑的事情!现在重点将放在使用ScriptEngineManager类的方法和ScriptEngine接口的效果上。考虑以下代码片段:

// Create a ScriptEngineManager and two ScriptEngines

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine1 = manager.getEngineByName("JavaScript");

ScriptEngine engine2 = manager.getEngineByName("JavaScript");

// Add two key-value pairs to the manager

manager.put("n1", 100);

manager.put("n2", 200);

图 10-3 显示了执行上述脚本后引擎管理器及其引擎的状态。此时,ScriptEngineManager中只存储了一个Bindings,两个脚本引擎将它作为它们的全局作用域Bindings来引用。

A978-1-4302-6662-4_10_Fig3_HTML.jpg

图 10-3。

Initial state of ScriptEngineManager and two ScriptEngines

让我们创建一个新的Bindings,并使用其方法将其设置为ScriptEngineManagerBindings,如下所示:

// Create a Bindings, add two key-value pairs to it, and set it as the new Bindings

// for the manager

Bindings newGlobal = new SimpleBindings();

newGlobal.put("n3", 300);

newGlobal.put("n4", 400);

manager.setBindings(newGlobal);

图 10-4 显示了代码执行后ScriptEngineManager和两个ScriptEngine的状态。请注意,ScriptEngineManager有了新的Bindings,而两个ScriptEngine仍然将旧的Bindings称为它们的全球范围Bindings

A978-1-4302-6662-4_10_Fig4_HTML.jpg

图 10-4。

State of ScriptEngineManager and two ScriptEngines after a new Bindings is set to the ScriptEngineManager

此时,对ScriptEngineManagerBindings所做的任何更改都不会反映在两个ScriptEngine的全局作用域Bindings中。您仍然可以对两个ScriptEngine共享的Bindings进行更改,并且两个ScriptEngine都将看到其中一个所做的更改。

让我们创建一个新的ScriptEngine,如图所示:

// Create a new ScriptEngine

ScriptEngine engine3 = manager.getEngineByName("JavaScript");

回想一下,ScriptEngine在创建时获得了全局作用域Bindings,BindingsScriptEngineManagerBindings相同。执行上述语句后,ScriptEngineManager和三个脚本引擎的状态如图 10-5 所示。

A978-1-4302-6662-4_10_Fig5_HTML.jpg

图 10-5。

State of ScriptEngineManager and three ScriptEngines after the third ScriptEngine is created

这里是对所谓的ScriptEngine s 的全局范围的“全球性”的另一种扭曲。这一次,您将使用一个ScriptEnginesetBindings()方法来设置它的全局范围Bindings。图 10-6 显示了执行以下代码片段后ScriptEngineManager和三个脚本引擎的状态:

// Set a new Bindings for the global scope of engine1

Bindings newGlobalEngine1 = new SimpleBindings();

newGlobalEngine1.put("n5", 500);

newGlobalEngine1.put("n6", 600);

engine1.setBindings(newGlobalEngine1, ScriptContext.GLOBAL_SCOPE);

A978-1-4302-6662-4_10_Fig6_HTML.jpg

图 10-6。

State of ScriptEngineManager and Three ScriptEngines After a New Global Scope Bindings Is Set for engine1 Tip

默认情况下,一个ScriptEngineManager创建的所有脚本引擎共享它的Bindings作为它们的全局作用域Bindings。如果您使用一个ScriptEnginesetBindings()方法来设置它的全局作用域Bindings,或者如果您使用一个ScriptEngineManagersetBindings()方法来设置它的Bindings,您就打破了“全球性”链,如本节所讨论的。为了保持“全局”链的完整性,您应该总是使用ScriptEngineManagerput()方法将键值对添加到它的Bindings中。要从由ScriptEngineManager创建的所有 ScriptEngines 的全局范围中删除一个键-值对,您需要使用ScriptEngineManagergetBindings()方法获取Bindings的引用,并在Bindings上使用remove()方法。

使用自定义脚本上下文

在上一节中,您看到每个ScriptEngine都有一个默认的脚本上下文。ScriptEngineget()put()getBindings()setBindings()方法在默认ScriptContext下运行。当ScriptEngineeval()方法没有指定ScriptContext时,使用引擎的默认上下文。ScriptEngineeval()方法的以下两个版本使用其默认上下文来执行脚本:

  • Object eval(String script)
  • Object eval(Reader reader)

您可以将一个Bindings传递给下面两个版本的方法:

  • Object eval(String script, Bindings bindings)
  • Object eval(Reader reader, Bindings bindings)

这些版本的eval()方法不使用默认的ScriptEngine上下文。他们使用一个新的ScriptContext,它的引擎作用域Bindings是传递给这些方法的,全局作用域Bindings与引擎的默认上下文相同。注意,eval()方法的这两个版本保持了ScriptEngine的默认上下文不变。

您可以将一个ScriptContext传递给下面两个版本的eval()方法:

  • Object eval(String script, ScriptContext context)
  • Object eval(Reader reader, ScriptContext context)

这些版本的eval()方法使用指定的上下文来执行脚本。它们保持ScriptEngine的默认上下文不变。

三组eval()方法允许您使用不同的隔离级别执行脚本:

  • 第一组让所有脚本共享默认上下文。
  • 第二组让脚本使用不同的引擎作用域Bindings并共享全局作用域Bindings
  • 第三组让脚本在隔离的ScriptContext中执行。

清单 10-11 展示了如何使用不同版本的eval()方法在不同的隔离级别执行脚本。该程序使用三个变量,分别叫做msgn1n2。它显示存储在msg变量中的值。将n1n2的值相加,并显示总和。该脚本打印出在计算总和时使用了什么值n1n2n1的值存储在由所有ScriptEngine的默认上下文共享的ScriptEngineManagerBindings中。n2的值存储在默认上下文和自定义上下文的引擎范围中。该脚本使用引擎的默认上下文执行两次,一次在开始,一次在结束,以证明在eval()方法中使用自定义BindingsScriptContext不会影响ScriptEngine的默认上下文中的Bindings。该程序在其main()方法中声明了一个throws子句,以使代码更短。

清单 10-11。使用不同的隔离级别执行脚本

// CustomContext.java

package com.jdojo.script;

import javax.script.Bindings;

import javax.script.ScriptContext;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

import javax.script.SimpleScriptContext;

import static javax.script.SimpleScriptContext.ENGINE_SCOPE;

import static javax.script.SimpleScriptContext.GLOBAL_SCOPE;

public class CustomContext {

public static void main(String[] args) throws ScriptException {

ScriptEngineManager manager = new ScriptEngineManager();

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

// Add n1 to Bindings of the manager, which will be shared

// by all engines as their global scope Bindings

manager.put("n1", 100);

// Prepare the script

String script = "var sum = n1 + n2;" +

"print(msg + " +

"' n1=' + n1 + ', n2=' + n2 + " +

"', sum=' + sum);";

// Add n2 to the engine scope of the default context of the engine

engine.put("n2", 200);

engine.put("msg", "Using the default context:");

engine.eval(script);

// Use a Bindings to execute the script

Bindings bindings = engine.createBindings();

bindings.put("n2", 300);

bindings.put("msg", "Using a Bindings:");

engine.eval(script, bindings);

// Use a ScriptContext to execute the script

ScriptContext ctx = new SimpleScriptContext();

Bindings ctxGlobalBindings = engine.createBindings();

ctx.setBindings(ctxGlobalBindings, GLOBAL_SCOPE);

ctx.setAttribute("n1", 400, GLOBAL_SCOPE);

ctx.setAttribute("n2", 500, ENGINE_SCOPE);

ctx.setAttribute("msg", "Using a ScriptContext:", ENGINE_SCOPE);

engine.eval(script, ctx);

// Execute the script again using the default context to

// prove that the default context is unaffected.

engine.eval(script);

}

}

Using the default context: n1=100, n2=200, sum=300

Using a Bindings: n1=100, n2=300, sum=400

Using a ScriptContext: n1=400, n2=500, sum=900

Using the default context: n1=100, n2=200, sum=300

eval()方法的返回值

ScriptEngineeval()方法返回一个Object,这是脚本中的最后一个值。如果脚本中没有最后一个值,它将返回null。依赖于脚本中的最后一个值很容易出错,同时也令人困惑。下面的代码片段展示了一些为 Nashorn 使用eval()方法返回值的例子。代码中的注释指示方法的返回值。

Object result = null;

// Assigns 3 to result

result = engine.eval("1 + 2;");

// Assigns 7 to result

result = engine.eval("1 + 2; 3 + 4;");

// Assigns 6 to result

result = engine.eval("1 + 2; 3 + 4; var v = 5; v = 6;");

// Assigns 7 to result

result = engine.eval("1 + 2; 3 + 4; var v = 5;");

// Assigns null to result

result = engine.eval("print(1 + 2)");

最好不要依赖于eval()方法的返回值。您应该将一个 Java 对象作为参数传递给脚本,并让脚本将脚本的返回值存储在该对象中。在执行了eval()方法之后,您可以查询这个 Java 对象的返回值。清单 10-12 包含一个包装整数的Result类的代码。您将向脚本传递一个Result类的对象,脚本将在其中存储返回值。脚本完成后,您可以在 Java 代码中读取存储在 Result 对象中的整数值。需要将Result声明为公共的,以便脚本引擎可以访问它。清单 10-13 中的程序展示了如何将一个Result对象传递给一个用值填充Result对象的脚本。该程序在main()方法的声明中包含一个throws子句,以保持代码简短。

清单 10-12。包装整数的结果类

// Result.java

package com.jdojo.script;

public class Result {

private int val = -1;

public void setValue(int x) {

val = x;

}

public int getValue() {

return val;

}

}

清单 10-13。在结果对象中收集脚本的返回值

// ResultBearingScript.java

package com.jdojo.script;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class ResultBearingScript {

public static void main(String[] args) throws ScriptException {

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

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

// Pass a Result object to the script. The script will store the

// result of the script in the result object

Result result = new Result();

engine.put("result", result);

// Store the script in a String

String script = "3 + 4; result.setValue(101);";

// Execute the script, which uses the passed in Result object to

// return a value

engine.eval(script);

// Use the result object to get the returned value from the script

int returnedValue = result.getValue(); // Will be 101

System.out.println("Returned value is " + returnedValue);

}

}

Returned value is 101

引擎范围绑定的保留键

通常,引擎范围Bindings中的一个键代表一个脚本变量。有些键是保留的,它们有特殊的含义。它们的值可以通过引擎的实现传递给引擎。一个实现可以定义附加的保留密钥。

表 10-2 包含所有保留键的列表。这些键在ScriptEngine接口中也被声明为常量。脚本引擎的实现不需要在引擎范围绑定中将所有这些键传递给引擎。作为开发人员,您不应该使用这些键将参数从 Java 应用传递到脚本引擎。

表 10-2。

The List of Reserved Keys for Engine Scope Bindings

| 钥匙 | ScriptEngine 接口中的常数 | 键值的含义 | | --- | --- | --- | | " javax.script.argv " | `ScriptEngine.ARGV` | 用来传递一个数组对象来传递一组位置参数。 | | " javax.script.engine " | `ScriptEngine.ENGINE` | 脚本引擎的名称。 | | " javax.script.engine_version " | `ScriptEngine.ENGINE_VERSION` | 脚本引擎的版本。 | | " javax.script.filename " | `ScriptEngine.FILENAME` | 用于传递文件或资源的名称,即脚本的来源。 | | " javax.script.language " | `ScriptEngine.LANGUAGE` | 脚本引擎支持的语言的名称。 | | " javax.script.language_version " | `ScriptEngine.LANGUAGE_VERSION` | 引擎支持的脚本语言版本。 | | " javax.script.name " | `ScriptEngine.NAME` | 脚本语言的简称。 |

更改默认脚本上下文

您可以分别使用getContext()setContext()方法来获取和设置ScriptEngine的默认上下文,如下所示:

ScriptEngineManager manager = new ScriptEngineManager();

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

// Get the default context of the ScriptEngine

ScriptContext defaultCtx = engine.getContext();

// Work with defaultCtx here

// Create a new context

ScriptContext ctx = new SimpleScriptContext();

// Configure ctx here

// Set ctx as the new default context for the engine

engine.setContext(ctx);

注意,为一个ScriptEngine设置一个新的默认上下文不会使用ScriptEngineManagerBindings作为它的全局作用域Bindings。如果您希望新的默认上下文使用ScriptEngineManagerBindings,您需要显式设置它,如下所示:

// Create a new context

ScriptContext ctx = new SimpleScriptContext();

// Set the global scope Bindings for ctx the same as the Bindings for the manager

ctx.setBindings(manager.getBindings(), ScriptContext.GLOBAL_SCOPE);

// Set ctx as the new default context for the engine

engine.setContext(ctx);

将脚本输出发送到文件

您可以自定义脚本执行的输入源、输出目标和错误输出目标。您需要为用于执行脚本的ScriptContext设置适当的读取器和写入器。下面的代码片段将把脚本输出写到当前目录中名为jsoutput.txt的文件中:

// Create a

FileWriter writer = new FileWriter("jsoutput.txt");

// Get the default context of the engine

ScriptContext defaultCtx = engine.getContext();

// Set the output writer for the default context of the engine

defaultCtx.setWriter(writer);

该代码为ScriptEngine的默认上下文设置了一个自定义输出编写器,在使用默认上下文的脚本执行过程中将会用到这个编写器。如果您想使用定制的输出编写器来执行特定的脚本,您需要使用一个定制的ScriptContext并设置它的编写器。

Tip

ScriptContext设置自定义输出编写器不会影响 Java 应用标准输出的目的地。要重定向 Java 应用的标准输出,您需要使用System.setOut()方法。

清单 10-14 显示了如何将脚本执行的输出写到名为jsoutput.txt的文件中。该程序在标准输出中打印输出文件的完整路径。运行该程序时,您可能会得到不同的输出。您需要在文本编辑器中打开输出文件来查看脚本的输出。

清单 10-14。将脚本输出写入文件

// CustomScriptOutput.java

package com.jdojo.script;

import java.io.File;

import java.io.FileWriter;

import java.io.IOException;

import javax.script.ScriptContext;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class CustomScriptOutput {

public static void main(String[] args) {

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

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

// Print the absolute path of the output file

File outputFile = new File("jsoutput.txt");

System.out.println("Script output will be written to " +

outputFile.getAbsolutePath());

FileWriter writer = null;

try {

writer = new FileWriter(outputFile);

// Set a custom output writer for the engine

ScriptContext defaultCtx = engine.getContext();

defaultCtx.setWriter(writer);

// Execute a script

String script = "print('Hello custom output writer')";

engine.eval(script);

}

catch (IOException | ScriptException e) {

e.printStackTrace();

}

finally {

if (writer != null) {

try {

writer.close();

}

catch (IOException e) {

e.printStackTrace();

}

}

}

}

}

Script output will be written to C:\jsoutput.txt

在脚本中调用过程

脚本语言可以允许创建过程、函数和方法。Java 脚本 API 允许您从 Java 应用中调用这样的过程、函数和方法。在本节中,我将使用术语“过程”来表示过程、函数和方法。当讨论的上下文需要时,我将使用特定的术语。

并非所有脚本引擎都需要支持过程调用。Nashorn JavaScript 引擎支持过程调用。如果脚本引擎支持,则脚本引擎类的实现必须实现接口。在调用过程之前,检查脚本引擎是否实现了Invocable接口是开发人员的责任。调用过程包括四个步骤:

  • 检查脚本引擎是否支持过程调用。
  • 将发动机参考转换为Invocable类型。
  • 评估包含该过程源代码的脚本。
  • 使用Invocable接口的invokeFunction()方法调用过程和函数。使用invokeMethod()方法来调用在脚本语言中创建的对象的方法。

以下代码片段检查脚本引擎实现类是否实现了接口:

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

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

// Make sure the script engine implements the Invocable interface

if (engine instanceof Invocable) {

System.out.println("Invoking procedures is supported.");

else

System.out.println("Invoking procedures is not supported.");

}

第二步是将引擎引用转换为Invocable接口类型。

Invocable inv = (Invocable)engine;

第三步是评估脚本,因此脚本引擎编译并存储过程的编译形式,供以后调用。以下代码片段执行此步骤:

// Declare a function named add that adds two numbers

String script = "function add(n1, n2) { return n1 + n2; }";

// Evaluate the function. Call to eval() does not invoke the function.

// It just compiles it.

engine.eval(script);

最后一步是调用过程或函数。

// Invoke the add function with 30 and 40 as the function's arguments.

// It is as if you called add(30, 40) in the script.

Object result = inv.invokeFunction("add", 30, 40);

invokeFunction()的第一个参数是过程或函数的名称。第二个参数是 varargs,用于指定过程或函数的参数。invokeFunction()方法返回过程或函数返回的值。

清单 10-15 显示了如何调用一个函数。它调用用 Nashorn JavaScript 编写的函数。

清单 10-15。调用用 Nashorn JavaScript 编写的函数

// InvokeFunction.java

package com.jdojo.script;

import javax.script.Invocable;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class InvokeFunction {

public static void main(String[] args) {

ScriptEngineManager manager = new ScriptEngineManager();

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

// Make sure the script engine implements the Invocable interface

if (!(engine instanceof Invocable)) {

System.out.println("Invoking procedures is not supported.");

return;

}

// Cast the engine reference to the Invocable type

Invocable inv = (Invocable)engine;

try {

String script = "function add(n1, n2) { return n1 + n2; }";

// Evaluate the script first

engine.eval(script);

// Invoke the add function twice

Object result1 = inv.invokeFunction("add", 30, 40);

System.out.println("Result1 = " + result1);

Object result2 = inv.invokeFunction("add", 10, 20);

System.out.println("Result2 = " + result2);

}

catch (ScriptException | NoSuchMethodException e) {

e.printStackTrace();

}

}

}

Result1 = 70

Result2 = 30

面向对象或基于对象的脚本语言可以让您定义对象及其方法。您可以使用Invocable接口的invokeMethod()方法调用这些对象的方法,声明如下:

Object invokeMethod(Object objectRef, String name, Object... args)

第一个参数是对象的引用,第二个参数是要在对象上调用的方法的名称,第三个参数是 varargs 参数,用于将参数传递给被调用的方法。

清单 10-16 演示了在 Nashorn JavaScript 中创建的对象上调用方法。注意,该对象是在 Nashorn 脚本中创建的。要从 Java 调用对象的方法,需要通过脚本引擎获取对象的引用。程序评估使用add()方法创建对象的脚本,并将其引用存储在名为calculator的变量中。engine.get("calculator")方法返回对 Java 代码的calculator对象的引用。

清单 10-16。在 Nashorn JavaScript 中创建的对象上调用方法

// InvokeMethod.java

package com.jdojo.script;

import javax.script.Invocable;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class InvokeMethod {

public static void main(String[] args) {

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

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

// Make sure the script engine implements the Invocable interface

if (!(engine instanceof Invocable)) {

System.out.println("Invoking methods is not supported.");

return;

}

// Cast the engine reference to the Invocable type

Invocable inv = (Invocable) engine;

try {

// Declare a global object with an add() method

String script = "var calculator = new Object();" +

"calculator.add = function add(n1, n2){return n1 + n2;}";

// Evaluate the script first

engine.eval(script);

// Get the calculator object reference created in the script

Object calculator = engine.get("calculator");

// Invoke the add() method on the calculator object

Object result = inv.invokeMethod(calculator, "add", 30, 40);

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

}

catch (ScriptException | NoSuchMethodException e) {

e.printStackTrace();

}

}

}

Result = 70

Tip

使用Invocable界面重复执行程序、函数和方法。具有过程、函数和方法的脚本评估将中间代码存储在引擎中,从而在重复执行时获得性能增益。

在脚本中实现 Java 接口

Java 脚本 API 允许您用脚本语言实现 Java 接口。Java 接口的方法可以使用顶层过程或对象的实例方法在脚本中实现。

用脚本语言实现 Java 接口的优点是,您可以用 Java 代码使用接口的实例,就好像接口是用 Java 实现的一样。您可以将接口的实例作为参数传递给 Java 方法。

Invocable接口的getInterface()方法用于获取在脚本中实现的 Java 接口的实例。该方法有两个版本:

  • <T> T getInterface(Class<T> cls)
  • <T> T getInterface(Object obj, Class<T> cls)

第一个版本用于获取 Java 接口的实例,该接口的方法在脚本中作为顶级过程实现。接口类型作为参数传递给该方法。假设你有一个Calculator接口,如清单 10-17 所示,它有两个方法叫做add()subtract()

清单 10-17。计算器界面

// Calculator.java

package com.jdojo.script;

public interface Calculator {

int add (int n1, int n2);

int subtract (int n1, int n2);

}

考虑以下两个用 JavaScript 编写的顶级函数:

function add(n1, n2) {

return n1 + n2;

}

function subtract(n1, n2) {

return n1 - n2;

}

以上两个函数提供了Calculator接口的两个方法的实现。JavaScript 引擎编译完上述函数后,您可以获得一个Calculator接口的实例,如下所示:

// Cast the engine reference to the Invocable type

Invocable inv = (Invocable)engine;

// Get the reference of the Calculator interface

Calculator calc = inv.getInterface(Calculator.class);

if (calc == null) {

System.err.println("Calculator interface implementation not found.");

}

else {

// Use calc to call add() and subtract() methods

}

您可以添加两个数字,如下所示:

int sum = calc.add(15, 10);

清单 10-18 展示了如何在 Nashorn 中使用顶级过程实现一个 Java 接口。请查阅脚本语言的文档,了解它如何支持此功能。

清单 10-18。使用脚本中的顶级函数实现 Java 接口

// UsingInterfaces.java

package com.jdojo.script;

import javax.script.Invocable;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class UsingInterfaces {

public static void main(String[] args) {

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

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

// Make sure the script engine implements Invocable interface

if (!(engine instanceof Invocable)) {

System.out.println("Interface implementation in script"

+ " is not supported.");

return;

}

// Cast the engine reference to the Invocable type

Invocable inv = (Invocable) engine;

// Create the script for add() and subtract() functions

String script = "function add(n1, n2) { return n1 + n2; } "

+ "function subtract(n1, n2) { return n1 - n2; }";

try {

// Compile the script that will be stored in the engine

engine.eval(script);

// Get the interface implementation

Calculator calc = inv.getInterface(Calculator.class);

if (calc == null) {

System.err.println("Calculator interface " +

"implementation not found.");

return;

}

int result1 = calc.add(15, 10);

System.out.println("add(15, 10) = " + result1);

int result2 = calc.subtract(15, 10);

System.out.println("subtract(15, 10) = " + result2);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

}

add(15, 10) = 25

subtract(15, 10) = 5

方法的第二个版本用于获取 Java 接口的实例,该接口的方法实现为对象的实例方法。它的第一个参数是用脚本语言创建的对象的引用。对象的实例方法实现作为第二个参数传入的接口类型。Nashorn 中的以下代码创建了一个对象,其实例方法实现了Calculator接口:

// Create an object

var calc = new Object();

// Add add() and subtract() methods to the calc object

calc.add = function add(n1, n2) {

return n1 + n2;

};

calc.subtract = function subtract(n1, n2) {

return n1 - n2;

};

当脚本对象的实例方法实现 Java 接口的方法时,您需要执行一个额外的步骤。在获取接口的实例之前,需要获取脚本对象的引用,如下所示:

// Get the reference of the global script object obj

Object calc = engine.get("calc");

// Get the implementation of the Calculator interface

Calculator calculator = inv.getInterface(calc, Calculator.class);

清单 10-19 展示了如何使用 Nashorn 将 Java 接口的方法实现为对象的实例方法。

清单 10-19。将 Java 接口的方法实现为脚本中对象的实例方法

// ScriptObjectImplInterface.java

package com.jdojo.script;

import javax.script.Invocable;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class ScriptObjectImplInterface {

public static void main(String[] args) {

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

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

// Make sure the engine implements the Invocable interface

if (!(engine instanceof Invocable)) {

System.out.println("Interface implementation in " +

"script is not supported.");

return;

}

// Cast the engine reference to the Invocable type

Invocable inv = (Invocable)engine;

String script = "var calc = new Object(); " +

"calc.add = function add(n1, n2) {return n1 + n2; }; " +

"calc.subtract = function subtract(n1, n2) {return n1 - n2;};";

try {

// Compile and store the script in the engine

engine.eval(script);

// Get the reference of the global script object calc

Object calc = engine.get("calc");

// Get the implementation of the Calculator interface

Calculator calculator = inv.getInterface(calc, Calculator.class);

if (calculator == null) {

System.err.println("Calculator interface " +

"implementation not found.");

return;

}

int result1 = calculator.add(15, 10);

System.out.println( "add(15, 10) = " + result1);

int result2 = calculator.subtract(15, 10);

System.out.println("subtract(15, 10) = " + result2);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

}

add(15, 10) = 25

subtract(15, 10) =

使用编译的脚本

脚本引擎可以允许编译脚本并重复执行它。执行编译后的脚本可以提高应用的性能。脚本引擎可以以 Java 类、Java 类文件的形式或特定于语言的形式编译和存储脚本。

并非所有脚本引擎都需要支持脚本编译。支持脚本编译的脚本引擎必须实现Compilable接口。Nashorn 引擎支持脚本编译。以下代码片段检查脚本引擎是否实现了接口:

// Get the script engine reference

ScriptEngineManager manager = new ScriptEngineManager();

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

if (engine instanceof Compilable) {

System.out.println("Script compilation is supported.");

}

else {

System.out.println("Script compilation is not supported.");

}

一旦您知道脚本引擎实现了Compilable接口,您就可以将其引用转换为Compilable类型

// Cast the engine reference to the Compilable type

Compilable comp = (Compilable)engine;

Compilable接口包含两个方法:

  • CompiledScript compile(String script) throws ScriptException
  • CompiledScript compile(Reader script) throws ScriptException

该方法的两个版本仅在脚本源的类型上有所不同。第一个版本接受脚本作为String,第二个版本接受脚本作为Reader

该方法返回一个CompiledScript类的对象。CompiledScript是一个抽象类。脚本引擎的提供者提供了这个类的具体实现。一个CompiledScript与创建它的ScriptEngine相关联。CompiledScript类的getEngine()方法返回与其关联的ScriptEngine的引用。

要执行编译后的脚本,您需要调用CompiledScript类的以下eval()方法之一:

  • Object eval() throws ScriptException
  • Object eval(Bindings bindings) throws ScriptException
  • Object eval(ScriptContext context) throws ScriptException

没有任何参数的eval()方法使用脚本引擎的默认脚本上下文来执行编译后的脚本。当你向另外两个版本传递一个Bindings或一个ScriptContext时,它们的工作方式与ScriptEngine接口的eval()方法相同。

清单 10-20 显示了如何编译并执行一个脚本。它使用不同的参数将相同的编译脚本执行两次。

清单 10-20。使用编译的脚本

// CompilableTest .java

package com.jdojo.script;

import javax.script.Bindings;

import javax.script.Compilable;

import javax.script.CompiledScript;

import javax.script.ScriptEngine;

import javax.script.ScriptEngineManager;

import javax.script.ScriptException;

public class CompilableTest {

public static void main(String[] args) {

// Get the Nashorn engine

ScriptEngineManager manager = new ScriptEngineManager();

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

if (!(engine instanceof Compilable)) {

System.out.println("Script compilation not supported.");

return;

}

// Cast the engine reference to the Compilable type

Compilable comp = (Compilable)engine;

try {

// Compile a script

String script = "print(n1 + n2)";

CompiledScript cScript = comp.compile(script);

// Store n1 and n2 script variables in a Bindings

Bindings scriptParams = engine.createBindings();

scriptParams.put("n1", 2);

scriptParams.put("n2", 3);

cScript.eval(scriptParams);

// Execute the script again with different values for n1 and n2

scriptParams.put("n1", 9);

scriptParams.put("n2", 7);

cScript.eval(scriptParams);

}

catch (ScriptException e) {

e.printStackTrace();

}

}

}

5

16

在脚本语言中使用 Java

脚本语言允许在脚本中使用 Java 类库。每种脚本语言都有自己的使用 Java 类的语法。讨论所有脚本语言的语法是不可能的,也超出了本书的范围。在这一节中,我将讨论在 Nashorn 中使用一些 Java 构造的语法。关于纳申语的完整报道,请参考 https://wiki.openjdk.java.net/display/Nashorn/Main 的网站。

声明变量

在脚本语言中声明变量与 Java 无关。通常,脚本语言允许您在不声明变量的情况下为变量赋值。变量的类型是在运行时根据它们存储的值的类型确定的。

在 Nashorn 中,关键字var用于声明一个变量。如果您愿意,可以在变量声明中省略关键字var。以下代码片段声明了两个变量,并为它们赋值:

// Declare a variable named msg using the var keyword

var msg = "Hello";

// Declare a variable named greeting without using the keyword var

greeting = "Hello";

导入 Java 类

在 Nashorn 中,有四种方法可以在脚本中导入 Java 类:

  • 使用Packages全局对象
  • 使用 Java 全局对象的type()函数
  • 使用importPackage()importClass()功能
  • with子句中使用 JavaImporter

下面几节将详细描述在脚本中导入 Java 类的四种方式。

使用包全局对象

Nashorn 将a ll Java 包定义为名为Packages的全局变量的属性。例如,java.langjavax.swing包可以分别称为Packages.java.langPackages.javax.swing。以下代码片段使用了 Nashorn 中的java.util.Listjavax.swing.JFrame:

// Create a List

var list1 = new Packages.java.util.ArrayList();

// Create a JFrame

var frame1 = new Packages.javax.swing.JFrame("Test");

Nashorn 将javajavaxorgcomedunet声明为全局变量,分别是Packages.javaPackages.javaxPackages.orgPackages.comPackages.eduPackages.net的别名。本书示例中的类名以前缀com开头,例如com.jdojo.script.Test。要在 JavaScript 代码中使用这个类名,可以使用Packages.com.jdojo.script.Testcom.jdojo.script.Test。但是,如果一个类名不是以这些预定义的前缀之一开头,您必须使用Packages全局变量来访问它;例如,如果您的类名是p1.Test,您需要在 JavaScript 代码中使用Packages.p1.Test来访问它。以下代码片段为Packages.javaPackages.javax使用了 java 和 javax 别名:

// Create a List

var list2 = new java.util.ArrayList();

// Create a JFrame

var frame2 = new javax.swing.JFrame("Test");

使用 Java 全局对象

Java 7 中的 Rhino JavaScript 也支持将包作为Packages对象的属性来访问。使用Packages对象速度较慢并且容易出错。Nashorn 定义了一个名为Java的新的全局对象,它包含许多有用的函数来处理 Java 包和类。如果你使用的是 Java 8 或更高版本,你应该使用Java对象而不是Packages对象。Java对象的type()函数将 Java 类型导入到脚本中。您需要传递要导入的 Java 类型的完全限定名。在 Nashorn 中,以下代码片段导入了java.util.ArrayList类并创建了它的对象:

// Import java.util.ArrayList type and call it ArrayList

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

// Create an object of the ArrayList type

var list = new ArrayList();

在代码中,您将从Java.type()函数返回的导入类型称为ArrayList,这也是导入的类的名称。这样做是为了让下一条语句看起来像是用 Java 编写的。第二条语句的读者会知道你正在创建一个ArrayList类的对象。但是,您可以为导入的类型指定任何想要的名称。下面的代码片段导入java.util.ArrayList并将其命名为MyList:

// Import java.util.ArrayList type and call it MyList

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

// Create an object of the MyList type

var list2 = new MyList();

使用 importPackage()和 importClass()函数

Rhino JavaScript 允许在脚本中使用 Java 类型的简单名称。Rhino JavaScript 有两个名为importPackage()importClass()的内置函数,分别用于从一个包中导入所有类和从一个包中导入一个类。出于兼容性的考虑,Nashorn 保留了这些功能。要在 Nashorn 中使用这些功能,您需要使用load()功能从mozilla_compat.js文件中加载兼容模块。以下代码片段使用这些函数重写了上述逻辑:

// Load the compatibility module. It is needed in Nashorn, not in Rhino.

load("nashorn:mozilla_compat.js");

// Import ArrayList class from the java.util package

importClass(java.util.ArrayList);

// Import all classes from the javax.swing package

importPackage(javax.swing);

// Use simple names of classes

var list1 = new ArrayList();

var frame1 = new JFrame("Test");

JavaScript 不会自动从java.lang包中导入所有类,因为 JavaScript 类名称相同,例如StringObjectNumbe r 等。,会与java.lang包中的类名冲突。要使用来自java.lang包的类,您可以导入它或者使用PackagesJava变量来使用它的完全限定名。您不能从java.lang包中导入所有的类。下面的代码片段生成了一个错误,因为 JavaScript 中已经定义了String类名:

// Load the compatibility module. It is needed in Nashorn, not in Rhino.

load("nashorn:mozilla_compat.js");

importClass(java.lang.String); // An

如果你想使用java.lang.String类,你需要使用它的完全限定名。以下代码片段使用内置的 JavaScript String类和java.lang.String类:

var javaStr = new java.lang.String("Hello"); // Java String class

var jsStr = new String("Hello");            // JavaScript String class

如果java.lang包中的类名与 JavaScript 顶级类名不冲突,可以使用importClass()函数导入 Java 类。例如,您可以使用下面的代码片段来使用java.lang.System类:

// Load the compatibility module. It is needed in Nashorn, not in Rhino.

load("nashorn:mozilla_compat.js");

importClass(java.lang.System);

var jsStr = new String("Hello");

System.out.println(jsStr);

在上面的代码片段中,jsStr是一个 JavaScript String,它被传递给接受java.lang.String类型的System.out.println() Java 方法。在这种情况下,JavaScript 自动处理从 JavaScript 类型到 Java 类型的转换。

使用 JavaImporter 对象

在 JavaScript 中,您可以通过在with子句中使用JavaImporter对象引用来使用简单的类名。JavaImporter类的构造函数接受 Java 包和类的列表。您可以创建一个JavaImporter对象,如下所示:

// Import all classes from the java.lang package

var langPkg = new JavaImporter(Packages.java.lang);

// Import all classes from the java.lang and java.util packages and the

// JFrame class from the javax.swing package

var pkg2 = JavaImporter(java.lang, java.util, javax.swing.JFrame);

注意第一条语句中使用了new操作符。第二条语句没有使用new操作符。这两个语句在 JavaScript 中都有效。

以下代码片段创建了一个JavaImporter对象,并在子句中使用它:

// Create a Java importer for java.lang and java.util packages

var javaLangAndUtilPkg = JavaImporter(java.lang, java.util);

// Use the imported types in the with clause

with (javaLangAndUtilPkg) {

var list = new ArrayList();

list.add("one");

list.add("two");

System.out.println("Hello");

System.out.println("List is " + list);

}

Hello

List is [one, two]

创建和使用 Java 对象

使用带有构造函数的new操作符在脚本中创建新的 Java 对象。以下代码片段在 Nashorn 中创建了一个String对象:

// Create a Java String object

var javaString = new java.lang.String("A Java string");

在大多数脚本语言中,访问 Java 对象的方法和属性是相似的。一些脚本语言允许您使用属性名调用对象的 getter 和 setter 方法。Nashorn 中的以下代码片段创建了一个java.util.Date对象,并使用属性名和方法名访问该对象的方法:

var dt = new java.util.Date();

var year = dt.year + 1900;

var month = dt.month + 1;

var date = dt.getDate();

println("Date:" + dt);

println("Year:" + year + ", Month:" + month + ", Day:" + date);

Date:Wed Jul 09 00:35:31 CDT 2014

Year:2014, Month:7, Day:9

使用 JavaScript 时,理解不同类型的String对象很重要。一个String对象可能是一个 JavaScript String对象或者一个 Java java.lang.String对象。JavaScript 为其String类定义了一个length属性,而 Java 为其java.lang.String类定义了一个方法。以下代码片段显示了创建和访问 JavaScript String和 Java java.lang.String对象的长度的不同:

// JavaScript String

var jsStr = new String("Hello JavaScript String");

print("JavaScript String: " + jsStr);

print("JavaScript String Length: " + jsStr.length);

// Java String

var javaStr = new java.lang.String("Hello Java String");

print("Java String: " + javaStr);

print("Java String Length: " + javaStr.length());

JavaScript String: Hello JavaScript String

JavaScript String Length: 23

Java String: Hello Java String

Java String Length: 17

使用重载的 Java 方法

Java 在编译时解决重载的方法调用。也就是说,Java 编译器确定代码运行时将调用的方法的签名。考虑清单 10-21 所示的PrintTest类的代码。您可能会在第二行得到不同的输出。

清单 10-21。在 Java 中使用重载方法

// PrintTest.java

package com.jdojo.script;

public class PrintTest {

public void print(String str) {

System.out.println("print(String): " + str);

}

public void print(Object obj) {

System.out.println("print(Object): " + obj);

}

public void print(Double num) {

System.out.println("print(Double): " + num);

}

public static void main(String[] args) {

PrintTest pt = new PrintTest();

Object[] list = new Object[]{"Hello", new Object(), 10.5};

for(Object arg : list) {

pt.print(arg);

}

}

}

print(Object): Hello

print(Object): java.lang.Object@affc70

print(Object): 10.5

当运行PrintTest类时,对print()方法的所有三个调用都调用PrintTest类的同一个版本print(Object)。当代码被编译时,Java 编译器将调用pt.print(arg)视为对带有Object类型参数的print()方法的调用(这是arg的类型),因此将该调用绑定到print(Object)方法。

在脚本语言中,变量的类型在运行时是已知的,而不是在编译时。脚本语言的解释器根据方法调用中参数的运行时类型适当地解析重载的方法调用。以下 JavaScript 代码的输出显示了对PrintTest类的print()方法的调用在运行时根据参数的类型进行解析:

// In JavaScript

var pt = new com.jdojo.script.PrintTest();

var list = ["Hello", new Object(), 10.5];

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

pt.print(list[i]);

}

print(String): Hello

print(Object): [object Object]

print(Double): 10.5

JavaScript 允许您显式选择重载方法的特定版本。您可以传递要用对象引用调用的重载方法的签名。以下代码片段选择了print(Object)版本:

// In JavaScript

var pt = new com.jdojo.script.PrintTest();

pt"print(java.lang.Object)"; // Calls print(Object)

pt"print(java.lang.Double)"; // Calls print(Double)

print(Object): 10.5

print(Double): 10.

使用 Java 数组

在 Rhino 和 Nashorn 中,用 JavaScript 创建 Java 数组的方式是不同的。在 Rhino 中,您需要使用java.lang.reflect.Array类的newInstance()静态方法创建一个 Java 数组。Nashorn 也支持这种语法。下面的代码片段展示了如何使用 Rhino 语法创建和访问 Java 数组:

// Create a java.lang.String array of 2 elements, populate it, and print the // elements

// Rhino you were able to use java.lang.String as the first argument, but in // Nashorn, you

// need to use java.lang.String.class instead.

var strArray = java.lang.reflect.Array.newInstance(java.lang.String.class, 2);

strArray[0] = "Hello";

strArray[1] = "Array";

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

print(strArray[i]);

}

Hello

Array

创建原始类型数组,如intdouble等。,您需要将它们的TYPE常量用于它们对应的包装类,如下所示:

// Create an int array of 2 elements, populate it, and print the elements

var intArray = java.lang.reflect.Array.newInstance(java.lang.Integer.TYPE, 2);

intArray[0] = 100;

intArray[1] = 200;

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

print(intArray[i]);

}

100

200

Nashorn 支持创建 Java 数组的新语法。首先,使用方法创建适当的 Java 数组类型,然后使用熟悉的new操作符创建数组。下面的代码片段展示了如何在 Nashorn 中创建一个包含两个元素的String[]:

// Get the java.lang.String[] type

var StringArray = Java.type("java.lang.String[]");

// Create a String[] array of 2 elements

var strArray = new StringArray (2);

strArray[0] = "Hello";

strArray[1] = "Array";

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

print(strArray[i]);

}

Hello

Array

Nashorn 支持以同样的方式创建原始类型的数组。以下代码片段在 Nashorn 中创建了两个元素的int[]:

// Get the int[] type

var IntArray = Java.type("int[]");

// Create a int[] array of 2 elements

var intArray = new IntArray(2);

intArray[0] = 100;

intArray[1] = 200;

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

print(intArray[i]);

}

100

200

当需要 Java 数组时,可以使用 JavaScript 数组。JavaScript 将执行从 JavaScript 数组到 Java 数组的必要转换。假设你有一个PrintArray类,如清单 10-22 所示,它包含一个接受String数组作为参数的print()方法。

清单 10-22。PrintArray 类

// PrintArray.java

package com.jdojo.script;

public class PrintArray {

public void print(String[] list) {

System.out.println("Inside print(String[] list):");

for(String s : list) {

System.out.println(s);

}

}

}

以下 JavaScript 代码片段将一个 JavaScript 数组传递给PrintArray.print(String[])方法。JavaScript 负责将本机数组转换成一个String数组,如输出所示。

// Create a JavaScript array and populate it with three strings

var names = new Array();

names[0] = "Rhino";

names[1] = "Nashorn";

names[2] = "JRuby";

// Create an object of the PrintArray class

var pa = new com.jdojo.script.PrintArray();

// Pass a JavaScript array to the PrintArray.print(String[] list) method

pa.print(names);

Inside print(String[] list):

Rhino

Nashorn

JRuby

Nashorn 使用Java.to()Java.from()函数支持 Java 和 JavaScript 数组之间的数组类型转换。Java.to()函数将 JavaScript 数组类型转换成 Java 数组类型。该函数将 array 对象作为第一个参数,将目标 Java 数组类型作为第二个参数。目标数组类型可以指定为字符串或类型对象。下面的代码片段将 JavaScript 数组转换成 Java String[]:

// Create a JavaScript array and populate it with three integers

var personIds = [100, 200, 300];

// Convert the JavaScript integer array to java String[]

var JavaStringArray = Java.to(personIds, "java.lang.String[]")

如果函数中的第二个参数被省略,JavaScript 数组将被转换为 Java Object[]

该函数将 Java 数组类型转换为 JavaScript 数组。该函数将 Java 数组作为参数。以下代码片段显示了如何将 Java int[]转换为 JavaScript 数组:

// Create a Java int[]

var IntArray = Java.type("int[]");

var personIds = new IntArray(3);

personIds[0] = 100;

personIds[1] = 200;

personIds[2] = 300;

// Convert the Java int[] array to a JavaScript array

var jsArray = Java.from(personIds);

// Print the elements in the JavaScript array

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

print(jsArray[i]);

}

100

200

300

Nashorn 似乎无法将 Java String[]转换成 JavaScript 数组。在以下脚本中尝试这样做将导致如下所示的错误:

// Create a Java String object

var str = new java.lang.String("Rhino,Nashorn,JRuby");

var strDelimiter = new java.lang.String(",");

var strArray = str.split(strDelimiter);

// Convert the Java String[] array to a JavaScript array

var jsArray = Java.from(strArray); // Nashorn throws an ScriptException here

// Print the elements in tje JavaScript array

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

print(jsArray[i]);

}

javax.script.ScriptException: TypeError: Can only convert Java arrays and lists to JavaScript arrays. Cant convert object of type {0}. in <eval> at line number 8...

Tip

从 JavaScript 函数返回一个 JavaScript 数组到 Java 代码是可能的。您需要在 Java 代码中提取原生数组的元素,因此您需要在 Java 中使用特定于 JavaScript 的类。不建议使用这种方法。您应该将 JavaScript 数组转换为 Java 数组,并从 JavaScript 函数返回 Java 数组,这样 Java 代码就只处理 Java 类。

扩展 Java 类实现接口

JavaScript 允许您在 JavaScript 中扩展 Java 类和实现 Java 接口。以下部分描述了实现这一点的不同方法。

使用脚本对象

您需要创建一个包含接口方法实现的脚本对象,并使用new操作符将其传递给 Java 接口的构造函数。在 Java 中,接口没有构造函数,也不能和new操作符一起使用。然而,JavaScript 让你做到了这一点。

让我们实现清单 10-17 所示的Calculator接口。下面的语句创建了一个实现add()subtract()方法的脚本对象。请注意,这两个方法的实现由逗号分隔。方法名及其实现由冒号分隔。

var calFuncObj =  {

add: function (n1, n2) {

return n1 + n2;

},

subtract: function (n1, n2) {

return n1 - n2;

}

};

以下语句创建接口的实现:

var calc = new com.jdojo.script.Calculator(calFuncObj);

现在您可以开始使用calc对象,就像它是Calculator接口的实现一样,如下所示:

var n1 = 15;

var n2 = 10;

var result1 = calc.add(n1, n2);

var result2 = calc.subtract(n1, n2);

print(n1 + " + " + n2 + " = " + result1);

print(n1 + " - " + n2 + " = " + result2);

15 + 10 = 25

15 - 10 = 5

使用匿名的类语法

该方法使用的语法与在 Java 中创建匿名类的语法非常相似。以下语句实现了 Java Calculator接口,并创建了该实现的实例:

var calc = new com.jdojo.script.Calculator() {

add: function (n1, n2) {

return n1 + n2;

},

subtract: function (n1, n2) {

return n1 - n2;

}

};

现在你可以像以前一样使用calc对象。

使用 JavaAdapter 对象和 Java.extend()函数

JavaScript 允许您实现多个接口,并使用JavaAdapter类扩展一个类。然而,与 JDK 捆绑在一起的 Rhino JavaScript 实现已经覆盖了JavaAdapter的实现,它只允许你实现一个接口;它不允许你扩展一个类。JavaAdapter构造函数的第一个参数是要实现的接口,第二个参数是实现方法的脚本对象。要在 Nashorn 中使用该对象,您需要加载 Rhino 兼容性模块。下面的代码片段使用JavaAdapter实现了Calculator接口:

// Need to load the compatibility module in Nashorn.

// You do not need to the following load() call in Rhino.

load("nashorn:mozilla_compat.js");

var calFuncObj =  {

add: function (n1, n2) {

return n1 + n2;

},

subtract: function (n1, n2) {

return n1 - n2;

}

};

var calc = new JavaAdapter(com.jdojo.script.Calculator, calFuncObj);

现在你可以像以前一样使用calc对象。

Nashorn 提供了一种更好的方法,可以让您使用Java.extend()函数扩展一个类并实现多个接口。在extend()函数中,你可以传递最多一个类类型和多个接口类型。它返回一个组合了所有传入类型的类型。您需要使用前面讨论过的匿名类语法来为新类型的抽象方法提供实现,或者覆盖被扩展类型的现有方法。下面的代码片段使用了Java.extend()方法来实现Calculator接口:

// Get the Calculator interface type

var CalculatorType = Java.type("com.jdojo.script.Calculator");

// Get a type that extends the Calculator type

var CalculatorExtender = Java.extend(CalculatorType);

// Implement the abstract methods in CalculatorExtender

// using an anonymous class like syntax

var calc = new CalculatorExtender() {

add: function (n1, n2) {

return n1 + n2;

},

subtract: function (n1, n2) {

return n1 - n2;

}

};

var n1 = 15;

var n2 = 10;

var result1 = calc.add(n1, n2);

var result2 = calc.subtract(n1, n2);

print(n1 + " + " + n2 + " = " + result1);

print(n1 + " - " + n2 + " = " + result2);

15 + 10 = 25

15 - 10 = 5

使用 JavaScript 函数

有时一个 Java 接口只有一个方法。在这些情况下,您可以传递一个 JavaScript 函数对象来代替接口的实现。Java 中的Runnable接口只有一个方法run()。当需要在 JavaScript 中使用Runnable接口的实例时,可以传递一个 JavaScript function 对象。

下面的代码片段展示了如何创建一个Thread对象并启动它。在类的构造函数中,传递的是 JavaScript 函数对象myRunFunc,而不是接口的实例。

function myRunFunc() {

print("A thread is running.");

}

// Call Thread(Runnable) constructor and pass the myRunFunc function object // that

// will serve as an implementation for the run() method of the Runnable    // interface.

var thread = new java.lang.Thread(myRunFunc);

thread.start();

A thread is running.

使用 Lambda 表达式

JavaScript 支持可以用作 lambda 表达式的匿名函数。下面是一个匿名函数,它将一个数字作为参数并返回其平方值:

function (n) {

return n * n;

}

下面是一个使用匿名函数作为 lambda 表达式在 JavaScript 中创建一个Runnable对象的例子。在Thread类的构造函数中使用了Runnable对象。

var Thread = Java.type("java.lang.Thread");

// Create a Thread using a Runnable object. The Runnable

// object is created using an anonymous function as a lambda expressions.

var thread = new Thread(function() {

print("Hello Thread");

});

// Start the thread

thread.start();

使用 lambda 表达式的 JavaScript 代码的 Java 等效代码如下:

// Create a Thread  using a Runnable object. The Runnable object is created using a

// lambda expressions.

Thread thread = new Thread(() -> {

System.out.println("Hello Thread");

});

// Start the thread

thread.start();

实现脚本引擎

实现一个成熟的脚本引擎不是一件简单的任务,这超出了本书的范围。本节旨在为您提供实现脚本引擎所需的设置的简要但完整的概述。在本节中,您将实现一个简单的脚本引擎,称为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脚本引擎实现的一部分,你将开发表 10-3 中列出的三个类。在随后的部分中,您将开发这些类。

表 10-3。

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

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

表达式类

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

清单 10-23。分析和计算算术表达式的表达式类

// 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 expressiona 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用于跟踪表达式是否已经被解析。该方法将其设置为true

构造函数

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

parse()方法

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

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

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

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

注意,匹配操作数的正则表达式并不完美。它将允许几种无效的情况,比如一个操作数有多个小数点,等等。然而,对于这个演示目的,它将做。

getOperandValue()方法

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

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

eval()方法

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

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

JKScriptEngine 类

清单 10-24 包含了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中的空白被忽略。

清单 10-24。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 类

清单 10-25 包含了JKScript引擎的ScriptEngineFactory接口的实现。它的一些方法返回“未实现”字符串,因为您不支持这些方法公开的功能。JKScriptEngineFactory类中的代码不言自明。如在getNames()方法中编码的那样,可以使用名称为jksJKScriptjkscriptScriptEngineManager来获得JKScript引擎的实例。

清单 10-25。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文件并输入如清单 10-26 所示的内容。文件中的第一行是以#号开头的注释。第二行是JKScript脚本引擎工厂类的完全限定名。

清单 10-26。名为 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\*.*

使用 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. ");

System.out.println("Add jkscript.jar to CLASSPATH.");

}

else {

// Evaluate your JKScript

}

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

清单 10-27。使用 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. ");

System.out.println("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.

Result of C:\jkscript.txt = 190.0

jrunscript 命令行 Shell

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

语法

使用jrunscript shell 的语法是

jrunscript [options] [arguments]

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

表 10-4。

The List of Options for the jrunscript shell

| [计]选项 | 描述 | | --- | --- | | `-classpath ` | 用于指定类路径。 | | `-cp ` | 同选项`-classpath`。 | | `-D=` | 为 Java 运行时设置系统属性。 | | `-J` | 将指定的``传递给运行`jrunscript`的 JVM。 | | `-l ` | 允许您指定要使用的脚本语言。默认情况下,Rhino JavaScript 用于 JDK 6 和 JDK 7。在 JDK 8 中,Nahsorn 是默认值。如果您想使用 JavaScript 之外的语言,比如说 JKScript,您将需要使用`-` cp 或`-classpath`选项来包含包含脚本引擎的 JAR 文件。 | | `-e 部分是一个参数列表,根据是否使用了-e-f选项来解释。传递给脚本的参数在脚本中以名为arguments的数组的形式存在。表 10-5 列出了与-e-f选项一起使用时参数的解释。

表 10-5。

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

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

外壳的执行模式

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

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

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

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

Hello Rhino Nashorn!

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

成批处理方式

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

清单 10-28。用 Nashorn JavaScript 编写的 nashorntest.js 脚本文件

// Print a message

print("Hello Nashorn!");

// Add two integers and print the value

var x = 10;

var y = 20;

var z = x + y;

print(x + " + " + y + " = " + z);

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

C:\>jrunscript -f nashorntest.js

Hello Nashorn!

10 + 20 = 30

对话方式

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

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

以下命令不使用任何选项和参数来进入交互模式。按 Enter 键会让 shell 评估输入的脚本。

c:\>jrunscript

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

Hello Interactive mode!

nashorn> var num = 190;

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

num is 190

nashorn> exit();

列出可用的脚本引擎

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 JavaScript 引擎对 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

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

清单 10-29。用 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 应用传递。

jjs 命令行工具

为了配合 Nashorn 脚本引擎,JDK 8 包含了一个名为jjs的新命令行工具。该命令位于JDK_HOME\bin目录中。该命令可用于运行文件中的脚本或以交互模式在命令行上输入的脚本。它还可以用于执行 shell 脚本。调用该命令的语法是

jjs <options> <script-files> <-- arguments>

这里,

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

表 10-6 列出了jjs工具的一些常用选项。要打印所有选项的列表,请运行带有选项的工具,如下所示:

jjs –xhelp

表 10-6。

Options for the jjs Comand-line Tool

| [计]选项 | 描述 | | --- | --- | | `-classpath or -cp ` | 用于指定类路径。 | | `-D=` | 为 Java 运行时设置系统属性。可以重复使用该选项来设置多个运行时属性值。 | | `-J` | 将指定的``传递给 JVM。 | | `-scripting` | 启用外壳脚本功能。 | | `-strict` | 启用使用 ECMAScript 版标准执行脚本的严格模式。 | | `-fx` | 将脚本作为 JavaFX 应用启动。 | | `-doe or –dump-on-error` | 如果指定了此选项,将打印错误的完整栈跟踪。默认情况下,会打印一条简短的错误消息。 | | `-v or –version` | 打印 Nashorn 引擎的版本。 | | `-fv or –fullversion` | 打印 Nashorn 引擎的完整版本。 | | `-t=`或`–timezone=` | 设置脚本执行的时区。默认时区是芝加哥/美国。 | | `-help`或`-h` | 输出帮助消息并退出。 | | `-xhelp` | 打印扩展帮助。 |

如果在没有指定任何选项或脚本文件的情况下运行jjs,它将以交互模式运行。当您输入脚本时,它会被解释。Nashorn 中的字符串可以用单引号或双引号括起来。以下是在交互模式下使用jjs工具的一些例子。假设您已经在机器的 path 环境变量中包含了jjs工具的路径。如果您没有这样做,您可以在下面的命令中将jjs替换为JDK_HOME\bin\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工具,稍后使用arguments属性访问它们。请注意,您必须在两个连字符和第一个参数之间添加一个空格。

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

jjs> for each (x in``arguments

1

2

3

4

5

jjs> quit()

c:\>

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

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

// 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工具,这允许您运行 shell 命令。您可以使用–scripting选项在脚本模式下启动jjs工具。shell 命令用反引号括起来。以下是在脚本模式下使用jjs工具使用datels shell 命令的示例:

c:\>jjs -

jjs> date``

Mon Jul 14 22:42:26 CDT 2014

stream.js

test.js

jjs> quit()

c:\>

您可以在变量中捕获 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 脚本。这是一个保留空格的多行字符串。一个以双尖括号(< EOFEND开始的 heredoc 被用作定界标识符。但是,您可以使用脚本中其他地方没有用作标识符的任何其他标识符。多行字符串从最后一行开始。字符串以相同的定界标识符结束。以下是在 Nashorn 中使用 heredoc 的示例:

var str = <<EOF

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

Bye Heredoc!

EOF

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

清单 10-31。使用 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 中 shell 脚本的更多信息,请参考 http://docs.oracle.com/javase/8/docs/technotes/guides/scripting/nashorn/shell.html .

Nashorn 中的 JavaFX

Nashorn 的命令行工具允许您从脚本中使用 JavaFX。您需要在 JavaScript 中创建一个start()函数,就像您在 Java 中启动 JavaFX 应用一样。纳森会处理剩下的事情。可选地,您也可以为您的 JavaFX 应用声明init()stop()函数。您可以使用 JavaFX 类的完全限定名,或者使用Java.type()函数导入它们。下面的代码片段展示了在 JavaFX 中创建Label的两种方法:

// Using the fully qualified name of the Label class

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 类型作为它们的简单名称导入。您需要使用load()方法加载这些脚本文件,然后您可以在脚本中使用 JavaFX 类的简单名称。Nashorn 包含一个fx:controls.js脚本文件,它将所有 JavaFX 控件类作为它们的简单类名导入。表 10-7 包含脚本文件及其导入的类/包的列表。

表 10-7。

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

| Nashorn 脚本文件 | 导入的类/包 | | --- | --- | | `fx:base.js` | `javafx.stage.Stage``javafx.scene.Scene``javafx.scene.Group``javafx/beans``javafx/collections``javafx/events` | | `fx:graphics.js` | `javafx/animation` `javafx/application` `javafx/concurrent` `javafx/css` `javafx/geometry` `javafx/print` `javafx/scene` `javafx/stage` | | `fx:controls.js` | `javafx/scene/chart` `javafx/scene/control` | | `fx:fxml.js` | `javafx/fxml` | | `fx:web.js` | `javafx/scene/web` | | `fx:media.js` | `javafx/scene/` | | `fx:swing.js` | `javafx/embed/swing` | | `fx:swt.js` | `javafx/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!");

清单 10-32 包含一个 JavaFX 应用的代码。将代码保存在名为hellojavafx.js的文件中。

清单 10-32。使用 Nashorn 脚本的 JavaFX 应用

// hellojavafx.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.setStyle("-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 = function() {

var name = nameFld.getText();

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

msg.text = "Hello " + name;

}

else {

msg.text = "Hello there";

}

};

// Add the event handler for the Exit button

exitBtn.onAction = function() {

Platform.exit();

};

// Create the root

var root = new VBox();

// Set the vertical spacing between children to 5px

root.spacing = 5;

// Add children to the root node

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

// Set the scene and title for the stage

stage.scene = new Scene(root, 350, 150);

stage.title = "Hello JavaFX from Nashorn";

// Show the stage

stage.show();

}

这是在第九章的中定义为ImprovedHelloFXApp Java 类的 JavaFX 应用的 Nashorn 脚本等价物。Nashorn 版本的代码编写起来稍微简单一点。在脚本中,您可以使用 Java 类的属性来调用它们的方法。例如,代替书写

root.setSpacing(5);

在 Java 中,你可以写

root.spacing = 5;

在 Nashorn JavaScript 中。

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

// Add the event handler for the Say Hello

sayHelloBtn.onAction = function() {

// Script code to handle the ActionEvent goes here

};

要运行 JavaFX 应用,您需要用一个–fx选项启动jjs工具。以下命令启动 JavaFX 应用,显示如图 10-7 所示的窗口。输入姓名并点击Say Hello按钮查看消息。点击Exit按钮退出应用。

C:\> jjs -fx hellojavafx.js

A978-1-4302-6662-4_10_Fig7_HTML.jpg

图 10-7。

A JavaFX window created using Nashorn script

jjs命令行工具使得使用 JavaFX 应用变得非常容易。只需一行代码就可以在 JavaFX 窗口中显示一条消息。Nashorn 创建了一个名为$STAGE的全局变量,它是 JavaFX 应用初级阶段的引用。请注意,只有在使用带有–fx选项的jjs工具时,脚本中的$STAGE全局变量才可用。清单 10-33 显示了最简单的 JavaFX 应用的代码,它在 JavaFX 窗口中显示一个带有消息的Label。将其保存在名为simplestfxapp.js的文件中。注意,您不必再为start()方法创建任何函数。您甚至不需要在$STAGE变量上调用show()方法。Nashorn 会自动显示舞台。

清单 10-33。在 Nashorn 脚本中使用$STAGE 全局变量

// simplestfxapp.js

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

以下命令将运行最简单的 JavaFX 应用,该应用显示如图 10-8 所示的窗口。

A978-1-4302-6662-4_10_Fig8_HTML.jpg

图 10-8。

The simplest JavaFX application using Nashorn script

摘要

脚本语言是一种编程语言,它使您能够编写由运行时环境评估(或解释)的脚本,运行时环境称为脚本引擎(或解释器)。脚本是使用脚本语言的语法编写的字符序列,用作由解释器执行的程序的源。Java 脚本 API 允许您执行用任何脚本语言编写的脚本,这些脚本可以从 Java 应用编译成 Java 字节码。JDK 6 和 7 附带了一个名为 Rhino JavaScript engine 的脚本引擎。在 JDK 8 中,Rhino JavaScript 引擎已经被一个名为 Nashorn 的脚本引擎所取代。

使用脚本引擎执行脚本,脚本引擎是ScriptEngine接口的一个实例。ScriptEngine接口的实现者也提供了ScriptEngineFactory接口的实现,其工作是创建脚本引擎的实例并提供关于脚本引擎的细节。ScriptEngineManager类为脚本引擎提供了发现和实例化机制。一个ScriptManager维护一个键值对的映射,作为一个由它创建的所有脚本引擎共享的Bindings接口的实例。

您可以执行包含在StringReader中的脚本。ScriptEngineeval()方法用于执行脚本。您可以使用ScriptContext向脚本传递参数。传递的参数可以是脚本引擎的本地参数,脚本执行的本地参数,或者由ScriptManager创建的所有脚本引擎的全局参数。使用 Java 脚本 API,您还可以执行用脚本语言编写的过程和函数。如果脚本引擎支持,您还可以预编译脚本,并执行从 Java 重复的脚本以获得更好的性能。

您可以使用 Java 脚本 API 实现您的脚本引擎。您需要为ScriptEngineScriptEngineFactory接口提供实现。你需要以某种方式打包你的脚本引擎代码,这样引擎就可以在运行时被ScriptManager发现。

Java 8 提供了两个命令行工具,分别叫做jrunscriptjjs。它们位于JDK_HOME\bin目录中。它们用于在命令行上运行脚本。jrunscript工具是脚本语言中立的;它可以用来执行 Nashorn、JRuby、groovy 等任何脚本语言的脚本。jjs工具用于运行 Nashorn 脚本及其扩展;您可以使用jjs工具运行 shell 命令、脚本和 Java 应用。