使用java动态调用动态语言


动态调用动态语言,第 1 部分: 引入 Java 脚本 API 使用 javax.script API 修改运行的应用程序 Tom McQueeney (tom.mcqueeney@gmail.com), 首席技术顾问, Idea Integration 简介: 我们不需要将动态语言编译为 Java™ 字节码就可以在 Java 应用程序中使用它们。使用 Java Platform, Standard Edition 6 (Java SE)中添加的脚本包(并且向后兼容 Java SE 5),Java 代码可以在运行时以一种简单的、 统一的方式调用多种动态语言。本系列文章共分两个部分,第 1 部分将介绍 Java 脚本 API 的各种特性。文章将使用 一个简单的 Hello World 应用程序展示 Java 代码如何执行脚本代码以及脚本如何反过来执行 Java 代码。第 2 部分 将深入研究 Java 脚本 API 的强大功能。 查看本系列更多内容 标记本文! 发布日期: 2007 年 9 月 14 日 级别: 初级 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 对象调用方法,并访问对象的属性,从而使脚本能够 将行为添加到运行中的应用程序中(如果在开发时无法预计应用程序的行为)。 页码,1/10 调用外部脚本可用于运行时应用程序增强、配置、监控或一些其他的运行时操作,比如说在不停止应用程序的情况下 修改业务规则。脚本包可能的作用包括: l 在比 Java 语言更简单的语言中编写业务规则,而不用借助成熟的规则引擎。 l 创建插件架构,使用户能够动态地定制应用程序。 l 将已有脚本集成到 Java 应用程序中,比如说处理或转换文件文章的脚本。 l 使用成熟的编程语言(而不是属性文件)从外部配置应用程序的运行时行为。 l 在 Java 应用程序中添加一门特定于域的语言(domain-specific language)。 l 在开发 Java 应用程序原型的过程中使用脚本语言。 l 在脚本语言中编写应用程序测试代码。 回页首 你好,脚本世界 HelloScriptingWorld 类(本文中的相关代码均可从 下载部分 获得)演示了 Java 脚本包的一些关键特性。它使 用硬编码的 JavaScript 作为示例脚本语言。此类的 main() 方法(如清单 1 所示)将创建一个 JavaScript 脚本引 擎,然后分别调用五个方法(在下文的清单中有显示)用于突出显示脚本包的特性。 清单 1. HelloScriptingWorld main 方法 public static void main(String[] args) throws ScriptException, NoSuchMethodException { ScriptEngineManager scriptEngineMgr = new ScriptEngineManager(); ScriptEngine jsEngine = scriptEngineMgr.getEngineByName("JavaScript"); if (jsEngine == null) { System.err.println("No script engine found for JavaScript"); System.exit(1); } System.out.println("Calling invokeHelloScript..."); invokeHelloScript(jsEngine); System.out.println("\nCalling defineScriptFunction..."); defineScriptFunction(jsEngine); System.out.println("\nCalling invokeScriptFunctionFromEngine..."); invokeScriptFunctionFromEngine(jsEngine); System.out.println("\nCalling invokeScriptFunctionFromJava..."); invokeScriptFunctionFromJava(jsEngine); System.out.println("\nCalling invokeJavaFromScriptFunction..."); invokeJavaFromScriptFunction(jsEngine); 页码,2/10 main() 方法的主要功能是获取一个 javax.script.ScriptEngine 实例(清单 1 中的前两行代码)。脚本引擎可 以在特定的语言中加载并执行脚本。它是 Java 脚本包中使用最为频繁、作用最为重要的类。我们从 javax.script.ScriptEngineManager 获取一个脚本引擎(第一行代码)。通常,程序只需要获取一个脚本引擎 实例,除非使用了很多种脚本语言。 ScriptEngineManager 类 ScriptEngineManager可能是脚本包中惟一一个经常使用的具体类;其他大多数都是接口。它或许是脚本包中惟一 的一个要直接或间接地(通过 Spring Framework 之类的依赖性注入机制)实例化的类。ScriptEngineManager 可 以使用以下三种方式返回脚本引擎: l 通过引擎或语言的名称,比如说 清单 1 请求 JavaScript 引擎。 l 通过该语言脚本共同使用的文件扩展名,比如说 Ruby 脚本的 .rb。 l 通过脚本引擎声明的、知道如何处理的 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 接口 如前所述,代码将使用 ScriptEngine 实例执行脚本。脚本引擎充当脚本代码和最后执行代码的底层语言解释器或编 译器之间的中间程序。这样,我们就不需要了解各个解释器使用哪些类来执行脚本。比如说,JRuby 脚本引擎可以将 代码传递给 JRuby 的 org.jruby.Ruby 类的一个实例,首先将脚本编译成中间形式,然后再调用它计算脚本并处理 返回值。脚本引擎实现隐藏了一些细节,包括解释器如何与 Java 代码共享类定义、应用程序对象和输入/输出流。 图 1 显示了应用程序、Java 脚本 API 和 ScriptEngine 实现、脚本语言解释器之间的总体关系。我们可以看到, 应用程序只依赖于脚本 API,它提供了 ScriptEngineManager 类和 ScriptEngine 接口。ScriptEngine 实现 组件处理使用特定脚本语言解释器的细节。 图 1:脚本 API 组件关系 } 页码,3/10 您可能会问:如何才能获取脚本引擎实现和语言解释器所需的 JAR 文件呢?最好的方法是在 java.net 上托管的开源 Scripting 项目中查找脚本引擎实现(请参阅 参考资料)。您可以在 java.net 上找到许多语言的脚本引擎实现和其他网 站的链接。Scripting 项目还提供了各种链接,通过这些链接可以下载受支持的脚本语言的解释器。 在 清单 1 中,main() 方法将 ScriptEngine 传递给各个方法用于计算该方法的 JavaScript 代码。第一个方法如 清单 2 所示。invokeHelloScript() 方法调用脚本引擎的 eval 方法计算和执行 JavaScript 代码中的特定字符 串。ScriptEngine 接口定义了 6 个重载的 eval() 方法,用于将接收的脚本当作字符串或 java.io.Reader 对 象计算,java.io.Reader 对象一般用于从外部源(例如文件)读取脚本。 清单 2. invokeHelloScript 方法 脚本执行上下文 HelloScriptingWorld 应用程序中的示例脚本 使用 JavaScript println() 函数向控制台输出结果,但是我们拥 有输入和输出流的完全控制权。脚本引擎提供了一个选项用于修改脚本执行的上下文,这意味着我们可以修改标准输 入流、标准输出流和标准错误流,同时还可以定义哪些全局变量和 Java 对象对正在执行的脚本可用。 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 invokeHelloScript(ScriptEngine jsEngine) throws ScriptException { jsEngine.eval("println('Hello from JavaScript')"); } private static void defineScriptFunction(ScriptEngine engine) throws ScriptException { 页码,4/10 这两个方法演示了脚本引擎可以维持应用程序组件的状态,并且能够在后续的 eval() 方法调用过程中使用其状态。 invokeScriptFunctionFromEngine() 方法可以利用所维持的状态,方法是调用定义在 eval() 调用中的 sayHello() JavaScript 函数。 许多脚本引擎在 eval() 调用之间维持全局变量和函数的状态。但是有一点值得格外注意,Java 脚本 API 并不要求 脚本引擎提供这一特性。本文中所使用的 JavaScript、Groovy 和 JRuby 脚本引擎确实在 eval() 调用之间维持了这 些状态。 清单 4 中的代码在前一个示例的基础上做了几分修改。原来的 invokeScriptFunctionFromJava() 方法在调用 sayHello() JavaScript 函数时没有使用 ScriptEngine 的 eval() 方法或 JavaScript 代码。与此不同,清单 4 中的方法使用 Java 脚本 API 的 javax.script.Invocable 接口调用由脚本引擎所维持的函数。 invokeScriptFunctionFromJava() 方法将脚本引擎对象传递给 Invocable 接口,然后对该接口调用 invokeFunction() 方法,最终使用给定的参数调用 sayHello() JavaScript 函数。如果调用的函数需要返回值, 则 invokeFunction() 方法会将值封装为 Java 对象类型并返回。 清单 4. invokeScriptFunctionFromJava 方法 使用代理实现高级脚本调用 当脚本函数或方法实现了一个 Java 接口时,就可以使用高级 Invocable。Invocable 接口定义了一个 getInterface() 方法,该方法使用接口做为参数并且将返回一个实现该接口的 Java 代码对象。从脚本引擎获得代 理对象之后,可以将它作为正常的 Java 对象对待。对该代理调用的方法将委托给脚本引擎通过脚本语言执行。 注意,清单 4 中没有 JavaScript 代码。Invocable 接口允许 Java 代码调用脚本函数,而无需知道其实现语言。如 果脚本引擎无法找到给定名称或参数类型的函数,那么 invokeFunction() 方法将抛出一个 java.lang.NoSuchMethodException。 Java 脚本 API 并不要求脚本引擎实现 Invocable 接口。实际上,清单 4 中的代码应该使用 instanceof 运算符 // Define a function in the script engine engine.eval( "function sayHello(name) {" + " println('Hello, ' + name)" + "}" ); } private static void invokeScriptFunctionFromEngine(ScriptEngine engine) throws ScriptException { engine.eval("sayHello('World!')"); } private static void invokeScriptFunctionFromJava(ScriptEngine engine) throws ScriptException, NoSuchMethodException { Invocable invocableEngine = (Invocable) engine; invocableEngine.invokeFunction("sayHello", "from Java"); } 页码,5/10 确保脚本引擎在转换(cast)之前实现了 Invocable 接口。 通过脚本代码调用 Java 方法 清单 3 和 清单 4 中的示例展示了 Java 代码如何调用脚本语言中定义的函数或方法。您可能会问:脚本语言中编写 的代码是否可以反过来对 Java 对象调用方法呢?答案是可以。清单 5 中的 invokeJavaFromScriptFunction() 方法显示了如何使脚本引擎能够访问 Java 对象,以及脚本代码如何才能对这些 Java 对象调用方法。明确的说, invokeJavaFromScriptFunction() 方法使用脚本引擎的 put() 方法将 HelloScriptingWorld 类的实例本 身提供给引擎。当引擎拥有 Java 对象的访问权之后(使用 put() 调用所提供的名称),eval() 方法脚本中的脚本 代码将使用该对象。 清单 5. invokeJavaFromScriptFunction 和 getHelloReply 方法 清单 5 中的 eval() 方法调用中所包含的 JavaScript 代码使用脚本引擎的 put() 方法调用所提供的变量名称 helloScriptingWorld 访问并使用 HelloScriptingWorld Java 对象。清单 5 中的第二行 JavaScript 代码将 调用 getHelloReply() 公有 Java 方法。getHelloReply() 方法将返回 Java method getHelloReply says, 'Hello, ' 字符串。eval() 方法中的 JavaScript 代码将 Java 返回值赋给 msg 变量,然后 再将其打印输出给控制台。 Java 对象转换 当脚本引擎使运行于引擎环境中的脚本能够使用 Java 对象时,引擎需要将其封装到适用于该脚本语言的对象类型中。 封装可能会涉及到一些适当的对象-值转换,比如说允许 Java Integer 对象直接在脚本语言的数学表达式中使用。 关于如何将 Java 对象转换为脚本对象的研究是与各个脚本语言的引擎特别相关的,并且不在本文的讨论范围之内。但 是,您应该意识到转换的发生,因为可以通过测试来确保所使用的脚本语言执行转换的方式符合您的期望。 ScriptEngine.put 及其相关 get() 方法是在运行于脚本引擎中的 Java 代码和脚本之间共享对象和数据的主要途 径。(有关这一方面的详细论述,请参阅本文后面的 Script-execution scope 一节。)当我们调用引擎的 put() 方法 时,脚本引擎会将第二个参数(任何 Java 对象)关联到特定的字符串关键字。大多数脚本引擎都是让脚本使用特定的 变量名称来访问 Java 对象。脚本引擎可以随意对待传递给 put() 方法的名称。比如说,JRuby 脚本引擎让 Ruby 代 码使用全局 $helloScriptingWorld 对象访问 helloScriptingWorld,以符合 Ruby 全局变量的语法。 脚本引擎的 get() 方法检索脚本环境中可用的值。一般而言,Java 代码通过 get() 方法可以访问脚本环境中的所有 private static void invokeJavaFromScriptFunction(ScriptEngine engine) throws ScriptException { engine.put("helloScriptingWorld", new HelloScriptingWorld()); engine.eval( "println('Invoking getHelloReply method from JavaScript...');" + "var msg = helloScriptingWorld.getHelloReply(vJavaScript');" + "println('Java returned: ' + msg)" ); } /** Method invoked from the above script to return a string. */ public String getHelloReply(String name) { return "Java method getHelloReply says, 'Hello, " + name + "'"; } 页码,6/10 全局变量和函数。但是只有明确使用 put() 与脚本共享的 Java 对象才可以被脚本访问。 外部脚本在运行着的应用程序中访问和操作 Java 对象的这种功能是扩展 Java 程序功能的一项强有力的技巧。(第 2 部分将通过示例研究这一技巧)。 回页首 运行 HelloScriptingWorld 应用程序 您可以通过下载和构建源代码来运行 HelloScriptingWorld 应用程序。此 .zip 中文件含有一个 Ant 脚本和一个 Maven 构建脚本,可以帮助大家编译和运行示例应用程序。请执行以下步骤: 1. 下载 此 .zip 文件。 2. 创建一个新目录,比如说 java-scripting,并将步骤 1 中所下载的文件解压到该目录中。 3. 打开命令行 shell 并转到该目录。 4. 运行 ant run-hello 命令。 您应该可以看到类似于清单 6 的 Ant 控制台输出。注意,defineScriptFunction() 函数没有产生任何输出,因 为它虽然定义了输出但是却没有调用 JavaScript 函数。 清单 6. 运行 HelloScriptingWorld 时的输出 回页首 Java 5 兼容性 Java SE 6 引入了 Java 脚本 API,但是您也可以使用 Java SE 5 运行此 API。只需要提供缺少的 javax.script 包类的一个实现即可。所幸的是,Java Specification Request 223 参考实现中含有这个实现(请参阅 参考资料 获得 Calling invokeHelloScript... Hello from JavaScript Calling defineScriptFunction... Calling invokeScriptFunctionFromEngine... Hello, World! Calling invokeScriptFunctionFromJava... Hello, from Java Calling invokeJavaFromScriptFunction... Invoking getHelloReply method from JavaScript... Java returned: Java method getHelloReply says, 'Hello, JavaScript' 页码,7/10 下载链接。)JSR 223 对 Java 脚本 API 做出了定义。 如果您已经下载了 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 一起编写的外部脚本文件来定义可在运 行时修改的业务逻辑。如您如见,在脚本语言中定义业务规则可以使规则的编写更加轻松,并且更易于程序员之外的 人员阅读,比如说业务分析师或规则编写人员。 回页首 页码,8/10 下载 关于下载方法的信息 参考资料 学习 l 您可以参阅本文在 developerWorks 全球站点上的 英文原文 。 l developerWorks 文章“给 Java SE 注入脚本语言的活力”:介绍了在 Java SE6(Mustang)中所增加的对 脚本语言的支持功能。 l JSR-223: Scripting for the Java Platform:该 JSR 对添加到 Java SE 6 中的 Java 脚本 API 做出了定义。 l Java 脚本程序员指南:Sun 公司的 JDK 6 文档,其中含有 Java 脚本 API 的程序员指南。 l Jakarta Bean Scripting Framework:BSF 项目为 Java 脚本 API 提供了基石。 l “Making Scripting Languages JSR-223-Aware”(Thomas Künneth, java.net, 2006 年 9 月):这篇文章介 绍了当没有脚本引擎可用时如何在 Java 脚本 API 中使用脚本语言。 l Groovy:从 Groovy 项目站点了解更多有关 Groovy 的知识,它是一种用于 Java 平台的敏捷的动态语言。 l 实战 Groovy :要深入研究 Groovy,可以阅读 developerWorks 系列文章。 l Mozilla Rhino:其中含有大量与 JavaScript 引擎有关的文档和参考资料,Sun Microsystems 和 BEA Systems 的 Java 运行时绑定了 JavaScript 引擎。 l JRuby:JRuby 是 Ruby 编程语言的纯 Java 实现。其项目 Web 站点含有最新的项目新闻和有关 JRuby 使 用的参考资料。 l 浏览 技术书店 获得关于各类技术专题的书籍。 l developerWorks Java 技术专区:数百篇关于 Java 编程各个方面的文章。 获得产品和技术 l Groovy:下载最新的 Groovy 发行版。 l Java SE 6 和 BEA JRockit:内部支持 Java 脚本 API 的开发包和运行时环境,其中含有一个简化版本的 Mozilla Rhino JavaScript 引擎。 l Scripting 项目:java.net 上的开源 Scripting 项目提供了二十余种语言的脚本引擎接口,和其他一些知名 描述 名字 大小 下载方法 源代码和 JAR 文件 j-javascripting1.zip 116KB HTTP 页码,9/10 Java 脚本引擎的链接。要使用其中的脚本语言,需要安装此项目中的脚本引擎实现 JAR 文件,和脚本语言解 释器 JAR 文件。 l Scripting for the Java Platform 1.0 参考实现:JSR-223 参考实现提供了三个 JAR 文件,允许 Java 脚本 API 在 Java SE 5 中运行。下载并解压 sjp-1_0-fr-ri.zip 文件,然后将 js.jar、script-api.jar 和 script-js.jar 文 件放置在类路径下。 讨论 l 通过参与 developerWorks 博客 加入 developerWorks 社区。 页码,10/10 动态调用动态语言,第 2 部分: 在运行时寻找、执行和修改脚本 动态地修改业务逻辑 Tom McQueeney (tom.mcqueeney@gmail.com), 首席技术顾问, Idea Integration 简介: Java™ 脚本编程 API(Java scripting API)是 Java SE 6 中新增的,它向后兼容 Java SE 5,支持以一种简单 且统一的方式在运行时从 Java 应用程序调用数十种脚本语言。本系列的 第 1 部分 介绍了这个 API 的基本特性。第 2 部分进一步讲解它的功能,演示如何在无需停止并重新启动应用程序的情况下,在运行时执行外部 Ruby、Groovy 和 JavaScript 脚本以修改业务逻辑。 查看本系列更多内容 标记本文! 发布日期: 2007 年 9 月 24 日 级别: 中级 Java SE 6 中新增的 Java 脚本编程 API 为运行用各种动态语言编写的外部程序(并与之共享代码和数据)提供了一种 统一的方式。Java 应用程序与脚本语言的强大功能和灵活性相结合是非常有意义的,尤其是在脚本语言能够更简洁地执 行某些任务的情况下。但是,Java 脚本编程 API 不仅仅能够以一种统一的方式在 Java 程序中添加许多种脚本语言代 码,它还支持在运行时寻找、读取和执行脚本。可以利用这些动态功能在程序运行时修改脚本,从而修改应用程序的逻 辑。本文演示如何使用 Java 脚本编程 API 调用外部脚本来动态地修改程序逻辑。还要讨论在将一种或多种脚本语言集 成到 Java 应用程序中时可能遇到的问题。 第 1 部分 用一个 Hello World 风格的应用程序介绍了 Java 脚本编程 API。这里将展示一个更真实的示例应用程序, 这个程序使用脚本编程 API 创建一个动态的规则引擎,它可以以外部 Groovy、JavaScript 和 Ruby 脚本的形式定义规 则。这些规则决定申请人是否符合某些抵押产品的住宅贷款条件。如果用脚本语言定义业务规则,规则就更容易编写, 也便于非程序员(比如贷款审查员)阅读。通过使用 Java 脚本编程 API 将这些规则放在程序之外,还可以支持在应用 程序运行时修改规则和添加新的抵押产品。 真实的应用程序 这个示例应用程序为虚构的 Shaky Ground Financial 公司处理住宅贷款申请。住宅抵押行业不断地推出新的贷款产品, 还常常修改对合格申请人的限制规则。Shaky Ground 公司不但希望能够快速地添加和删除抵押产品,还需要快速修改 业务规则,从而控制哪些人符合产品的贷款条件。 Java 脚本编程 API 正好能够满足这种需求。这个应用程序由一个 ScriptMortgageQualifier 类组成,这个类负责 判断打算购买某一资产的贷款人是否符合给定的抵押贷款产品的条件。清单 1 给出这个类。 清单 1. ScriptMortgageQualifier 类 // Imports and Javadoc not shown. public class ScriptMortgageQualifier { private ScriptEngineManager scriptEngineManager = new ScriptEngineManager(); public MortgageQualificationResult qualifyMortgage( Borrower borrower, Property property, Loan loan, 页码,1/17 File mortgageRulesFile ) throws FileNotFoundException, IllegalArgumentException, ScriptException { ScriptEngine scriptEngine = getEngineForFile(mortgageRulesFile); if (scriptEngine == null) { throw new IllegalArgumentException( "No script engine on classpath to handle file: " + mortgageRulesFile ); } // Make params accessible to scripts by adding to engine's context. scriptEngine.put("borrower", borrower); scriptEngine.put("property", property); scriptEngine.put("loan", loan); // Make return-value object available to scripts. MortgageQualificationResult scriptResult = new MortgageQualificationResult(); scriptEngine.put("result", scriptResult); // Add an object scripts can call to exit early from processing. scriptEngine.put("scriptExit", new ScriptEarlyExit()); try { scriptEngine.eval(new FileReader(mortgageRulesFile)); } catch (ScriptException se) { // Re-throw exception unless it's our early-exit exception. if (se.getMessage() == null || !se.getMessage().contains("ScriptEarlyExitException") ) { throw se; } // Set script result message if early-exit exception embedded. Throwable t = se.getCause(); while (t != null) { if (t instanceof ScriptEarlyExitException) { scriptResult.setMessage(t.getMessage()); break; } t = t.getCause(); } } return scriptResult; } /** Returns a script engine based on the extension of the given file. */ private ScriptEngine getEngineForFile(File f) { String fileExtension = getFileExtension(f); return scriptEngineManager.getEngineByExtension(fileExtension); } 页码,2/17 这个类相当简单,因为它把所有业务决策任务都委派给了外部脚本。每个脚本表示一个抵押产品。每个脚本文件中的代 码包含一系列业务规则,这些规则定义了符合这种抵押产品要求的贷款人类型、资产类型和贷款类型。由于采用了这种 方式,只需在脚本目录中添加新的脚本文件,就可以添加新的抵押产品。如果某一抵押产品的业务逻辑改变了,那么只 需更新脚本来反映规则的变化。 通过用脚本语言编写抵押产品业务规则,可以展示 Java 脚本编程 API 的功能。这个程序还说明有时候脚本语言代码更 容易阅读、修改和理解,即使是非程序员也可以掌握脚本代码。 回页首 ScriptMortgageQualifier 类的工作方式 ScriptMortgageQualifier 中的主要方法是 qualifyMortgage()。这个方法通过参数接受以下信息: l 贷款人 l 要购买的资产 l 贷款细节 /** Returns the file's extension, or "" if the file has no extension */ private String getFileExtension(File file) { String scriptName = file.getName(); int dotIndex = scriptName.lastIndexOf('.'); if (dotIndex != -1) { return scriptName.substring(dotIndex + 1); } else { return ""; } } /** Internal exception so ScriptEarlyExit.exit can exit scripts early */ private static class ScriptEarlyExitException extends Exception { public ScriptEarlyExitException(String msg) { super(msg); } } /** Object passed to all scripts so they can indicate an early exit. */ private static class ScriptEarlyExit { public void noMessage() throws ScriptEarlyExitException { throw new ScriptEarlyExitException(null); } public void withMessage(String msg) throws ScriptEarlyExitException { throw new ScriptEarlyExitException(msg); } } } 页码,3/17 l 一个 File 对象,其中包含要执行的脚本 这个方法的任务是用业务实体参数运行脚本文件并返回一个结果对象,这个对象指出贷款人是否符合抵押产品的要求。 这里没有给出 Borrower、Property 和 Loan 的代码。它们只是简单的实体类,可以在本文的源代码中找到它们的代 码(见 下载)。 为了找到一个 ScriptEngine 来运行脚本文件,qualifyMortgage() 方法使用了 getEngineForFile() 内部 helper 方法。getEngineForFile() 方法使用 scriptEngineManager 实例变量(这个变量在类实例化时被设置为 一个 ScriptEngineManager)寻找能够处理具有给定文件扩展名的脚本的脚本引擎。getEngineForFile() 方法 使用 ScriptEngineManager.getEngineByExtension() 方法(见 清单 1 中的粗体代码)搜索并返回 ScriptEngine。 找到脚本引擎之后,qualifyMortgage() 将它接收的实体参数绑定到引擎的上下文,从而让脚本能够使用这些参数。 前三个 scriptEngine.put() 调用(也是粗体代码)执行这些绑定。第四个 scriptEngine.put() 调用创建一个 新的 MortgageQualificationResult Java 对象并通过脚本引擎共享它。脚本可以通过设置这个对象的属性将它的 运行结果返回给 Java 应用程序,qualifyMortgage() 将返回这个共享对象。脚本使用 result 全局变量访问这个 Java 对象。每个脚本负责使用这个共享对象将自己的结果返回给 Java 应用程序。 最后一个 scriptEngine.put() 调用让脚本可以通过 scriptExit 变量使用一个内部 helper 类 (ScriptEarlyExit,见 清单 1)的实例。ScriptEarlyExit 定义了两个简单的方法 —— withMessage() 和 noMessage(),它们惟一的作用是抛出一个异常。如果脚本调用 scriptExit.withMessage() 或 scriptExit.noMessage(),那么方法抛出一个 ScriptEarlyExitException 异常。脚本引擎会捕捉这个异常、 终止脚本处理并向调用脚本的 eval() 方法抛出一个 ScriptException 异常。 通过以这种迂回的方式提前退出脚本,就可以以一致的方式从函数或方法外的脚本处理过程返回。并非所有脚本语言都 提供了这种方式所需的语句。例如,在 JavaScript 中,在执行高层代码时(这个示例应用程序中的抵押处理脚本正是采 用这种构造方式),无法使用 return 语句。共享对象 scriptExit 解决了这个问题,一旦脚本判断出贷款人不符合 抵押产品的要求,用任何语言编写的脚本都可以通过这个对象退出。 在 qualifyMortgage 中,对脚本引擎的 eval 方法的调用(见粗体代码)使用一个 try/catch 块捕捉 ScriptException 异常。catch 块中的代码检查 ScriptException 错误消息,从而判断这个脚本异常是由 ScriptEarlyExitException 造成的,还是由真正的脚本错误造成的。如果错误消息包含名称 ScriptEarlyExitException,那么代码就认为一切正常并忽略这个脚本异常。 这种在 Java 脚本编程 API 的脚本异常错误消息中搜索字符串的技术有点儿笨拙,但这对于本示例中使用的 Groovy、 JavaScript 和 Ruby 语言解释器是有效的。如果所有脚本语言实现将从调用的 Java 代码抛出的 Java 异常添加到异常 堆栈中,那么会更方便,这样就可以使用 Throwable.getCause() 方法获取这些异常。JRuby 和 Groovy 等解释器 会这样做,但是内置的 Rhino JavaScript 解释器并不这样做。 回页首 运行代码:ScriptMortgageQualifierRunner 为了测试 ScriptMortgageQualifier 类,将使用测试数据表示四个贷款人、贷款人打算购买的一项资产和一笔抵押 贷款。我们将用一个贷款人、资产和贷款运行所有三个脚本,检查贷款人是否满足脚本所代表的抵押产品的业务规则。 清单 2 给出 ScriptMortgageQualifierRunner 程序的部分代码,我们将用这个程序创建测试对象、在一个目录中 寻找脚本文件并通过 清单 1 中的 ScriptMortgageQualifier 类运行它们。为了节省篇幅,这里没有给出这个程序 页码,4/17 的 createGoodBorrower()、createAverageBorrower()、createInvestorBorrower()、 createRiskyBorrower()、createProperty() 和 createLoan() helper 方法。这些方法的作用仅仅是创建实体 对象并设置测试所需的值。在 下载 一节中可以获得所有方法的完整源代码。 清单 2. ScriptMortgageQualifierRunner 程序 // Imports and some helper methods not shown. public class ScriptMortgageQualifierRunner { private static File scriptDirectory; private static Borrower goodBorrower = createGoodBorrower(); private static Borrower averageBorrower = createAverageBorrower(); private static Borrower investorBorrower = createInvestorBorrower(); private static Borrower riskyBorrower = createRiskyBorrower(); private static Property property = createProperty(); private static Loan loan = createLoan(); /** * Main method to create a File for the directory name on the command line, * then call the run method if that directory exists. */ public static void main(String[] args) { if (args.length > 0 && args[0].contains("-help")) { printUsageAndExit(); } String dirName; if (args.length == 0) { dirName = "."; // Current directory. } else { dirName = args[0]; } scriptDirectory = new File(dirName); if (!scriptDirectory.exists() || !scriptDirectory.isDirectory()) { printUsageAndExit(); } run(); } /** * Determines mortgage loan-qualification status for four test borrowers by * processing all script files in the given directory. Each script will determine * whether the given borrower is qualified for a particular mortgage type */ public static void run() { ScriptMortgageQualifier mortgageQualifier = new ScriptMortgageQualifier(); for(;;) { // Requires Ctrl-C to exit runQualifications(mortgageQualifier, goodBorrower, loan, property); 页码,5/17 runQualifications(mortgageQualifier, averageBorrower, loan, property); loan.setDownPayment(30000.0); // Reduce down payment to 10% runQualifications(mortgageQualifier, investorBorrower, loan, property); loan.setDownPayment(10000.0); // Reduce down payment to 3 1/3% runQualifications(mortgageQualifier, riskyBorrower, loan, property); waitOneMinute(); } } /** * Reads all script files in the scriptDirectory and runs them with this borrower's * information to see if he/she qualifies for each mortgage product. */ private static void runQualifications( ScriptMortgageQualifier mortgageQualifier, Borrower borrower, Loan loan, Property property ) { for (File scriptFile : getScriptFiles(scriptDirectory)) { // Print info about the borrower, loan and property. System.out.println("Processing file: " + scriptFile.getName()); System.out.println(" Borrower: " + borrower.getName()); System.out.println(" Credit score: " + borrower.getCreditScore()); System.out.println(" Sales price: " + property.getSalesPrice()); System.out.println(" Down payment: " + loan.getDownPayment()); MortgageQualificationResult result = null; try { // Run the script rules for this borrower on the loan product. result = mortgageQualifier.qualifyMortgage( borrower, property, loan, scriptFile ); } catch (FileNotFoundException fnfe) { System.out.println( "Can't read script file: " + fnfe.getMessage() ); } catch (IllegalArgumentException e) { System.out.println( "No script engine available to handle file: " + scriptFile.getName() ); } catch (ScriptException e) { System.out.println( "Script '" + scriptFile.getName() + "' encountered an error: " + e.getMessage() ); 页码,6/17 ScriptMortgageQualifierRunner 中的 main() 方法搜索命令行上提供的脚本文件目录,如果这个目录存在,就 用目录的 File 对象设置一个静态变量,并调用 run() 方法执行进一步的处理。 run() 方法对 清单 1 中的 ScriptMortgageQualifier 类进行实例化,然后用一个无限循环调用内部方法 runQualifications(),测试四个贷款人/贷款场景。这个无限循环模拟连续的抵押申请处理。这个循环让我们可以 在脚本目录中添加或修改脚本文件(抵押贷款产品),这些修改会动态地生效,不需要停止应用程序。因为这个应用程 } if (result == null) continue; // Must have hit exception. // Print results. System.out.println( "* Mortgage product: " + result.getProductName() + ", Qualified? " + result.isQualified() + "\n* Interest rate: " + result.getInterestRate() + "\n* Message: " + result.getMessage() ); System.out.println(); } } /** Returns files with a '.' other than as the first or last character. */ private static File[] getScriptFiles(File directory) { return directory.listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { int indexOfDot = name.indexOf('.'); // Ignore files w/o a dot, or with dot as first or last char. if (indexOfDot < 1 || indexOfDot == (name.length() - 1)) { return false; } else { return true; } } }); } private static void waitOneMinute() { System.out.println( "\nSleeping for one minute before reprocessing files." + "\nUse Ctrl-C to exit..." ); System.out.flush(); try { Thread.sleep(1000 * 60); } catch (InterruptedException e) { System.exit(1); } } } 页码,7/17 序的业务逻辑放在外部脚本中,所以可以在运行时动态地修改业务逻辑。 对于脚本目录中的每个脚本文件,runQualifications() helper 方法分别调用 ScriptMortgageQualifer.qualifyMortgage 一次。每个调用前面有一系列打印语句,它们输出脚本文件和贷款 人的相关信息;调用之后,用打印语句显示结果,即贷款人是否符合抵押产品的要求。脚本代码使用共享的 MortgageQualificationResult Java 对象返回其结果,检查这个对象的属性就可以判断贷款人是否合格。 本文的源代码 ZIP 文件包含三个用 Groovy、JavaScript 和 Ruby 编写的脚本文件示例。它们分别代表一种标准的 30 年期固定利率抵押贷款产品。脚本中的代码判断贷款人是否符合这种抵押类型的要求,然后通过调用脚本引擎 put() 方法中提供的共享全局变量 result 来返回结果。全局变量 result 是 MortgageQualificationResult 类的实 例(部分代码见清单 3)。 清单 3. MortgageQualificationResult 类 脚本设置 result 的属性,从而指出贷款人是否符合抵押贷款的要求以及应该采用的利率。脚本可以通过 message 和 productName 属性指出导致贷款人不合格的原因和返回相关的产品名称。 回页首 脚本文件 在给出 ScriptMortgageQualifierRunner 的输出之前,我们先看看这个程序运行的 Groovy、JavaScript 和 Ruby 脚本文件。Groovy 脚本中的业务逻辑定义了一种条件相当宽松的抵押产品,同时由于金融风险比较高,因此利率 比较高。JavaScript 脚本代表一种政府担保的抵押贷款,这种贷款要求贷款人必须满足最大收入和其他限制。Ruby 脚本 定义的抵押产品业务规则要求贷款人有良好的信用记录,这些人要支付足够的首付款,这种抵押贷款的利率比较低。 清单 4 给出 Groovy 脚本,即使您不了解 Groovy,也应该能够看懂这个脚本。 清单 4. Groovy 抵押脚本 public class MortgageQualificationResult { private boolean qualified; private double interestRate; private String message; private String productName; // .. Standard setters and getters not shown. } /* This Groovy script defines the "Groovy Mortgage" product. This product is relaxed in its requirements of borrowers. There is a higher interest rate to make up for the looser standard. All borrowers will be approved if their credit history is good, they can make a down payment of at least 5%, and they either earn more than $2,000/month or have a net worth (assets minus liabilities) of $25,000. 页码,8/17 请注意全局变量 result、borrower、loan 和 property,脚本使用这些变量访问和设置共享 Java 对象中的值。 这些变量名是通过调用 ScriptEngine.put() 方法设置的。 还要注意 result.productName = 'Groovy Mortgage' 这样的 Groovy 语句。这个语句似乎是直接设置 MortgageQualificationResult 对象的字符串属性 productName,但是,清单 3 清楚地说明它是一个私有的实 例变量。这并不 表示 Java 脚本编程 API 允许违反封装规则,而是说明通过使用 Java 脚本编程 API,Groovy 和大多 数其他脚本语言解释器可以很好地操作共享的 Java 对象。如果一个 Groovy 语句尝试设置或读取 Java 对象的私有属 性值,Groovy 就会寻找并使用 JavaBean 风格的公共 setter 或 getter 方法。例如,语句 result.productName = 'Groovy Mortgage' 会自动转换为适当的 Java 语句:result.setProductName ("Groovy Mortgage")。这个 Java setter 语句也是有效的 Groovy 代码,可以在脚本中使用,但是直接使用属性 赋值语句更符合 Groovy 的风格。 现在看看清单 5 中的 JavaScript 抵押产品脚本。这个 JavaScript 脚本代表一种政府担保的贷款,政府支持这种贷款是 为了提高公民的住宅拥有率。所以,业务规则要求这是贷款人购买的第一套住宅,而且贷款人打算在此居住,而不是出 租获利。 */ // Our product name. result.productName = 'Groovy Mortgage' // Check for the minimum income and net worth def netWorth = borrower.totalAssets - borrower.totalLiabilities if (borrower.monthlyIncome < 2000 && netWorth < 25000) { scriptExit.withMessage "Low monthly income of ${borrower.monthlyIncome}" + ' requires a net worth of at least $25,000.' } def downPaymentPercent = loan.downPayment / property.salesPrice * 100 if (downPaymentPercent < 5) { scriptExit.withMessage 'Down payment of ' + "${String.format('%1$.2f', downPaymentPercent)}% is insufficient." + ' 5% minimum required.' } if (borrower.creditScore < 600) { scriptExit.withMessage 'Credit score of 600 required.' } // Everyone else qualifies. Find interest rate based on down payment percent. result.qualified = true result.message = 'Groovy! You qualify.' switch (downPaymentPercent) { case 0..5: result.interestRate = 0.08; break case 6..10: result.interestRate = 0.075; break case 11..15: result.interestRate = 0.07; break case 16..20: result.interestRate = 0.065; break default: result.interestRate = 0.06; break } 页码,9/17 清单 5. JavaScript 抵押脚本 注意,JavaScript 代码不能像 Groovy 脚本那样使用 Java scriptExit.withMessage() 方法在一个语句中设置不合 格消息并退出脚本。这是因为 Rhino JavaScript 解释器并不把抛出的 Java 异常在 ScriptException 堆栈跟踪中作 为嵌入的 “错误原因” 向上传递。因此,在堆栈跟踪中更难找到 Java 代码抛出的脚本异常消息。所以 清单 5 中的 JavaScript 代码需要单独设置结果消息,然后再调用 scriptExit.noMessage() 来产生异常,从而终止脚本处理。 /** * This script defines the "JavaScript FirstTime Mortgage" product. * It is a government-sponsored mortgage intended for low-income, first-time * home buyers without a lot of assets who intend to live in the home. * Bankruptcies and bad (but not terrible!) credit are OK. */ result.productName = 'JavaScript FirstTime Mortgage' if (!borrower.intendsToOccupy) { result.message = 'This mortgage is not intended for investors.' scriptExit.noMessage() } if (!borrower.firstTimeBuyer) { result.message = 'Only first-time home buyers qualify for this mortgage.' scriptExit.noMessage() } if (borrower.monthlyIncome > 4000) { result.message = 'Monthly salary of $' + borrower.monthlyIncome + ' exceeds the $4,000 maximum.' scriptExit.noMessage() } if (borrower.creditScore < 500) { result.message = 'Your credit score of ' + borrower.creditScore + ' does not meet the 500 requirement.' scriptExit.noMessage() } // Qualifies. Determine interest rate based on loan amount and credit score. result.qualified = true result.message = 'Congratulations, you qualify.' if (loan.loanAmount > 450000) { result.interestRate = 0.08 // Big loans and poor credit require higher rate. } else if (borrower.creditScore < 550) { result.interestRate = 0.08 } else if (borrower.creditScore < 600) { result.interestRate = 0.07 } else if (borrower.creditScore < 700) { result.interestRate = 0.065 } else { // Good credit gets best rate. result.interestRate = 0.06 } 页码,10/17 第三个抵押产品脚本是用 Ruby 编写的,见清单 6。这种抵押产品要求贷款人具有良好的信用记录,他们可以支付百分 之二十的首付款。 清单 6. Ruby 抵押脚本 在 JRuby 1.0 中不要忘记 $ 符号 在 Ruby 脚本中访问共享的 Java 对象时,一定要记住 Ruby 的全局变量语法。如果省略了全局变量前面的 $ 符号,那 么 JRuby 1.0 和当前的 JRuby 1.0.1 二进制版本会抛出一个 RaiseException,而且不提供错误的相关信息。JRuby 源代码存储库中已经纠正了这个 bug,所以在以后的二进制版本中应该不会出现这个问题。 如清单 6 所示,在 Ruby 脚本中,需要在变量名前面加上 $ 符号,这样才能访问放在脚本引擎范围内的共享 Java 对 象。这是 Ruby 的全局变量语法。脚本引擎以全局变量的形式向脚本共享 Java 对象,所以必须使用 Ruby 的全局变量 语法。 # This Ruby script defines the "Ruby Mortgage" product. # It is intended for premium borrowers with its low interest rate # and 20% down payment requirement. # Our product name $result.product_name = 'Ruby Mortgage' # Borrowers with credit unworthiness do not qualify. if $borrower.credit_score < 700 $scriptExit.with_message "Credit score of #{$borrower.credit_score}" + " is lower than 700 minimum" end $scriptExit.with_message 'No bankruptcies allowed' if $borrower.hasDeclaredBankruptcy # Check other negatives down_payment_percent = $loan.down_payment / $property.sales_price * 100 if down_payment_percent < 20 $scriptExit.with_message 'Down payment must be at least 20% of sale price.' end # Borrower qualifies. Determine interest rate of loan $result.message = "Qualified!" $result.qualified = true # Give the best interest rate to the best credit risks. if $borrower.credit_score > 750 || down_payment_percent > 25 $result.interestRate = 0.06 elsif $borrower.credit_score > 700 && $borrower.totalAssets > 100000 $result.interestRate = 0.062 else $result.interestRate = 0.065 end 页码,11/17 还要注意,在调用共享的 Java 对象时,JRuby 会自动地将 Ruby 式代码转换为 Java 式代码。例如,如果 JRuby 发 现代码按照 Ruby 命名约定(即以下划线分隔单词)调用 Java 对象上的方法,比如 $result.product_name = 'Ruby Mortgage',那么 JRuby 会寻找不带下划线的大小写混合式方法名。因此,Ruby 式方法名 product_name= 会正确地转换为 Java 调用 result.setProductName("Ruby Mortgage")。 回页首 程序输出 现在用这三个抵押产品脚本文件运行 ScriptMortgageQualifierRunner 程序,看看它的输出。可以使用源代码下 载文件中的 Ant 脚本运行这个程序。如果喜欢使用 Maven,那么可以按照 ZIP 文件中的 README.txt 文件中的说明用 Maven 构建并运行这个程序。Ant 命令是 ant run。run 任务确保脚本引擎和语言 JAR 文件在类路径中。清单 7 给 出 Ant 的输出。 清单 7. Ant 产生的程序输出 > ant run Buildfile: build.xml compile: [mkdir] Created dir: C:\temp\script-article\build-main\classes [javac] Compiling 10 source files to C:\temp\script-article\build-main\classes run: [java] Processing file: GroovyMortgage.groovy [java] Borrower: Good Borrower [java] Credit score: 800 [java] Sales price: 300000.0 [java] Down payment: 60000.0 [java] * Mortgage product: Groovy Mortgage, Qualified? true [java] * Interest rate: 0.06 [java] * Message: Groovy! You qualify. [java] Processing file: JavaScriptFirstTimeMortgage.js [java] Borrower: Good Borrower [java] Credit score: 800 [java] Sales price: 300000.0 [java] Down payment: 60000.0 [java] * Mortgage product: JavaScript FirstTime Mortgage, Qualified? false [java] * Interest rate: 0.0 [java] * Message: Only first-time home buyers qualify for this mortgage. [java] Processing file: RubyPrimeMortgage.rb [java] Borrower: Good Borrower [java] Credit score: 800 [java] Sales price: 300000.0 页码,12/17 [java] Down payment: 60000.0 [java] * Mortgage product: Ruby Mortgage, Qualified? true [java] * Interest rate: 0.06 [java] * Message: Qualified! [java] Processing file: GroovyMortgage.groovy [java] Borrower: Average Borrower [java] Credit score: 700 [java] Sales price: 300000.0 [java] Down payment: 60000.0 [java] * Mortgage product: Groovy Mortgage, Qualified? true [java] * Interest rate: 0.06 [java] * Message: Groovy! You qualify. [java] Processing file: JavaScriptFirstTimeMortgage.js [java] Borrower: Average Borrower [java] Credit score: 700 [java] Sales price: 300000.0 [java] Down payment: 60000.0 [java] * Mortgage product: JavaScript FirstTime Mortgage, Qualified? false [java] * Interest rate: 0.0 [java] * Message: Monthly salary of $4500 exceeds the $4,000 maximum. [java] Processing file: RubyPrimeMortgage.rb [java] Borrower: Average Borrower [java] Credit score: 700 [java] Sales price: 300000.0 [java] Down payment: 60000.0 [java] * Mortgage product: Ruby Mortgage, Qualified? true [java] * Interest rate: 0.065 [java] * Message: Qualified! [java] Processing file: GroovyMortgage.groovy [java] Borrower: Investor Borrower [java] Credit score: 720 [java] Sales price: 300000.0 [java] Down payment: 30000.0 [java] * Mortgage product: Groovy Mortgage, Qualified? true [java] * Interest rate: 0.06 [java] * Message: Groovy! You qualify. [java] Processing file: JavaScriptFirstTimeMortgage.js [java] Borrower: Investor Borrower [java] Credit score: 720 [java] Sales price: 300000.0 [java] Down payment: 30000.0 [java] * Mortgage product: JavaScript FirstTime Mortgage, Qualified? false [java] * Interest rate: 0.0 [java] * Message: This mortgage is not intended for investors. 页码,13/17 这个输出共有 12 个部分,这是因为程序将四个贷款人示例提交给三个脚本,检查这 12 种组合中贷款人是否符合抵押 产品的要求。为了演示本文解释的技术,这个程序会等待一分钟,然后重复处理抵押脚本。在这段停顿期间,可以编辑 脚本文件来修改业务规则,还可以在脚本目录中添加新的脚本文件来表示新的抵押产品。在每次重复运行时,程序会扫 描脚本目录并处理它找到的所有脚本文件。 例如,假设您希望提高贷款所需的最低信用分数。在一分钟的停顿期间,可以编辑 src/main/scripts/mortgage-products 目录中的 JavaScriptFirstTimeMortgage.js 脚本(见 清单 5),将第 23 行上的业务规则由 if (borrower.creditScore < 500) { 改为 if (borrower.creditScore < 550) {。在下次运行规则时,Risk E. Borrower 就不再符合 JavaScript FirstTime Mortgage 的要求。这个贷款人的信用分数是 520,这个分数低于目前的 条件。错误消息现在是 “Your credit score of 520 does not meet the 500 requirement”,但是同样可以在程序运行时纠 [java] Processing file: RubyPrimeMortgage.rb [java] Borrower: Investor Borrower [java] Credit score: 720 [java] Sales price: 300000.0 [java] Down payment: 30000.0 [java] * Mortgage product: Ruby Mortgage, Qualified? false [java] * Interest rate: 0.0 [java] * Message: Down payment must be at least 20% of sale price. [java] Processing file: GroovyMortgage.groovy [java] Borrower: Risk E. Borrower [java] Credit score: 520 [java] Sales price: 300000.0 [java] Down payment: 10000.0 [java] * Mortgage product: Groovy Mortgage, Qualified? false [java] * Interest rate: 0.0 [java] * Message: Down payment of 3.33% is insufficient. 5% minimum required. [java] Processing file: JavaScriptFirstTimeMortgage.js [java] Borrower: Risk E. Borrower [java] Credit score: 520 [java] Sales price: 300000.0 [java] Down payment: 10000.0 [java] * Mortgage product: JavaScript FirstTime Mortgage, Qualified? true [java] * Interest rate: 0.08 [java] * Message: Congratulations, you qualify. [java] Processing file: RubyPrimeMortgage.rb [java] Borrower: Risk E. Borrower [java] Credit score: 520 [java] Sales price: 300000.0 [java] Down payment: 10000.0 [java] * Mortgage product: Ruby Mortgage, Qualified? false [java] * Interest rate: 0.0 [java] * Message: Credit score of 520 is lower than 700 minimum [java] Sleeping for one minute before reprocessing files. [java] Use Ctrl-C to exit... 页码,14/17 正这个错误的消息。 回页首 避免动态脚本风险 在运行时修改程序的功能是非常强大的,同样也可能导致风险。可以在正在运行的应用程序中添加新的功能和新的业务 规则,而无需停止并重新启动应用程序。同样,也很容易引入新的 bug,甚至是严重的 bug。 但是,动态地修改正在运行的应用程序并不比修改停止运行的应用程序更危险。静态技术仅仅意味着必须重新启动应用 程序,然后才能发现那些新的错误。良好的软件开发实践表明,对生产性应用程序的任何修改(无论是动态的,还是静 态的)都应该先接受测试,然后才能引入生产环境中。Java 脚本编程 API 并未改变这一规则。 外部脚本文件可以在开发期间进行常规的单元测试。可以使用 JUnit 或其他测试工具和模拟 Java 对象来测试脚本,确 保脚本在运行时不会出现错误并产生所期望的结果。将应用程序逻辑放在外部非 Java 脚本文件中并不意味着无法测试 这些脚本。 如果您当过 Web CGI 脚本程序员,那么一定知道必须注意传递给 ScriptEngine 的 eval() 方法的东西。脚本引擎 会立即执行传递给 eval 方法的代码。因此,绝不要把来自不可信来源的字符串或 Reader 对象传递给脚本引擎。 例如,假设我们使用脚本编程 API 远程监视一个 Web 应用程序。我们让脚本引擎能够访问关键的 Java 对象,这些对 象提供 Web 应用程序的状态信息。还创建一个简单的 Web 页面,这个页面接受任意脚本表达式,它将这些表达式传 递给脚本引擎进行计算并在 Web 页面上显示输出。这样就可以对正在运行的 Java 对象进行查询并执行对象上的方 法,从而帮助判断应用程序的状态。 但是,在这种情况下,能够访问这个 Web 页面的任何人都可以执行任意脚本语句,可以访问任意共享 Java 对象。编 程时的失误、错误的配置和安全漏洞会把机密信息泄露给未授权用户,或者让应用程序遭遇拒绝服务攻击(例如,攻击 者可以执行与 System.exit 或 /bin/rm -fr / 等效的脚本语句)。与任何强大的工具一样,Java 脚本编程 API 要求您保持谨慎,注意安全。 回页首 进一步开拓的方向 本文主要关注让 Java 应用程序能够在运行时动态地读取并执行外部脚本,以及让脚本能够访问显式提供给它们的 Java 对象。Java 脚本编程 API 还提供了其他特性。例如: l 可以使用脚本语言实现一个 Java 接口,然后像使用任何其他 Java 接口引用一样从 Java 代码调用脚本代码。 l 可以在脚本中实例化并使用 Java 对象,还可以让 Java 应用程序能够访问这些对象。 l 可以在装载动态脚本时进行预编译,这可以让以后的执行过程更快。 l 可以设置脚本使用的输入流和输出流,这样就很容易将文件用作脚本的控制台输入源,以及将脚本的控制台输 出转发到文件或其他流。 l 可以设置位置参数,脚本可以将这些参数用作命令行参数。 Java 脚本编程 API 定义了脚本引擎可以选择实现的一些功能,所以并非所有脚本引擎都提供这些功能。在 参考资料 页码,15/17 中可以找到关于这些特性和其他特性的读物和在线参考资料。 回页首 下载 关于下载方法的信息 参考资料 学习 l 您可以参阅本文在 developerWorks 全球站点上的 英文原文 。 l “动态调用动态语言,第 1 部分”:这篇文章介绍了 Java 脚本编程 API 的特性,并用一个简单的应用程序演 示了 Java 代码如何执行脚本代码以及脚本如何执行 Java 代码。 l JSR-223: Scripting for the Java Platform:这个 Java 规范提案定义了 Java SE 6 中添加的 Java 脚本编程 API。 l Java Scripting Programmer's Guide:Sun 的 JDK 6 文档包含一份 Java 脚本编程 API 程序员指南。 l Jakarta Bean Scripting Framework:BSF 项目提供了 Java 脚本编程 API 的基础。 l “Making Scripting Languages JSR-223-Aware”(Thomas Künneth,java.net,2006 年 9 月):这篇文章讲 解了在没有脚本引擎可用的情况下,如何让 Java 脚本编程 API 可以使用某种脚本语言。 l Groovy:通过 Groovy 项目 Web 站点进一步了解这种用于 Java 平台的敏捷的动态语言。 l 实战 Groovy :要深入研究 Groovy,可以阅读 developerWorks 系列文章。 l Mozilla Rhino:通过文档和其他参考资料进一步了解这种 JavaScript 引擎,这种引擎包含 Sun Microsystems 和 BEA Systems 提供的 Java 运行时。 l JRuby:JRuby 是 Ruby 编程语言的一种纯 Java 实现。项目的 Web 站点提供最新的项目新闻和有关 JRuby 使用方法的其他参考资料。 l 在 technology bookstore 浏览关于这些主题和其他技术主题的图书。 l developerWorks Java 技术专区:这里提供数百篇关于 Java 编程各个方面的文章。 描述 名字 大小 下载方法 源代码和所有 JAR 文件 java-scripting-part2.zip 4.5MB HTTP 页码,16/17 获得产品和技术 l Groovy:下载最新的 Groovy 版本。 l Java SE 6 和 BEA JRockit:这些开发工具包和运行时环境支持 Java 脚本编程 API,还包含 Mozilla Rhino JavaScript 引擎的一个简化版本。 l Scripting 项目:java.net 上的开放源码 Scripting 项目为 20 多种语言提供了脚本引擎接口,并链接到其他著名 的 Java 脚本引擎。要想使用这些脚本语言之一,只需安装这个项目提供的脚本引擎实现 JAR 文件以及脚本语 言解释器 JAR 文件。 l Scripting for the Java Platform 1.0 Reference Implementation:JSR-223 参考实现提供三个 JAR 文件,支持 在 Java SE 5 中运行 Java 脚本编程 API。下载并解压 sjp-1_0-fr-ri.zip 文件,将 js.jar、script-api.jar 和 script-js.jar 文件放在自己的类路径中。 讨论 l 通过参与 developerWorks blog 加入 developerWorks 社区。 页码,17/17
还剩26页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 3 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

nsdy123

贡献于2013-07-04

下载需要 3 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf