Groovy 与应用的集成
1. Groovy 集成机制
Groovy 语言提供了几种在运行时与应用(由 Java 或 Groovy 所编写)相集成的机制,涉及到了从最基本的简单代码执行,到最完整的集成缓存和编译器自定义设置等诸多方面。
本部分内容所有范例都是用 Groovy 编写的,但这样的机制也可以用于 Java 编写的应用程序。
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
类方便了简单脚本的求值计算,但并不能超出一定的范围:由于没有脚本缓存,这意味着不能够计算几行代码。
1.2 GroovyShell
1.2.1 多数据源
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
方法。
1.2.2 在脚本与程序间共享数据
使用 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⃣️ 为绑定对象添加一个日期(并不局限于简单类型)。
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
或像下例中那样使用 explicit
类型都是错误的,会引起失败,因为这样做的结果等于创建了本地变量:
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"
}
在多线程环境中使用共享数据应该极为小心。传入 GroovyShell
的 Binding
实例并不具有线程安全性,会被所有脚本所共享。
利用被 parse
返回的 Script
实例可以解决 Binding
共享实例的问题:
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⃣️ 在 b1
中存储 x
变量。
2⃣️ 在 b2
中存储 x
变量。
但是,必须注意,此时仍旧共享的是脚本的同一个实例。因此,如果两个线程都要利用同一脚本,就不能采用这种方法,这时必须创建两个独立的脚本实例。
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⃣️ 为线程 1 创建一个脚本实例。
2⃣️ 为线程 2 创建一个脚本实例。
3⃣️ 将第 1 个绑定对象赋予脚本 1。
4⃣️ 将第 2 个绑定对象赋予脚本 1。
5⃣️ 在单独的一个线程中运行脚本 1。
6⃣️ 在单独的一个线程中运行脚本 2。
7⃣️ 等待结束。
在需要线程安全的场合(比如该例),建议最好直接使用 GroovyClassLoader。
1.2.3 自定义脚本类
如你所见,parse
方法返回了一个 groovy.lang.Script
实例,但完全可以使用自定义类,只需它扩展 Script
即可。可以用它来为脚本(如下例)提供额外的行为:
abstract class MyScript extends Script {
String name
String greet() {
"Hello, $name!"
}
}
自定义类定义了一个叫 name
的属性,以及一个叫 greet
的新方法。通过使用自定义配置,该类可用作脚本基类。
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⃣️ 让它使用 MyScript
作为脚本基类。
3⃣️ 然后在创建 shell 时,使用编译器配置。
4⃣️ 脚本现在可以访问新方法 greet
。
并不局限于只使用 scriptBaseClass 配置。可以使用任何编译器配置微调选项,包括 compilation customizers。
1.3 GroovyClassLoader
上一部分内容介绍了 GroovyShell
,它是一种执行脚本的便利工具,但除了脚本之外,编译其他的内容就复杂多了。它内部使用了 groovy.lang.GroovyClassLoader
,这是运行时编译以及执行类加载的核心。
通过利用 GroovyClassLoader
,而不是 GroovyShell
,可以加载类,而不是脚本实例:
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⃣️ 创建一个新的 GroovyClassLoader
。
2⃣️ parseClass
能返回一个 Class
的实例。
3⃣️ 可以看到,返回的类真的是脚本中定义的那一个。
4⃣️ 你可以创建该类(并不是脚本)的一个新实例。
5⃣️ 然后调用任何其上的方法。
GroovyClassLoader
持有一个它所创建的所有类的引用,因此很容易造成内存泄露,尤其当你两次执行同一脚本时,比如一个字符串,那么你将获得两个不同的类:
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⃣️ 动态创建一个名为 Foo
的类。
2⃣️ 创建一个看起来一样的类,使用一个单独的 parseClass
调用。
3⃣️ 确保两个类拥有同一名称。
4⃣️ 但它们其实是不同的。
原因在于,GroovyClassLoader
并不跟踪源文本。如果想要同一实例,源必须是一个文件,比如下例:
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⃣️ 从 File
中解析类。
2⃣️ 从不同的一个文件实例中解析一个类,但指向同一实际文件。
3⃣️ 确保类的名字相同。
4⃣️ 但现在它们就是同一个实例了。
将 File
作为输入,GroovyClassLoader
能够捕获生成的类文件,从而避免在运行时对同一数据源创建多个类。
1.4 GroovyScriptEngine
groovy.util.GroovyScriptEngine
类能够为那些依赖脚本重载及依赖的应用程序提供一种灵活的基础。尽管 GroovyShell
聚焦单独的脚本,GroovyClassLoader
能够处理任何 Groovy 类的动态编译与加载,然而 GroovyScriptEngine
能够为 GroovyClassLoader
其上再增添一个能够处理脚本依赖及重新加载的功能层。
为了说明这一点,下面来创建脚本引擎,用无限循环来执行脚本。首先需要创建一个目录,将下列脚本(ReloadingTest.groovy)放入其中。
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⃣️ 创建一个脚本引擎,在源目录中寻找数据源。
2⃣️ 执行脚本,返回 Greeter
实例。
3⃣️ 打印问候信息。
然后,你就会发现每秒都会输出问候信息,如下所示:
Hello, world!
Hello, world!
...
不用打断脚本执行过程,现在用下面的内容来替代 ReloadingTest
文件:
ReloadingTest.groovy
class Greeter {
String sayHello() {
def greet = "Hello, Groovy!"
greet
}
}
new Greeter()
于是,输出信息就变为:
Hello, world!
...
Hello, Groovy!
Hello, Groovy!
...
但它还可能会依赖其他脚本。接下来在同一目录中创建下面这个文件,同样不用干扰上述脚本执行:
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!
...
作为最后一项测试,下面我们在不改动 ReloadingTest
文件的前提下,更新 Dependency.groovy
文件。
Depencency.groovy
class Dependency {
String message = 'Hello, dependency 2'
}
可以看到重新加载了依赖文件:
Hello, dependency 1!
...
Hello, dependency 2!
Hello, dependency 2!
1.5 CompilationUnit
最后,我们直接依靠 org.codehaus.groovy.control.CompilationUnit
类在编译时执行更多的指令。该类负责确定编译的各种步骤,可以让我们引入更多新的步骤,或者甚至停止各种编译阶段。比如说在联合编译器中如何生成存根。
但是,不建议重写 CompilationUnit
,如果没有其他的办法时才应该这样做。
2. Bean 脚本框架
Bean 脚本框架(BSF,Bean Scripting Framework) 试图通过一个 API 来调用 Java 中的脚本语言。它已经很长时间没有更新过了,但由于支持标准的 JSR-223 API,所以还没有被遗弃。
BSF 引擎由 org.codehaus.groovy.bsf.GroovyEngine
类所实现。但这一事实常常被 BSF 的 API 所掩盖。通过 BSF API,只会把 Groovy 看成其他同样的脚本语言。
由于 Groovy 对 Java 集成有着原生的支持,所以你只需要注意下面两种情形中的 BSF 应用:还想调用其他语言时(如 JRuby),或者想与你所使用的脚本语言保持一种非常松散的耦合。
2.1 入门
假设在类路径中有 Groovy 和 BSF 的 jar 文件,那么可以使用下列代码运行简单的 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());
2.2 传入变量
BSF 可以使你在 Java 和脚本语言间传入 bean。可以注册/不注册 bean,使其被 BSF 所知晓,然后利用 BSF 方法在需要时查找 bean。另外,还可以声明/不声明 bean。这将注册它们,但也使其能够直接用于脚本语言。第二种方法是 Groovy 通常所习惯采用的方法,如下所示:
BSFManager manager = new BSFManager();
manager.declareBean("xyz", 4, Integer.class);
Object answer = manager.eval("groovy", "test.groovy", 0, 0, "xyz + 1");
assertEquals(5, answer);
2.3 其他调用选项
上面的范例使用了 eval
方法。BSF 提供了多种方法(详情参见 BSF 文档),其中可用的另一种方法是 apply
。它能让你用脚本语言定义一种匿名函数,然后把该函数应用到参数中。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());
2.4 访问脚本引擎
虽然不是很常用,但 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);
3 JSR 223 javax.script API
JSR-223 是 Java 中标准的脚本框架调用 API。从 Java 6 开始引入进来,主要目用来提供一种常用框架,以便从 Java 中调用多种语言。由于 Groovy 自身已经提供了更丰富的集成机制,所以如果不想在同一应用中使用多种语言,那么建议使用 Groovy 的集成机制,而不是功能受限的 JSR-223 API。
下面展示的是如何初始化 JSR-223 引擎,从而在 Java 中与 Groovy 建立联系:
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
...
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("groovy");
然后执行起 Groovy 脚本就方便多了:
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);
引擎持有脚本函数的每一个默认的硬编码引用。要想改变这一点,可以为名为 ##jsr223.groovy.engine.keep.globals
的脚本上下文设置引擎级别的范围属性:用 phantom
字符串使用虚引用,用 weak
来使用弱引用,用 soft
来使用软引用,忽略大小写问题。任何其他的字符串都会导致使用硬编码引用。