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
,如果没有其他的办法时才应该这样做。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论