8. 应用集成 Groovy¶
8.1. Groovy 集成机制¶
Groovy 语言提供了几种在运行时集成至应用程序中的方法,包括从最基础的代码执行,到集成缓存及自定义编译器。 本章节的实例使用 Groovy 编写,其同样集成机制也适用于 Java 。
8.1.1. Eval¶
groovy.util.Eval
类在运行时,动态执行 Groovy 最简单的方式。可以直接通过方法 me
实现调用:
import groovy.util.Eval
assert Eval.me('33*3') == 99
assert Eval.me('"foo".toUpperCase()') == 'FOO'
Eval
支持接受参数的多变种执行:
assert Eval.x(4, '2*x') == 8 //<1>
assert Eval.me('k', 4, '2*k') == 8 //<2>
assert Eval.xy(4, 5, 'x*y') == 20 //<3>
assert Eval.xyz(4, 5, 6, 'x*y+z') == 26 //<4>
<1> 绑定参数命名为 x
<2> 自定义绑定参数命名为 k
<3> 两个绑定参数分别命名为 x 和 y
<4> 三个绑定参数分别命名为 x , y 和 z
Eval
可以执行简单脚本,对于复杂的脚本将无能为力:由于其无法缓存脚本,但并不意味着只能执行单行脚本。
8.1.2. GroovyShell¶
8.1.2.1. Multiple sources¶
groovy.lang.GroovyShell
适用于需要缓存脚本实例的执行过程。
Eval
类可以返回脚本执行结果, GroovyShell
类可以提供更多选择。
def shell = new GroovyShell() //<1>
def result = shell.evaluate '3*5' //<2>
def result2 = shell.evaluate(new StringReader('3*5')) //<3>
assert result == result2
def script = shell.parse '3*5' //<4>
assert script instanceof groovy.lang.Script
assert script.run() == 15 //<5>
<1> 创建一个 GroovyShell
实例
<2> 与 Eval
一样可以直接执行代码
<3> 可以从多个数据源读取数据 (String, Reader, File, InputStream)
<4> 可以延缓脚本执行。parse
返回 Script
实例
<5> Script
定义了一个 run
方法
8.1.2.2. Sharing data between a script and the application¶
使用 groovy.lang.Binding
可以在应用程序与脚本之间共享数据:
def sharedData = new Binding() //<1>
def shell = new GroovyShell(sharedData) //<2>
def now = new Date()
sharedData.setProperty('text', 'I am shared data!') //<3>
sharedData.setProperty('date', now) //<4>
String result = shell.evaluate('"At $date, $text"') //<5>
assert result == "At $now, I am shared data!"
<1> 创建 Binding
对象,用于存储共享数据
<2> 创建 GroovyShell
对象,使用共享数据
<3> 添加字符串数据共享绑定
<4> 添加一个 date 类型数据绑定(共享数据不局限于基本数据类型)
<5> 执行脚本
注解
脚本中也将数据写入共享数据对象。
def sharedData = new Binding() //<1>
def shell = new GroovyShell(sharedData) //<2>
shell.evaluate('foo=123') //<3>
assert sharedData.getProperty('foo') == 123 //<4>
<1> 创建 Binding
对象
<2> 创建 GroovyShell
对象,使用共享数据
<3> 使用未声明的变量存储数据结果至共享数据对象中
<4> 读取共享结果数据
从脚本中写入的共享数据,必须为未声明的变量。像下面例子中,使用定义或明确类型将会失败,其原因是这样你就已定义了一个本地变量:
def sharedData = new Binding()
def shell = new GroovyShell(sharedData)
shell.evaluate('int foo=123')
try {
assert sharedData.getProperty('foo')
} catch (MissingPropertyException e) {
println "foo is defined as a local variable"
}
在多线程环境中使用共享数据,需要更加的小心。Binding
实例传递给 GroovyShell
是非线程安全的,其在所有脚本中共享。
It is possible to work around the shared instance of Binding by leveraging the Script instance which is returned by parse:
def shell = new GroovyShell()
def b1 = new Binding(x:3) //<1>
def b2 = new Binding(x:4) //<2>
def script = shell.parse('x = 2*x')
script.binding = b1
script.run()
script.binding = b2
script.run()
assert b1.getProperty('x') == 6
assert b2.getProperty('x') == 8
assert b1 != b2
<1> will store the x variable inside b1
<2> will store the x variable inside b2
然而,你需要注意的是你仍旧在同一个共享的脚本实例上。如果有两个线程在同一个脚本上工作,这项技术将不可使用。在那种情况下,就必须创建两个独立的脚本实例:
def shell = new GroovyShell()
def b1 = new Binding(x:3)
def b2 = new Binding(x:4)
def script1 = shell.parse('x = 2*x') //<1>
def script2 = shell.parse('x = 2*x') //<2>
assert script1 != script2
script1.binding = b1 //<3>
script2.binding = b2 //<4>
def t1 = Thread.start { script1.run() } //<5>
def t2 = Thread.start { script2.run() } //<6>
[t1,t2]*.join() //<7>
assert b1.getProperty('x') == 6
assert b2.getProperty('x') == 8
assert b1 != b2
<1> 为 thread 1 创建 script 实例
<2> 为 thread 2 创建 script 实例
<3> 第一个共享对象分配给 script 1
<4> 第二个共享对象分配给 script 2
<5> 在 thread 1 中启动 script 1
<6> 在 thread 2 中启动 script 2
<7> 等待执行结束
如果你需要线程安全,建议直接使用 GroovyClassLoader
8.1.2.3. Custom script class¶
我们已经知道 parse
方法返回 groovy.lang.Script
实例,还能够通过自定义扩展 Script
类来增强 script
的处理能力,例如:
abstract class MyScript extends Script {
String name
String greet() {
"Hello, $name!"
}
}
扩展类自定义了一个 name
属性以及 greet
方法。通过配置这个类可以按照 script
类方式使用。
import org.codehaus.groovy.control.CompilerConfiguration
def config = new CompilerConfiguration() // <1>
config.scriptBaseClass = 'MyScript' // <2>
def shell = new GroovyShell(this.class.classLoader, new Binding(), config) // <3>
def script = shell.parse('greet()') // <4>
assert script instanceof MyScript
script.setName('Michel')
assert script.run() == 'Hello, Michel!'
<1> 创建 CompilerConfiguration
实例
<2> 指定 scripts
执行的基类名称为 MyScript
<3> 创建 shell
是指定当前定义的编译器配置信息
<4> 此 script
可以访问 greet
方法
并不限于使用唯一的 scriptBaseClass
配置。可以使用任意多个编译器配置,甚至自定义。
8.1.3. GroovyClassLoader¶
前面章节,我看到 GroovyShell
是用来执行 scripts
的一个简单工具。在其内部,它使用 groovy.lang.GroovyClassLoader
在运行时编译并加载 classes
.
通过使用 GroovyClassLoader
替代 GroovyShell
,你可以加载 classes
替代 scripts
实例。
import groovy.lang.GroovyClassLoader
def gcl = new GroovyClassLoader() // <1>
def clazz = gcl.parseClass('class Foo { void doIt() { println "ok" } }') // <2>
assert clazz.name == 'Foo' // <3>
def o = clazz.newInstance() // <4>
o.doIt() // <5>
<1> create a new GroovyClassLoader
<2> parseClass will return an instance of Class
<3> you can check that the class which is returns is really the one defined in the script
<4> and you can create a new instance of the class, which is not a script
<5> then call any method on it
GroovyClassLoader
中维护它创建的所有 classes
, 所有存储溢出会比较容易出现。尤其是, 当你执行两次相同的脚本,你将得到两个不同的类!
import groovy.lang.GroovyClassLoader
def gcl = new GroovyClassLoader()
def clazz1 = gcl.parseClass('class Foo { }') // <1>
def clazz2 = gcl.parseClass('class Foo { }') // <2>
assert clazz1.name == 'Foo' // <3>
assert clazz2.name == 'Foo'
assert clazz1 != clazz2 // <4>
<1> dynamically create a class named “Foo”
<2> create an identical looking class, using a separate parseClass call
<3> make sure both classes have the same name
<4> but they are actually different!
如果你希望获得相同的实例, 源数据必须为文件,例如:
def gcl = new GroovyClassLoader()
def clazz1 = gcl.parseClass(file) // <1>
def clazz2 = gcl.parseClass(new File(file.absolutePath)) // <2>
assert clazz1.name == 'Foo' // <3>
assert clazz2.name == 'Foo'
assert clazz1 == clazz2 // <4>
<1> parse a class from a File
<2> parse a class from a distinct file instance, but pointing to the same physical file
<3> make sure our classes have the same name
<4> but now, they are the same instance
使用文件作为数据源, GroovyClassLoader
可以缓存生成的 class
文件, 这样可以避免在运行时创建多个类从单一源中。
Using a File as input, the GroovyClassLoader is capable of caching the generated class file, which avoids creating multiple classes at runtime for the same source.
8.2. GroovyScriptEngine¶
groovy.util.GroovyScriptEngine
依靠 script
重载和依赖,为应用的灵活扩展提供基础设施。GroovyShell
关注独立的 Script
, GroovyClassLoader
用于动态编译及加载 Groovy
类, GroovyScriptEngine
在 GroovyClassLoader
之上建立一层用于处理 script
的依赖及重新加载。
为说明这点,我们将创建一个 script engine
并在无限循环中执行代码。
首先创建下面代码:
ReloadingTest.groovy
class Greeter {
String sayHello() {
def greet = "Hello, world!"
greet
}
}
new Greeter()
然后使用 GroovyScriptEngine
执行代码:
def binding = new Binding()
def engine = new GroovyScriptEngine([tmpDir.toURI().toURL()] as URL[]) // <1>
while (true) {
def greeter = engine.run('ReloadingTest.groovy', binding) // <2>
println greeter.sayHello() // <3>
Thread.sleep(1000)
}
<1> create a script engine which will look for sources into our source directory
<2> execute the script, which will return an instance of Greeter
<3> print the greeting message
每秒钟将看到下面的打印信息:
Hello, world!
Hello, world!
...
在不终止 script
执行,修改 ReloadingTest
文件,如:
//ReloadingTest.groovy
class Greeter {
String sayHello() {
def greet = "Hello, Groovy!"
greet
}
}
new Greeter()
打印内容将会变化:
Hello, world!
...
Hello, Groovy!
Hello, Groovy!
...
下面将演示 script
的依赖,新建下面文件,同样不中断 script
的执行:
//Depencency.groovy
class Dependency {
String message = 'Hello, dependency 1'
}
更新 ReloadingTest
文件:
//ReloadingTest.groovy
import Dependency
class Greeter {
String sayHello() {
def greet = new Dependency().message
greet
}
}
new Greeter()
打印内容将会有如下变化:
Hello, Groovy!
...
Hello, dependency 1!
Hello, dependency 1!
...
你可以更新 Dependency.groovy
文件:
//Depencency.groovy
class Dependency {
String message = 'Hello, dependency 2'
}
你将看到 dependency
文件被从新加载:
Hello, dependency 1!
...
Hello, dependency 2!
Hello, dependency 2!
8.2.1. CompilationUnit¶
Ultimately, it is possible to perform more operations during compilation by relying directly on the org.codehaus.groovy.control.CompilationUnit class. This class is responsible for determining the various steps of compilation and would let you introduce new steps or even stop compilation at various phases. This is for example how stub generation is done, for the joint compiler.
However, overriding CompilationUnit is not recommended and should only be done if no other standard solution works.
8.3. Bean Scripting Framework¶
Bean Scripting Framework 用于创建 Java 调用脚本语言的 API。 BSF
已经有很长时间没有更新,并且在 JSR-223 中已经废弃。
Groovy
中的 BSF
引擎使用 org.codehaus.groovy.bsf.GroovyEngine
实现。事实上, BSF APIs
已经将其隐藏。通过 BSF API
使用 Groovy
和其他脚本语言一样。
由于 Groovy
其原生的支持 Java
,你所需要关心的 BSF
如何调用其他语言,例如 : JRuby
8.3.1. Getting started¶
将 Groovy
和 BSF
的 jars
加入到 classpath
。你可以像下面 Java
代码中调用 Groovy
脚本:
String myScript = "println('Hello World')\n return [1, 2, 3]";
BSFManager manager = new BSFManager();
List answer = (List) manager.eval("groovy", "myScript.groovy", 0, 0, myScript);
assertEquals(3, answer.size());
8.3.2. 传递参数¶
BSF
可以在 Java
与脚本语言中传递参数。你可以在 BSF
中注册/注销 beans
,之后可以在 BSF
方法中调用。注册的内容可以直接在脚本中使用。
例如:
BSFManager manager = new BSFManager();
manager.declareBean("xyz", 4, Integer.class);
Object answer = manager.eval("groovy", "test.groovy", 0, 0, "xyz + 1");
assertEquals(5, answer);
8.3.3. Other calling options¶
前面例子中使用 eval
方法。BSF
中有多种方法可以使用,详细可以查看 BSF 文档 。这里介绍另一个方法 apple
,其可以使用脚本语言定义匿名函数,并使用其参数。Groovy
上可以使用闭包支持这种函数,如下:
BSFManager manager = new BSFManager();
Vector<String> ignoreParamNames = null;
Vector<Integer> args = new Vector<Integer>();
args.add(2);
args.add(5);
args.add(1);
Integer actual = (Integer) manager.apply("groovy", "applyTest", 0, 0,
"def summer = { a, b, c -> a * 100 + b * 10 + c }", ignoreParamNames, args);
assertEquals(251, actual.intValue());
8.3.4. Access to the scripting engine¶
BSF
中提供勾子,用于直接获取脚本引擎。如下:
BSFManager manager = new BSFManager();
BSFEngine bsfEngine = manager.loadScriptingEngine("groovy");
manager.declareBean("myvar", "hello", String.class);
Object myvar = manager.lookupBean("myvar");
String result = (String) bsfEngine.call(myvar, "reverse", new Object[0]);
assertEquals("olleh", result);
8.3.5. JSR 223 javax.script API¶
JSR-223
是 Java
中调用脚本语言框架的标准接口。从 Java 6
开始,其目标是为了提供一套通用框架来调用脚本语言。 Groovy
提供了丰富的集成机制,我们也建议使用 Groovy
集成机制替代 JSR-223 API
。
这里有关于使用 JSR-223
引擎的实例:
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
...
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("groovy");
接着下面可以开始执行 Groovy
脚本:
Then you can execute Groovy scripts easily:
Integer sum = (Integer) engine.eval("(1..10).sum()");
assertEquals(new Integer(55), sum);
其中也可以共享变量:
engine.put("first", "HELLO");
engine.put("second", "world");
String result = (String) engine.eval("first.toLowerCase() + ' ' + second.toUpperCase()");
assertEquals("hello WORLD", result);
下面演示调用可执行方法:
import javax.script.Invocable;
...
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("groovy");
String fact = "def factorial(n) { n == 1 ? 1 : n * factorial(n - 1) }";
engine.eval(fact);
Invocable inv = (Invocable) engine;
Object[] params = {5};
Object result = inv.invokeFunction("factorial", params);
assertEquals(new Integer(120), result);