Groovy是一门基于JVM的语言,堪称动态语言版Java,其各种动态语言特性填补了Java的各种空缺,让人拍案叫绝......有幸接触Groovy,遂手动翻译三篇官方文档,以便于读者从Java到Groovy的快速迁移。
Groovy 语言提出了几种在运行时将自身集成到应用程序(Java 甚至 Groovy)中的方法,从最基本的简单代码执行到最完整的集成缓存和编译器定制。
本节中编写的所有示例都使用 Groovy,但可以在 Java 中使用相同的集成机制。
1.1. Eval
这个groovy.util.Eval类是最简单的方式在运行时动态的执行Groovy。可以使用me方法:
java
import groovy.util.Evalassert Eval.me('33*3') == 99
assert Eval.me('"foo".toUpperCase()') == 'FOO'
Eval支持多种参数接收变体来支持简易的表达式:
java
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可以提供更多的选择。
ini
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去在脚本和应用间共享数据:
scss
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 | 执行脚本 |
注意,也有可以在脚本中写入绑定:
scss
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类型将会失败,因为你将会创建一个本地变量:
java
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的共享实例:
java
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 | 将x变量存储在b1中 |
|---|---|
| 2 | 将x变量存储在b2中 |
但是,你必须知道你仍在共享同一脚本实例。因此,如果你有两个线程处理同一个脚本,则无法使用此技术。在这种情况下,你必须确保创建两个不同的脚本实例:
scss
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 |
| 4 | 将第一个绑定分配给脚本2 |
| 5 | 在单独的线程中启动第一个脚本 |
| 6 | 在单独的线程中启动第二个脚本 |
| 7 | 等待完成 |
如果你需要像这里这样的线程安全,建议直接使用 GroovyClassLoader。
1.2.3. 自定义脚本类
我们可以看到parse方法返回一个groovy.lang.Script的实例,但是有可能需要去使用一个自定义的类去扩展Script本身。它可用于为脚本提供额外的行为,如下例所示:
scala
abstract class MyScript extends Script {
String name
String greet() {
"Hello, $name!"
}
}
这个自定义类定义了一个叫做name的参数和一个叫做greet的方法。这个类可以通过一个自定义的配置来被用作脚本的基础类:
java
import org.codehaus.groovy.control.CompilerConfigurationdef 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,你将能够加载类,而不是脚本实例:
ini
import groovy.lang.GroovyClassLoaderdef 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保留了它创建的所有类的引用,因此很容易造成内存泄漏。特别是,如果你执行两次相同的脚本,如果它是一个字符串,那么你将获得两个不同的类!
ini
import groovy.lang.GroovyClassLoaderdef 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不跟踪源文本。如果你想拥有相同的实例,则源必须是一个文件,如下例所示:
ini
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 | 从文件中解析一个类 |
|---|---|
| 2 | 从不同的文件实例解析一个类,但指向同一个物理文件 |
| 3 | 确保我们的类具有相同的名称 |
| 4 | 但现在,它们是同一个实例 |
使用一个File作为输入,GroovyClassLoader是有能力缓存生成的类文件的,这样能够避免在运行时创建多个同源类。
1.4. GroovyScriptEngine
groovy.util.GroovyScriptEngine类为有脚本依赖的应用程序提供了灵活的脚本重载基础。虽然GroovyShell专注于独立脚本,而GroovyClassLoader处理任何 Groovy 类的动态编译和加载,但是GroovyScriptEngine将在GroovyClassLoader之上添加一个层来处理脚本依赖关系和重新加载。
为了说明这一点,我们将创建一个脚本引擎并在无限循环中执行代码。首先,你需要创建一个目录,其中包含以下脚本:
ReloadingTest.groovy
javascript
class Greeter {
String sayHello() {
def greet = "Hello, world!"
greet
}
}new Greeter()
然后你可以使用GroovyScriptEngine执行此代码:
scss
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
javascript
class Greeter {
String sayHello() {
def greet = "Hello, Groovy!"
greet
}
}new Greeter()
消息应更改为:
Hello, world!...Hello, Groovy!Hello, Groovy!...
但也可能依赖于另一个脚本。为了说明这一点,在同一目录中创建以下文件,而不中断正在执行的脚本:
Dependency.groovy
arduino
class Dependency {
String message = 'Hello, dependency 1'
}
并像这样更新ReloadingTest脚本:
ReloadingTest.groovy
javascript
import Dependencyclass Greeter {
String sayHello() {
def greet = new Dependency().message
greet
}
}new Greeter()
这一次,消息应该变为:
Hello, Groovy!...Hello, dependency 1!Hello, dependency 1!...
作为最后一个测试,你可以更新Dependency.groovy文件,而无需触及ReloadingTest文件:
Dependency.groovy
arduino
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,只有在没有其他标准解决方案有效的情况下才应该这样做。