Java开发人员知道Java语言并不总是每种任务的最佳语言。 今年的JRuby和Groovy的1.0版本发行了对向Java应用程序添加动态语言的兴趣。 借助Groovy,JRuby,Rhino,Jython和其他开源项目,可以使用所谓的脚本语言编写代码并在JVM中运行它(请参阅参考资料 )。 然而,将这些语言与Java代码集成通常意味着学习每个解释器的独特API和功能。
添加到Java SE 6的javax.script
包使集成动态语言更加容易。 它提供了一种简单的方法,即使用一小组接口和具体类来调用多种脚本语言。 但是,Java脚本API不仅能使编写应用程序的各个部分的脚本变得容易,而且还具有更多的功能。 脚本包允许您在运行时读取和调用外部脚本,这意味着您可以动态更改这些脚本以更改正在运行的应用程序的行为。
本文是由两部分组成的系列文章的第一篇,介绍了使用Hello World风格的应用程序的Java脚本API的功能和键类。 第2部分提供了一个更实际的示例应用程序,展示了脚本API的更多功能。 该应用程序使用脚本API来创建动态规则引擎,其中规则被编码为用Groovy,Javascript和Ruby编写的外部脚本。 该规则决定了房屋贷款的申请人是否符合特定抵押产品的条件。 使用Java脚本API将规则外部化,可以更改规则并在运行时添加新的抵押产品。
脚本一词通常是指从解释程序外壳运行而无需单独的编译步骤的语言。 术语“ 动态”通常是指等到运行时才能确定变量类型或对象行为的语言,并包含诸如闭包和连续符之类的功能。 几种通用编程语言都符合这两个术语。 此处首选使用脚本语言 ,因为本文重点关注Java脚本API,而不是因为提到的语言缺乏动态功能。
该脚本包已于2006年12月添加到Java语言中,以提供一种将脚本语言集成到Java应用程序中的统一方法。 对于语言开发人员来说,该软件包提供了一种编写必要的粘合代码的方法,以使它们的语言可以从Java应用程序中动态调用。 对于Java开发人员,脚本包提供了一小组类和接口,这些类和接口允许使用通用API调用以多种语言编写的脚本。 因此,脚本包类似于Java数据库连接(JDBC)包,因为可以使用一致的接口将不同的语言(如不同的数据库)集成到Java平台中。
以前,涉及到Java语言来动态调用脚本语言,这涉及使用每种语言的发行版提供的独特类或使用Apache的Jakarta Bean脚本框架(BSF)。 BSF统一了单个API背后的几种脚本语言(请参阅参考资料 )。 使用主要基于BSF的Java SE 6脚本API,可以将二十多种脚本语言(包括AppleScript,Groovy,Javascript,Jelly,PHP,Python,Ruby和Velocity)集成到Java代码中。
脚本API提供了Java应用程序和外部脚本之间的双向可见性。 您的Java代码不仅可以调用外部脚本,还可以使这些脚本访问选定的Java对象。 例如,外部Ruby脚本可以调用Java对象上的方法并访问其属性,从而允许这些脚本向正在运行的应用程序添加开发时无法预期的行为。
调用外部脚本可用于运行时应用程序增强,配置,监视或其他运行时操作,例如在不停止应用程序的情况下更改业务规则。 脚本包的可能用途包括:
您可以将HelloScriptingWorld
类与本文的所有代码一起下载 (请参阅下载 ),该类演示了Java脚本包的主要功能。 它使用Javascript的硬编码片段作为示例脚本语言。 清单1中所示,该类的main()
方法创建一个Javascript脚本引擎,然后调用五个方法(如下清单所示),这些方法突出显示脚本包的功能:
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);
}
main()
方法的主要功能是获取javax.script.ScriptEngine
的实例( 清单1中的前两个语句)。 脚本引擎以特定语言加载和执行脚本。 它是Java脚本包中最常用和最重要的类。 您可以从javax.script.ScriptEngineManager
(第一条语句)获得脚本引擎。 除非使用许多脚本语言,否则典型程序仅需要获取脚本引擎的一个实例。
ScriptEngineManager
可能是您将定期使用的脚本包中唯一的具体类。 其余大多数是接口。 并且它可能是脚本包中唯一要直接实例化的类(或通过诸如Spring Framework这样的依赖注入机制间接实例化) ScriptEngineManager
可以通过以下三种方式之一返回脚本引擎:
Javascript
引擎。 这个Hello World示例使用Javascript的部分原因是代码易于理解,但是主要是因为Sun Microsystems和BEA Systems提供的Java 6运行时环境与基于Mozilla Rhino开源Javascript实现Javascript解释器捆绑在一起。 使用Javascript,您无需将脚本语言的JAR文件添加到类路径。
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,该API提供ScriptEngineManager
类和ScriptEngine
接口。 ScriptEngine
实现组件处理使用特定脚本语言解释器的细节。
您可能想知道在哪里可以获取脚本引擎实现和语言解释器所需的JAR文件。 脚本引擎实现的最佳选择首先是由java.net托管的开源Scripting项目(请参阅参考资料 )。 在这里,您会找到多种语言的脚本引擎实现,以及指向其他位置托管的脚本引擎实现的链接。 脚本项目还提供了下载解释器的链接,以支持其支持的脚本语言。
在清单1中 , main()
方法将ScriptEngine
传递给每个方法,以用于评估该方法Javascript代码。 清单2中显示了第一个方法invokeHelloScript()
方法调用脚本引擎的eval
方法来评估和执行给定Javascript代码字符串。 ScriptEngine
接口定义了六个重载的eval()
方法,这些方法接受一个脚本来评估为字符串还是java.io.Reader
对象,该对象通常用于从文件等外部来源读取脚本。
private static void invokeHelloScript(ScriptEngine jsEngine) throws ScriptException {jsEngine.eval("println('Hello from Javascript')");
}
HelloScriptingWorld
应用程序中的示例脚本使用Javascript println()
函数输出到控制台,但是您可以完全控制输入和输出流。 脚本引擎提供了更改脚本执行上下文的选项,这意味着您可以更改用于标准输入,标准输出和标准错误的流,以及定义哪些全局变量和Java对象可用于正在执行的脚本。
invokeHelloScript()
方法中的Hello from Javascript
将Hello from Javascript
信号输出到标准输出流,在本例中为控制台窗口。 ( 清单6包含了运行HelloScriptingWorldApplication
的完整输出。)
注意,该类中的this和其他方法声明它们抛出javax.script.ScriptException
。 此已检查的异常-由脚本包定义的唯一异常-表明引擎未能解析或执行给定的代码。 所有脚本引擎的eval()
方法都声明它们抛出ScriptException
,因此您的代码需要适当地处理它。
清单3显示了两个相关方法: defineScriptFunction()
和invokeScriptFunctionFromEngine()
。 defineScriptFunction()
方法还使用Javascript的硬编码代码段调用脚本引擎的eval()
方法。 但是请注意,此方法只定义了一个Javascript函数sayHello()
。 没有代码正在执行。 sayHello()
函数采用一个参数,该参数在下面的println()
语句中输出到控制台。 脚本引擎Javascript解释器将此函数添加到其全局环境中,使其可在随后的eval
调用中使用(这并不奇怪)在invokeScriptFunctionFromEngine()
方法中。
private static void defineScriptFunction(ScriptEngine engine) throws ScriptException {// Define a function in the script engineengine.eval("function sayHello(name) {" +" println('Hello, ' + name)" +"}");
}private static void invokeScriptFunctionFromEngine(ScriptEngine engine)throws ScriptException
{engine.eval("sayHello('World!')");
}
这对方法演示了脚本引擎可以维护应用程序组件的状态,并使该状态在后续调用引擎的eval()
方法期间可用。 invokeScriptFunctionFromEngine()
方法通过调用在先前对eval()
调用中定义的sayHello()
Javascript函数来利用保持状态的优势。
许多脚本引擎在调用eval()
之间维护全局变量和函数的状态。 但是,请务必注意,Java脚本API不需要脚本引擎来提供此功能。 本文中使用Javascript,Groovy和JRuby脚本引擎确实在eval()
调用之间保持状态。
清单4是前面示例的变形。 invokeScriptFunctionFromJava()
方法的不同之处在于,它无需使用ScriptEngine
的eval()
方法或Javascript代码即可调用sayHello()
Javascript函数。 相反,它使用Java脚本API的javax.script.Invocable
接口来调用由脚本引擎维护的函数。 invokeScriptFunctionFromJava()
方法将脚本引擎对象强制转换为Invocable
接口,然后在该接口上调用invokeFunction()
方法,以使用给定的参数调用sayHello()
Javascript函数。 如果被调用的函数返回一个值,则invokeFunction()
方法将其包装为Java Object
类型返回。
private static void invokeScriptFunctionFromJava(ScriptEngine engine)throws ScriptException, NoSuchMethodException
{
Invocable invocableEngine = (Invocable) engine;invocableEngine.invokeFunction("sayHello", "from Java");
}
当脚本函数或方法实现Java接口时,可以使用Invocable
的高级用法。 Invocable
接口定义了getInterface()
方法,该方法将接口作为参数并返回实现所提供接口的Java代理对象。 从脚本引擎获取代理对象后,可以将其视为常规Java对象。 在代理上调用的方法被委托给脚本引擎,以由脚本语言执行。
请注意, 清单4不包含Javascript。 Invocable
接口允许Java代码在不知道其实现语言的情况下调用脚本函数。 如果脚本引擎找不到具有给定名称或参数类型的函数,则invokeFunction()
方法将引发java.lang.NoSuchMethodException
。
Java脚本API不需要脚本引擎来实现Invocable
接口。 实际上, 清单4中的代码应该使用instanceof
运算符来确保脚本引擎在进行Invocable
之前实现Invocable
接口。
清单3和清单4中的示例显示Java代码如何调用脚本语言中定义的函数或方法。 您可能想知道用脚本语言编写的代码是否可以依次调用Java对象上的方法。 它可以。 清单5中的invokeJavaFromScriptFunction()
方法显示了如何使脚本引擎访问Java对象,以及脚本代码如何调用该Java对象上的方法。 具体来说, invokeJavaFromScriptFunction()
方法使用脚本引擎的put()
方法向引擎提供HelloScriptingWorld
类本身的实例。 引擎使用对put()
的调用中提供的名称访问Java对象后,对eval()
方法的调用中的脚本代码就会使用它。
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 + "'";
}
清单5调用eval()
方法中包含Javascript代码通过使用脚本引擎put()
方法调用中提供的变量名helloScriptingWorld
来访问HelloScriptingWorld
Java对象。 Javascript代码的第二行调用getHelloReply()
公共Java方法,如清单5所示。 getHelloReply()
方法返回Java method getHelloReply says, 'Hello,
字符串。 eval()
方法中Javascript代码将Java返回值分配给msg
变量,然后将其输出到控制台。
当脚本引擎使Java对象可用于在引擎环境中运行的脚本时,引擎需要将其包装为适用于脚本语言的对象类型。 包装可能涉及适当的对象-值转换,例如允许将Java Integer
对象直接在脚本语言数学表达式中使用。 对Java对象如何转换为脚本对象的探索是特定于每种脚本语言引擎的,并且不在本文的讨论范围之内。 但是,您应该知道正在发生翻译,因此您可以进行测试以确保您使用的脚本语言以您期望的方式执行转换。
ScriptEngine.put
及其关联的get()
方法是在Java代码和脚本引擎中运行的脚本之间共享对象和数据的主要方式。 (有关此主题的扩展讨论,请参见本文后面的脚本执行范围 。)当您调用引擎的put()
方法时,脚本引擎会将第二个参数(任何Java对象)与给定的字符串键相关联。 大多数脚本引擎使这些Java对象可以使用给定变量名的脚本进行访问。 脚本引擎可以随意使用传递给put()
方法的名称。 例如,JRuby脚本引擎使helloScriptingWorld
可用于全局$helloScriptingWorld
变量下的Ruby代码,以适合全局变量的Ruby语法。
脚本引擎的get()
方法检索脚本环境中可用的值。 通常,可以通过get()
方法从Java代码访问脚本环境中的每个全局变量和函数。 但是,脚本只能访问那些使用put()
与脚本引擎显式共享的Java对象。
外部脚本访问和操作正在运行的应用程序中的Java对象的能力是扩展Java程序功能的强大技术。 ( 第2部分中的示例利用了该技术。)
您可以通过下载并构建源代码来运行HelloScriptingWorld应用程序。 .zip文件包含一个Ant脚本和一个Maven构建文件,以帮助编译和运行示例应用程序。 跟着这些步骤:
ant run-hello
。 您应该看到来自Ant的控制台输出,与清单6中的输出类似。请注意, defineScriptFunction()
方法不产生任何输出,因为它定义但未调用Javascript函数。
Calling invokeHelloScript...
Hello from JavascriptCalling defineScriptFunction...Calling invokeScriptFunctionFromEngine...
Hello, World!Calling invokeScriptFunctionFromJava...
Hello, from JavaCalling invokeJavaFromScriptFunction...
Invoking getHelloReply method from Javascript...
Java returned: Java method getHelloReply says, 'Hello, Javascript'
Java SE 6引入了Java脚本API,但是您也可以在Java SE 5中运行该API。您只需要提供缺少的javax.script
包类的实现即可。 幸运的是,可以从Java Specification Request 223参考实现中获得一个实现( 有关下载链接,请参见参考资料)。 JSR 223定义了Java脚本API。
如果下载JSR 223参考实现,请解压缩文件并将script-api.jar,script-js.jar和js.jar文件放在类路径中。 这些文件提供了Java SE 6附带的脚本API,Javascript脚本引擎接口和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概念起初可能会造成混淆,因为它们的含义重叠。 本文的源代码下载文件在src / test / java目录中包含一个名为ScriptApiRhinoTest的JUnit测试文件,以帮助通过Java代码解释这些概念。
既然您已经对Java脚本API有了基本的了解,那么本文的第2部分将使用更实际的示例应用程序来完善和扩展该知识。 该应用程序使用结合了Groovy,Ruby和Javascript编写的外部脚本文件来定义可以在运行时更改的业务逻辑。 就像您将看到的那样,用脚本语言定义业务规则可以使规则更易于编写,并且可能使非程序员(例如业务分析师或规则编写者)更易于阅读。
翻译自: https://www.ibm.com/developerworks/java/library/j-Javascripting1/index.html