- 讲师:刘萍萍 / 谢楠
- 课时:160h
- 价格 4580 元
特色双名师解密新课程高频考点,送国家电网教材讲义,助力一次通关
配套通关班送国网在线题库一套
百度广告
Java 开发人员清楚 Java 并不是在任何情况下都是最佳的语言。今年,1.0 版本的 JRuby 和 Groovy 的发行引领了一场热潮,促使人们纷纷在自己的 Java 应用程序中添加动态语言。Groovy、JRuby、Rhino、Jython 和一些其他的开源项目使在所谓的脚本语言中编写代码并在 JVM 中运行成为了可能(请参阅 参考资料)。通常,在 Java 代码中集成这些语言需要对各种解释器所特有的 API 和特性有所了解。
Java SE 6 中添加的 javax.script 包使集成动态语言更加容易。通过使用一小组接口和具体类,这个包使我们能够简单地调用多种脚本语言。但是,Java 脚本 API 的功能不只是在应用程序中编写脚本;这个脚本包使我们能够在运行时读取和调用外部脚本,这意味着我们可以动态地修改这些脚本从而更改运行应用程序的行为。
Java 脚本 API
脚本与动态的对比
术语脚本 通常表示在解释器 shell 中运行的语言,它们往往没有单独的编译步骤。术语动态 通常表示等到运行时判断变量类型或对象行为的语言,往往具有闭包和连续特性。一些通用的编程语言同时具有这两种特性。此处首选脚本语言 是因为的着重点是 Java 脚本 API,而不是因为提及的语言缺少动态特性。
2006 年 10 月,Java 语言添加了脚本包,从而提供了一种统一的方式将脚本语言集成到 Java 应用程序中去。对于语言开发人员,他们可以使用这个包编写粘连代码(glue code),从而使人们能够在 Java 应用程序中调用他们的语言。对于 Java 开发人员,脚本包提供了一组类和接口,允许使用一个公共 API 调用多种语言编写的脚本。因此,脚本包类似于不同语言(比如说不同的数据库)中的 Java Database Connectivity (JDBC) 包,可以使用一致的接口集成到 Java 平台中去。
以前,在 Java 代码中,动态调用脚本语言涉及到使用各种语言发行版所提供的独特类或使用 Apache 的 Jakarta Bean Scripting Framework (BSF)。BSF 在一个 API 内部统一了一组脚本语言(请参阅 参考资料)。使用 Java SE 6 脚本 API,二十余种脚本语言(AppleScript、Groovy、JavaScript、Jelly、PHP、Python、Ruby 和 Velocity)都可以集成到 Java 代码中,这在很大程序上依赖的是 BSF。
脚本 API 在 Java 应用程序和外部脚本之间提供了双向可见性。Java 代码不仅可以调用外部脚本,而且还允许那些脚本访问选定的 Java 对象。比如说,外部 Ruby 脚本可以对 Java 对象调用方法,并访问对象的属性,从而使脚本能够将行为添加到运行中的应用程序中(如果在开发时无法预计应用程序的行为)。
调用外部脚本可用于运行时应用程序增强、配置、监控或一些其他的运行时操作,比如说在不停止应用程序的情况下修改业务规则。脚本包可能的作用包括:
·在比 Java 语言更简单的语言中编写业务规则,而不用借助成熟的规则引擎。
·将已有脚本集成到 Java 应用程序中,比如说处理或转换文件文章的脚本。
·在 Java 应用程序中添加一门特定于域的语言(domain-specific language)。
·在脚本语言中编写应用程序测试代码。
你好,脚本世界
HelloScriptingWorld 类(中的相关代码均可从 下载部分 获得)演示了 Java 脚本包的一些关键特性。它使用硬编码的 JavaScript 作为示例脚本语言。此类的 main() 方法(如清单 1 所示)将创建一个 JavaScript 脚本引擎,然后分别调用五个方法(在下文的清单中有显示)用于突出显示脚本包的特性。
清单 1. HelloScriptingWorld main 方法
public static void main(String args) throws ScriptException, NoSuchMethodException {
ScriptEngineManager scriptEngineMgr = new ScriptEngineManager(); if (jsEngine == null) { System.out.println("Calling invokeHelloScript..."); System.out.println(""nCalling defineScriptFunction..."); System.out.println(""nCalling invokeScriptFunctionFromEngine..."); System.out.println(""nCalling invokeScriptFunctionFromJava..."); System.out.println(""nCalling invokeJavaFromScriptFunction..."); |
ScriptEngineManager 可能是脚本包中惟一一个经常使用的具体类;其他大多数都是接口。它或许是脚本包中惟一的一个要直接或间接地(通过 Spring Framework 之类的依赖性注入机制)实例化的类。ScriptEngineManager 可以使用以下三种方式返回脚本引擎:
·通过引擎或语言的名称,比如说 清单 1 请求 JavaScript 引擎。
·通过脚本引擎声明的、知道如何处理的 MIME 类型。
示例为什么要使用 JavaScript?
中的 Hello World 示例使用了部分 JavaScript 脚本,这是因为 JavaScript 代码易于理解,不过主要还是因为 Sun Microsystems 和 BEA Systems 所提供的 Java 6 运行时环境附带有基于 Mozilla Rhino 开源 JavaScript 实现的 JavaScript 解释器。使用 JavaScript,我们无需在类路径中添加脚本语言 JAR 文件。
ScriptEngineManager 间接查找和创建脚本引擎。也就是说,当实例化脚本引擎管理程序时,ScriptEngineManager 会使用 Java 6 中新增的服务发现机制在类路径中查找所有注册的 javax.script.ScriptEngineFactory 实现。这些工厂类封装在 Java 脚本 API 实现中;也许您永远都不需要直接处理这些工厂类。
ScriptEngineManager 找到所有的脚本引擎工厂类之后,它会查询各个类并判断是否能够创建所请求类型的脚本引擎 -- 清单 1 中为 JavaScript 引擎。如果工厂说可以创建所需语言的脚本引擎,那么管理程序将要求工厂创建一个引擎并将其返回给调用者。如果没有找到所请求语言的工厂,那么管理程序将返回 null,清单 1 中的代码将检查 null 返回值并做出预防。|||
如前所述,代码将使用 ScriptEngine 实例执行脚本。脚本引擎充当脚本代码和最后执行代码的底层语言解释器或编译器之间的中间程序。这样,我们就不需要了解各个解释器使用哪些类来执行脚本。比如说,JRuby 脚本引擎可以将代码传递给 JRuby 的 org.jruby.Ruby 类的一个实例,首先将脚本编译成中间形式,然后再调用它计算脚本并处理返回值。脚本引擎实现隐藏了一些细节,包括解释器如何与 Java 代码共享类定义、应用程序对象和输入/输出流。
图 1 显示了应用程序、Java 脚本 API 和 ScriptEngine 实现、脚本语言解释器之间的总体关系。我们可以看到,应用程序只依赖于脚本 API,它提供了 ScriptEngineManager 类和 ScriptEngine 接口。ScriptEngine 实现组件处理使用特定脚本语言解释器的细节。
|
清单 2. invokeHelloScript 方法
private static void invokeHelloScript(ScriptEngine jsEngine) throws ScriptException { } |
invokeHelloScript() 方法中的 JavaScript 将 Hello from JavaScript 输出到标准输出流,在本例中为控制台窗口。(清单 6 含有运行 HelloScriptingWorldApplication 时的完整输出。)
注意,类中的这一方法和其他方法都声明抛出了 javax.script.ScriptException。这个选中的异常(脚本包中定义的惟一一个异常)表示引擎无法解析或执行给定的代码。所有脚本引擎 eval() 方法都声明抛出一个 ScriptException,因此我们的代码需要适当处理这些异常。
清单 3 显示了两个有关的方法:defineScriptFunction() 和 invokeScriptFunctionFromEngine()。defineScriptFunction() 方法还使用一段硬编码的 JavaScript 代码调用脚本引擎的 eval() 方法。但是有一点需要注意,该方法的所有工作只是定义了一个 JavaScript 函数 sayHello()。并没有执行任何代码。sayHello() 函数只有一个参数,它会使用 println() 语句将这个参数输出到控制台。脚本引擎的 JavaScript 解释器将这个函数添加到全局环境,以供后续的 eval 调用使用(该调用发生在 invokeScriptFunctionFromEngine() 方法中,这并不奇怪)。
清单 3. defineScriptFunction 和 invokeScriptFunctionFromEngine 方法
private static void defineScriptFunction(ScriptEngine engine) throws ScriptException { engine.eval( " println('Hello, ' + name)" + ); private static void invokeScriptFunctionFromEngine(ScriptEngine engine) { } |
清单 4 中的代码在前一个示例的基础上做了几分修改。原来的 invokeScriptFunctionFromJava() 方法在调用 sayHello() JavaScript 函数时没有使用 ScriptEngine 的 eval() 方法或 JavaScript 代码。与此不同,清单 4 中的方法使用 Java 脚本 API 的 javax.script.Invocable 接口调用由脚本引擎所维持的函数。invokeScriptFunctionFromJava() 方法将脚本引擎对象传递给 Invocable 接口,然后对该接口调用 invokeFunction() 方法,最终使用给定的参数调用 sayHello() JavaScript 函数。如果调用的函数需要返回值,则 invokeFunction() 方法会将值封装为 Java 对象类型并返回。
清单 4. invokeScriptFunctionFromJava 方法
private static void invokeScriptFunctionFromJava(ScriptEngine engine) { invocableEngine.invokeFunction("sayHello", "from Java"); |
注意,清单 4 中没有 JavaScript 代码。Invocable 接口允许 Java 代码调用脚本函数,而无需知道其实现语言。如果脚本引擎无法找到给定名称或参数类型的函数,那么 invokeFunction() 方法将抛出一个 java.lang.NoSuchMethodException。
Java 脚本 API 并不要求脚本引擎实现 Invocable 接口。实际上,清单 4 中的代码应该使用 instanceof 运算符确保脚本引擎在转换(cast)之前实现了 Invocable 接口。|||
清单 3 和 清单 4 中的示例展示了 Java 代码如何调用脚本语言中定义的函数或方法。您可能会问:脚本语言中编写的代码是否可以反过来对 Java 对象调用方法呢?答案是可以。清单 5 中的 invokeJavaFromScriptFunction() 方法显示了如何使脚本引擎能够访问 Java 对象,以及脚本代码如何才能对这些 Java 对象调用方法。明确的说,invokeJavaFromScriptFunction() 方法使用脚本引擎的 put() 方法将 HelloScriptingWorld 类的实例本身提供给引擎。当引擎拥有 Java 对象的访问权之后(使用 put() 调用所提供的名称),eval() 方法脚本中的脚本代码将使用该对象。
清单 5. invokeJavaFromScriptFunction 和 getHelloReply 方法
private static void invokeJavaFromScriptFunction(ScriptEngine engine) { engine.eval( "var msg = helloScriptingWorld.getHelloReply(vJavaScript');" + ); /** Method invoked from the above script to return a string. */ return "Java method getHelloReply says, 'Hello, " + name + "'"; |
当脚本引擎使运行于引擎环境中的脚本能够使用 Java 对象时,引擎需要将其封装到适用于该脚本语言的对象类型中。封装可能会涉及到一些适当的对象-值转换,比如说允许 Java Integer 对象直接在脚本语言的数学表达式中使用。关于如何将 Java 对象转换为脚本对象的研究是与各个脚本语言的引擎特别相关的,并且不在的讨论范围之内。但是,您应该意识到转换的发生,因为可以通过测试来确保所使用的脚本语言执行转换的方式符合您的期望。
ScriptEngine.put 及其相关 get() 方法是在运行于脚本引擎中的 Java 代码和脚本之间共享对象和数据的主要途径。(有关这一方面的详细论述,请参阅后面的 Script-execution scope 一节。)当我们调用引擎的 put() 方法时,脚本引擎会将第二个参数(任何 Java 对象)关联到特定的字符串关键字。大多数脚本引擎都是让脚本使用特定的变量名称来访问 Java 对象。脚本引擎可以随意对待传递给 put() 方法的名称。比如说,JRuby 脚本引擎让 Ruby 代码使用全局 $helloScriptingWorld 对象访问 helloScriptingWorld,以符合 Ruby 全局变量的语法。
脚本引擎的 get() 方法检索脚本环境中可用的值。一般而言,Java 代码通过 get() 方法可以访问脚本环境中的所有全局变量和函数。但是只有明确使用 put() 与脚本共享的 Java 对象才可以被脚本访问。
外部脚本在运行着的应用程序中访问和操作 Java 对象的这种功能是扩展 Java 程序功能的一项强有力的技巧。(第 2 部分将通过示例研究这一技巧)。
运行 HelloScriptingWorld 应用程序
您可以通过下载和构建源代码来运行 HelloScriptingWorld 应用程序。此 .zip 中文件含有一个 Ant 脚本和一个 Maven 构建脚本,可以帮助大家编译和运行示例应用程序。请执行以下步骤:
·下载 此 .zip 文件。
·打开命令行 shell 并转到该目录。
您应该可以看到类似于清单 6 的 Ant 控制台输出。注意,defineScriptFunction() 函数没有产生任何输出,因为它虽然定义了输出但是却没有调用 JavaScript 函数。
清单 6. 运行 HelloScriptingWorld 时的输出
Calling invokeHelloScript... Calling defineScriptFunction... Calling invokeScriptFunctionFromEngine... Calling invokeScriptFunctionFromJava... Calling invokeJavaFromScriptFunction... |
如果您已经下载了 JSR 223 参考实现,解压下载文件并将 script-api.jar、script-js.jar 和 js.jar 文件复制到您的类路径下。这些文件将提供脚本 API、JavaScript 脚本引擎接口和 Java SE 6 中所附带的 JavaScript 脚本引擎。
脚本执行作用域
与简单地调用引擎的 get() 和 put() 方法相比,如何将 Java 对象公开给运行于脚本引擎中的脚本具有更好的可配置性。当我们在脚本引擎上调用 get() 或 put() 方法时,引擎将会在 javax.script.Bindings 接口的默认实例中检索或保存所请求的关键字。(Bindings 接口只是一个 Map 接口,用于强制关键字为字符串。)
当代码调用脚本引擎的 eval() 方法时,将使用引擎默认绑定的关键字和值。但是,您可以为 eval() 调用提供自己的 Bindings 对象,以限制哪些变量和对象对于该特定脚本可见。该调用外表上类似于 eval(String, Bindings) 或 eval(Reader, Bindings)。要帮助您创建自定义的 Bindings,脚本引擎将提供一个 createBindings() 方法,该方法和返回值是一个内容为空的 Bindings 对象。使用 Bindings 对象临时调用 eval 将隐藏先前保存在引擎默认绑定中的 Java 对象。
要添加功能,脚本引擎含有两个默认绑定:其一为 get() 和 put() 调用所使用的 "引擎作用域" 绑定 ;其二为 "全局作用域" 绑定,当无法在 "引擎作用域" 中找到对象时,引擎将使用第二种绑定进行查找。脚本引擎并不需要使脚本能够访问全局绑定。大多数脚本都可以访问它。
"全局作用域" 绑定的设计目的是在不同的脚本引擎之间共享对象。ScriptEngineManager 实例返回的所有脚本引擎都是 "全局作用域" 绑定对象。您可以使用 getBindings(ScriptContext.GLOBAL_SCOPE) 方法检索某个引擎的全局绑定,并且可以使用 setBindings(Bindings, ScriptContext.GLOBAL_SCOPE) 方法为引擎设置全局绑定。
ScriptContext 是一个定义和控制脚本引擎运行时上下文的接口。脚本引擎的 ScriptContext 含有 "引擎" 和 "全局" 作用域绑定,以及用于标准输入和输出操作的输入和输出流。您可以使用引擎的 getContext() 方法获取并操作脚本引擎的上下文。
一些脚本 API 概念,比如说作用域、绑定 和上下文,开始看来会令人迷惑,因为它们的含义有交叉的地方。的源代码下载文件含有一个名为 ScriptApiRhinoTest 的 JUnit 测试文件,位于 src/test/java directory 目录,该文件可以通过 Java 代码帮助解释这些概念。
未来的计划
现在,大家已经对 Java 脚本 API 有了最基本的认识,本系列文章的第 2 部分将在此基础上进行扩展,为大家演示一个更为实际的示例应用程序。该应用程序将使用 Groovy、Ruby 和 JavaScript 一起编写的外部脚件来定义可在运行时修改的业务逻辑。如您如见,在脚本语言中定义业务规则可以使规则的编写更加轻松,并且更易于程序员之外的人员阅读,比如说业务分析师或规则编写人员。
责编:罗莉
课程专业名称 |
讲师 |
课时 |
查看课程 |
---|
课程专业名称 |
讲师 |
课时 |
查看课程 |
---|
点击加载更多评论>>