Java平台为桌面和Web应用程序开发提供了丰富的资源。是,除非有专有软件解决方案,否则从平台外部使用其他资源是不切实际的。没有行业标准定义或阐明开发人员如何使用java类文件执行其他编程语言。脚本语言还没有一种标准的,行业支持的方法来与Java技术集成。 Java Specification Request(JSR)223 是一项更改,它通过定义标准框架和应用程序编程接口(API)来执行以下操作,从而帮助开发人员集成Java技术和脚本语言:
- Access and control Java technology-based objects from a scripting environment
- Create web content with scripting languages
- Embed scripting environments within Java technology-based applications
本文重点关注该规范的第三个目标,并将展示如何使用Java平台应用程序中的嵌入式脚本环境。 一个名为ScriptCalc的演示应用程序将提供一个有效的示例,说明如何使用JavaScript语言的自定义脚本扩展你的应用程序。
为什么要使用脚本语言
大多数脚本语言是动态执行的。 通常,你可以在不预先确定变量类型的情况下创建新变量,并且可以重用变量来存储不同类型的值。 同样,脚本语言会自动尝试执行许多类型转换,例如,根据需要将数字10转换为文本“ 10”。 尽管有些脚本语言是编译性的,但大多数语言都是解释性的。 脚本环境通常在同一过程中执行脚本的编译和执行。 通常,这些环境在首次执行时也会将脚本解析并编译为中间代码。
这些高质量的脚本语言可帮助你更快地编写应用程序,重复执行命令以及将来自不同技术的组件集成在一起。 专用脚本语言比通用语言更容易或更快速地执行特定任务。 例如,许多开发人员认为Perl脚本语言是处理文本和生成报告的好方法。 其他开发人员将bash
or ksh
shell 用于命令和作业控制。 其他脚本语言有助于方便地定义用户界面或Web内容。 开发人员可以使用Java编程语言和平台来完成这些任务中的任何一项,但是脚本语言有时可以更好地完成这项工作。 这个事实并没有削弱Java平台的功能和丰富性,而只是承认脚本语言在开发人员的工具箱中占有重要地位。
将脚本语言与Java平台相结合,为开发人员提供了利用两种环境的能力的机会。 出于任何原因,你可以继续使用脚本语言,并且可以使用功能强大的Java类库来扩展这些语言的功能。 如果您是Java语言程序员,则现在可以发布用户动态定制的应用程序。 Java平台和脚本语言之间的协同作用产生了一个环境,开发人员和最终用户可以在其中协作创建更有用的动态应用程序。
例如,假设一个计算器具有一组核心运算。 尽管基本计算器可能只有四个或五个基本操作,但是你可以提供用户可以自定义的可编程功能键。 客户可以使用他们喜欢的任何脚本语言向计算器添加抵押贷款计算,温度转换或其他更复杂的功能。 另一个使用场景文字处理器,它允许客户提供用于生成各种文件格式的自定义过滤器。 本文其余部分的示例将展示如何使用脚本为客户提供可定制的Java应用程序。
JSR 223 实现
JDK 6和JRE 6库中包含了用于JavaScript编程语言的Mozilla Rhino引擎。 Java SE 6平台实现了java.script API,你可以使用符合JSR 223的脚本引擎。 您可以通过访问Mozilla Rhino web site 网站来了解有关嵌入式JavaScript技术引擎的更多信息。
使用脚本 API 的方法
脚本 API 在 Java SE 6平台中 javax.script
包中. 如下表所示,The API仍然相对较小,由六个接口和六个类组成 .
Table 1: Interfaces and Classes in the Java SE 6 Platform
Interface | Class |
---|---|
Bindings |
AbstractScriptEngine |
Compilable |
CompiledScript |
Invocable |
ScriptEngineManager |
ScriptContext |
SimpleBindings |
ScriptEngine |
SimpleScriptContext |
ScriptEngineFactory |
ScriptException |
你的起点应该是 ScriptEngineManager
类. ScriptEngineManager
对象可以告诉你你 Java Runtime Environment (JRE) 可以使用那些脚本引擎. 它还可以提供 ScriptEngine
对象,这些对象可以解释特定脚本语言编写的搅拌. 使用此API的最简单方法是执行以下操作:
- 创建一个
ScriptEngineManager
对象. - 从manager 中获取一个
ScriptEngine
对象. - 使用
ScriptEngine
对象执行 script .
这听起来很容易, 但是代码是什么样的呢? Example 1 代码执行了这三个步骤, 并在控制台打印 Hello, world!
.
Example 1: 通过engine 名称创建一个 ScriptEngine
对象.
ScriptEngineManager mgr = new ScriptEngineManager();
ScriptEngine jsEngine = mgr.getEngineByName("JavaScript");
try {
jsEngine.eval("print('Hello, world!')");
} catch (ScriptException ex) {
ex.printStackTrace();
}
如果要查询支持的脚本引擎列表,将值传递到脚本环境或编译脚本以重复执行, API 仅仅智慧稍微复杂一点点. 其他 APIs 允许你查询 ScriptEngineManager
以获取与特定文件扩展名关联的引擎, 从文件执行脚本以及在脚本中调用特定功能. 本文会介绍其他一些功能.
可用的脚本引擎
ScriptEngineManager
对象为脚本框架提供发现机制. 可以找到ScriptEngineFactory
类, 该类创建 ScriptEngine
对象. 开发人员可以使用 JAR Service Provider specification 将脚本引擎添加到JRE. 尽管此规范不是我们要讨论的内容,更多信息请参考 JAR File Specification.
Example 1 直接从脚本管理器中获取了脚本引擎. 但是, 只有当你知道引擎名称时 , 这种访问 ScriptEngine
对象的方式才有效. 如果需要使用更复杂的条件来获取 ScriptEngine
对象, 则可能需要首先获取 ScriptEngineFactory
对象支持的引擎的整个列表. ScriptEngineFactory
可以为特定的脚本语言创建 ScriptEngine
对象.
Example 2 提供可发现工厂的列表.
Example 2: 你可以获取安装到Java平台的所有引擎的列表.
ScriptEngineManager mgr = new ScriptEngineManager();
List<ScriptEngineFactory> factories = mgr.getEngineFactories();
一旦拥有脚本引擎工厂后, 你可以获取有关工厂支持的脚本语言的各种详细信息:
- 引擎名称和版本
- 语言名称和版本
- 脚本引擎的别名
- 脚本语言的
ScriptEngine
对象
Example 3 展示如何获取这些信息.
Example 3: ScriptEngineFactory
对象提供有关其提供的引擎的详细信息.
ScriptEngineManager mgr = new ScriptEngineManager();
List<ScriptEngineFactory> factories =
mgr.getEngineFactories();
for (ScriptEngineFactory factory: factories) {
System.out.println("ScriptEngineFactory Info");
String engName = factory.getEngineName();
String engVersion = factory.getEngineVersion();
String langName = factory.getLanguageName();
String langVersion = factory.getLanguageVersion();
System.out.printf("\tScript Engine: %s (%s)\n",
engName, engVersion);
List<String> engNames = factory.getNames();
for(String name: engNames) {
System.out.printf("\tEngine Alias: %s\n", name);
}
System.out.printf("\tLanguage: %s (%s)\n",
langName, langVersion);
}
Example 3 控制台输出如下:
ScriptEngineFactory Info
Script Engine: Java ScriptEngine (2.0.0)
Engine Alias: Java
Engine Alias: java
Engine Alias: bern:java-scriptengine
Engine Alias: bern-java
Language: Java (1.8.0_211)
ScriptEngineFactory Info
Script Engine: Oracle Nashorn (1.8.0_211)
Engine Alias: nashorn
Engine Alias: Nashorn
Engine Alias: js
Engine Alias: JS
Engine Alias: JavaScript
Engine Alias: javascript
Engine Alias: ECMAScript
Engine Alias: ecmascript
Language: ECMAScript (ECMA - 262 Edition 5.1)
请注意,不同版本的JDK或者安装了不同脚本的引擎,输出都不太一样,其中 JavaScript 脚本引擎工厂是java内置的引擎。 Rhino是核心JDK 6库中包含的唯一引擎,Nashorn是JDK8库中的引擎。 你可以通过将基于JAR文件的服务提供程序安装到JRE中来添加其他引擎,如前所述。 本文的代码示例使用Nashorn引擎。 请注意,脚本引擎工厂提供了许多引擎名称别名,以帮助您获取脚本语言的引擎。
创建 ScriptEngine
的方法
一旦获得了有关工厂及其提供的引擎的所有信息 , 就可以在运行时决定使用哪个引擎工厂. 如果找到合适的 ScriptEngineFactory
, 则创建关联的 ScriptEngine
就比较容易了. 从工厂获取实际的引擎,如示例4所示, 只需要调用工厂的 getScriptEngine
方法. 此代码遍历所有已知的工厂, 搜索符合语言名称和版本的工厂. 在此示例中, 条件是硬编码的. 该代码获取支持ECMA - 262 Edition 5.1版的工厂.
Example 4: 获取满足你应用要求的脚本引擎.
ScriptEngineManager mgr = new ScriptEngineManager();
List<ScriptEngineFactory> scriptFactories =
mgr.getEngineFactories();
ScriptEngine engine = null;
for (ScriptEngineFactory factory: scriptFactories) {
String langName = factory.getLanguageName();
String langVersion = factory.getLanguageVersion();
if (langName.equals("ECMAScript") &&
langVersion.equals("ECMA - 262 Edition 5.1")) {
engine = factory.getScriptEngine();
break;
}
}
当然, 如果确定引擎可用, 则可以直接通过名称,文件扩展名甚至MIME类型向 ScriptEngineManager
中获取对象. 一下代码将获取 JavaScript programming language engine ,因为 js
是 JavaScript 语言的通用扩展名.
engine = mgr.getEngineByExtension("js");
如何运行脚本
ScriptEngine
对象运行脚本代码. 引擎的 eval
方法来翻译脚本, 脚本是从 String
或 java.io.Reader
对象中获得得字符序列 . Reader
对象也可以从文件获取其字符. 即使你已经部署了应用程序,也可以使用此功能来读取客户提供得脚本 .
Example 1 使用 eval
方法来计算 String
字符序列:
try {
jsEngine.eval("print('Hello, world!')");
} catch (ScriptException ex) {
ex.printStackTrace();
}
Nashorn's 的 print
方法实现将其参数数据发送到控制台. Hello, world!
消息出现在你的命令控制台中. 如果在 NetBeans 或 Eclipse 等集中开发环境中运行此命令 (IDE) , 则输出将显示在 IDE 的 调试或输出窗口中.
在应用程序中使用脚本的最佳原因之一是允许用户自定义其功能. 允许这种客制化的最简单的方式是读取客户提供的脚本文件. eval
的重载方法可以使用 Reader
参数, 你可以使用该参数来读取来自外部文件的脚本.
在应用程序的 JAR 文件之外查找资源可能会有问题 . 但是, 如果将脚本放置在classpath 的相对目录或用户明确定义的绝对位置中 , 则应用程序可以可靠的找到脚本. 如果确定所有用户自定义的脚本都存在于应该程序的JAR目录下的scripts
子目录中 , 则应确保 JAR 文件的子目录位于 classpath . 只要你的应用程序目录位于 classpath, 你的应用程序就应该在 scripts
子目录中找到客户定义的脚本 . 你可以使用JAR 中的清单文件(manifest )中 Class-path
语句将 JAR 文件的相对目录位置放在 classpath 下. JAR文件位置的相对路径用 .
字符表示. 本位币的 ScriptCalc demo 应用程序使用 manifest.xml
文件,与Example 5中的文件类似.
Example 5: 在classpath 中间 .
有助于应用程序查找具有相对于JAR文件的路径的脚本.
Manifest-Version: 1.0
Ant-Version: Apache Ant 1.6.5
Created-By: 1.6.0-rc-b89 (Sun Microsystems Inc.)
Main-Class: com.sun.demo.calculator.Calculator
Class-Path: .
Example 6 显示了如何计算客户提供的文件. 文件名为 /scripts/F1.js
, 位于应用程序目录下.
Example 6: eval
方法可以读取脚本文件.**
ScriptEngineManager engineMgr = new ScriptEngineManager();
ScriptEngine engine = engineMgr.getEngineByName("ECMAScript");
InputStream is =
this.getClass().getResourceAsStream("/scripts/F1.js");
try {
Reader reader = new InputStreamReader(is);
engine.eval(reader);
} catch (ScriptException ex) {
ex.printStackTrace();
}
如何调用脚本函数
运行整个脚本很有用, 但是你可能只想调用特定的脚本函数. 一些脚本引擎实现了 Invocable
接口. 如果引擎实现此接口, 则你可以调用,或者调用引擎提供的其他的特定的方法或函数.
脚本引擎不是必须要实现 Invocable
接口. 但是, JDK8 中包含的Nashorn JavaScript 技术实现了这个接口. 如果你的脚本包含一个名叫 sayHello
的函数, 则可以通过将 ScriptEngine
对象强转为 Invocable
对象并调用其 invokeFunction
方法来重复调用它. 另外, 如果你的脚本定了了对象,你可以使用 invokeMethod
方法来调用对象的方法. Example 7 演示了如何使用此接口.
Example 7: 你可以使用 Invocable
接口来调用特定方法.
ScriptEngineManager engineMgr = new ScriptEngineManager();
ScriptEngine jsEngine = engineMgr.getEngineByName("js");
jsEngine.eval("function sayHello() {" +
" print('Hello, world!');" +
"}");
Invocable invocableEngine = (Invocable) jsEngine;
invocableEngine.invokeFunction("sayHello");
Example 7 将会在将 Hello, world!
打印到控制台.
请注意 invokeMethod
和 invokeFunction
方法可能会引发多个异常, 因此你需要必火 ScriptException
, NoSuchMethodException
, 甚至是 NullPointerException
异常.
如何从脚本访问 Java 对象
JSR 223 的实现提供允许访问Java 类,方法,属性的编程语言绑定. 对于该特定脚本环境的原生对象,访问机制通常将遵循脚本语言的约定 .
如何将Java 对象放入脚本环境中?你可以使用Invocable
接口将对象作为参数传递到脚本过程中. 另外, 你也可以在其中 "put" 他们 : Java 编程语言代码可以通过调用脚本引擎的 put
方法将Java对象放置到脚本环境中. 此方法将键值对放入一个 javax.script.Bindings
对象中, 该对象由脚本引擎维护. Bindings
对象是可以从引擎内部访问的 key-value 对的映射.
假设你有一个要处理的脚本名称列表. Example 8 展示了在Java编程语言中需要处理的列表.
Example 8: Java 编程语言添加名称到列表.
List<String> namesList = new ArrayList<String>();
namesList.add("Jill");
namesList.add("Bob");
namesList.add("Laureen");
namesList.add("Ed");
创建名为jsEngine
的 ScriptEngine
对象后, 你可以将 namesList
Java 对象传递到脚本环境中. put
方法需要一个String
和 Object
表示的一个 key-value 对. 在 Example 9, 脚本代码可以使用 namesListKey
引用来访问 namesList
的Java 对象.
Example 9: 脚本代码访问和修改原生的Java 对象.
jsEngine.put("namesListKey", namesList);
System.out.println("Executing in script environment...");
try {
jsEngine.eval("var x;" +
"var names = namesListKey.toArray();" +
"for(x in names) {" +
" print(names[x] );" +
"}" +
"namesListKey.add(\"Dana\");");
} catch (ScriptException ex) {
ex.printStackTrace();
}
System.out.println("Executing in Java environment...");
namesList.stream().forEach(System.out::println);
将 namesListKey
键值对绑定到脚本引擎作用域后 , 可以将Java对象用作脚本对象 . 使用 namesListKey
变量, 脚本可以访问 namesList
对象. 在 Example 9 中 , 脚本打印出列表的名称并添加新的元素 Dana. 从 eval
方法返回后,通过打印 namesList
内容 , 该示例显示脚本已经成功访问和修改了列表.
示例9打印了两次列表 . 脚本打印该列表并添加了一个名称 . 在执行完脚本之后 , 代码再次打印该列表, 表明脚本成功的修改了列表:
Executing in script environment...
Jill
Bob
Laureen
Ed
Executing in Java environment...
Jill
Bob
Laureen
Ed
Dana
你也可以使用Invocable
接口将namesList
对象传递给脚本代码. 脚本代码可以访问和修改通过 Invocable
接口提供的过程参数,而不是使用 key-value 对绑定机制. Example 10 展示老人如何通过 Invocable
接口使用 Java 对象 . 该代码将 namesList
的值作为invokeFunction
方法的参数船体给脚本环境.
Example 10: 应用程序可以使用Invocable
接口将值传递给脚本.
Invocable invocableEngine = (Invocable)jsEngine;
try {
jsEngine.eval("function printNames1(namesList) {" +
" var x;" +
" var names = namesList.toArray();" +
" for(x in names) {" +
" print(names[x]);" +
" }" +
"}" +
"function addName(namesList, name) {" +
" namesList.add(name);" +
"}");
System.out.println("Executing in script environment...");
invocableEngine.invokeFunction("printNames1", namesList);
invocableEngine.invokeFunction("addName", namesList, "Dana");
System.out.println("Executing in Java environment...");
} catch (ScriptException ex) {
ex.printStackTrace();
} catch (NoSuchMethodException ex) {
ex.printStackTrace();
}
你还可以在脚本环境中创建新的 Java 对象. 你的脚本可以使用原生的Java类 . 你可以从脚本中创建一个Swing消息对话框, 替代将消息打印到控制台 , 如 Example 11 所示.
Example 11: 脚本可以导入Java平台程序包.
try {
jsEngine.eval("" +
"var optionPane = " +
" javax.swing.JOptionPane.showMessageDialog(null, 'Hello, world!');");
} catch (ScriptException ex) {
ex.printStackTrace();
}
如何访问脚本对象
eval
, invokeMethod
, 和 invokeFunction
方法始终返回一个 Object
实例. 对于大多数脚本引擎, 此对象是你的脚本计算的最终值. 因此,在脚本环境中访问对象的最简单方法是从脚本函数中返回它们, 或者确保你的脚本计算了你所需的对象.
脚本引擎实现将会某些脚本数据类型映射到Java编程语言中对应的类型 . 类如 , Nashorn 脚本引擎将数字 number
和string
类型映射成Java 编程语言的 Double
and String
类型. 如果你确定返回类型,你可以强制转化 eval
, invokeMethod
, 或者 invokeFunction
方法的返回值. 你应该查阅脚本引擎文档以获取类型映射的详细信息 . 当然 , 你的脚本也可以创建和返回原生Java对象.
总结
JSR 223规范定义了Java平台中的脚本。 Java SE 8平台实现了此规范,JDK 6和JRE 6提供了Mozilla Rhino脚本引擎,JDK 8和JRE 8提供了Nashorn脚本引擎 以支持JavaScript技术。 还可以使用其他脚本引擎,你可以将它们作为常见的JAR扩展添加到您的运行时环境中。
出于多种原因,你可能需要在应用程序中包含脚本支持:
复杂的配置选项.
用户自定义功能.
发布应用程序后易于维护.
用户技术栈 -- 最终用户可能熟悉其他脚本语言,而不是Java编程语言.
重用其他编程语言中的代码模块.
由于脚本的API相对较小,因此使用Java平台的脚本很容易. 你可以仅使用 javax.script
包中少数接口和类将脚本支持快速的添加到应用程序中.
更多信息请参考 JSR 223 specification, documentation, and reference implementation