领域专用语言
1 命令链
Groovy 可以使你省略顶级语句方法调用中参数外面的括号。“命令链”功能则将这种特性继续扩展,它可以将不需要括号的方法调用串接成链,既不需要参数周围的括号,链接的调用之间也不需要点号。举例来说,a b c d
实际上就等同于 a(b).c(d)
。它适用于多个参数、闭包参数,甚至命名参数。而且,这样的命令链也可以出现在赋值的右方。让我们来看看应用这一新的语法格式的范例:
// 等同于:turn(left).then(right)
turn left then right
// 等同于:take(2.pills).of(chloroquinine).after(6.hours)
take 2.pills of chloroquinine after 6.hours
// 等同于:paint(wall).with(red, green).and(yellow)
paint wall with red, green and yellow
// 命名参数
// 等同于:check(that: margarita).tastes(good)
check that: margarita tastes good
// 闭包作为参数
// 等同于:given({}).when({}).then({})
given { } when { } then { }
不带参数的链中也可以使用方法,但在这种情况下需要用括号。
// 等同于:select(all).unique().from(names)
select all unique() from names
如果命令链包含奇数个元素,链会由方法和参数组成,最终由一个最终属性访问:
// 等同于:take(3).cookies
// 同样也等于:take(3).getCookies()
take 3 cookies
借助命令链方法有趣的一点是,我们能够利用 Groovy 编写出更多的 DSL。
上面的范例展示了使用基于 DSL 的命令链,但是不知道如何创建一个。你可以使用很多策略,但是为了展示如何创建 DSL,下面列举一些范例,首先使用 map 映射与闭包:
show = { println it }
square_root = { Math.sqrt(it) }
def please(action) {
[the: { what ->
[of: { n -> action(what(n)) }]
}]
}
// 等同于:please(show).the(square_root).of(100)
please show the square_root of 100
// ==> 10.0
第二个范例要考虑如何编写 DSL 来简化一个现存的 API。或许你需要把这些代码展示给顾客、业务分析师或测试员,有可能这些人并不是技艺非常精湛的 Java 开发者。我们将使用 Splitter
,一个Google 的 Guava libraries 项目,因为它已经是一个良好的 Fluent API 了。下面展示如何使用它。
@Grab('com.google.guava:guava:r09')
import com.google.common.base.*
def result = Splitter.on(',').trimResults(CharMatcher.is('_' as char)).split("_a ,_b_ ,c__").iterator().toList()
对于 Java 开发者而言,这段代码很容易明白,但如果面对的是目标顾客,或者要写很多这样的语句,那就可能显得有些啰嗦了。再次提醒一下,DSL 的编写手段多种多样。利用映射和闭包来将它简化一下,首先编写一个辅助函数:
@Grab('com.google.guava:guava:r09')
import com.google.common.base.*
def split(string) {
[on: { sep ->
[trimming: { trimChar ->
Splitter.on(sep).trimResults(CharMatcher.is(trimChar as char)).split(string).iterator().toList()
}]
}]
}
然后找到原始范例中的这一行:
def result = Splitter.on(',').trimResults(CharMatcher.is('_' as char)).split("_a ,_b_ ,c__").iterator().toList()
替换成下面这行:
def result = split "_a ,_b_ ,c__" on ',' trimming '_\'
2. 操作符重载
Groovy 的多种操作符都可以被映射到对象的正则方法调用上。
这允许你提供自己的 Java 或 Groovy 对象,以便利用操作符重载这一优点。下面这张表展示了 Groovy 支持的操作符以及其映射的方法。
操作符 | 方法 |
---|---|
a + b |
a.plus(b) |
a - b |
a.minus(b) |
a * b |
a.multiply(b) |
a ** b |
a.power(b) |
a / b |
a.div(b) |
a % b |
a.mod(b) |
a | b |
a.or(b) |
a & b |
a.and(b) |
a ^ b |
a.xor(b) |
a++ 或 ++a |
a.next() |
a-- 或 --a |
a.previous() |
a[b] |
a.getAt(b) |
a[b] = c |
a.putAt(b, c) |
a << b |
a.leftShift(b) |
a >> b |
a.rightShift(b) |
a >>> b |
a.rightShiftUnsigned(b) |
switch(a) { case(b) : } |
b.isCase(a) |
if(a) |
a.asBoolean() |
~a |
a.bitwiseNegate() |
-a |
a.negative() |
+a |
a.positive() |
a as b |
a.asType(b) |
a == b |
a.equals(b) |
a != b |
! a.equals(b) |
a <=> b |
a.compareTo(b) |
a > b |
a.compareTo(b) > 0 |
a >= b |
a.compareTo(b) >= 0 |
a < b |
a.compareTo(b) < 0 |
a <= b |
a.compareTo(b) <= 0 |
3. 脚本基类
3.1 Script 类
Groovy 脚本经常被编译为类。比如下面这个简单的脚本:
println 'Hello from Groovy'
会被编译扩展自 groovy.lang.Script 抽象类的类。该类只包含一个抽象方法:run
。当脚本编译时,语句体就会成为 run
方法,脚本中的其他方法都位于实现类中。Script
类为通过 Binding
集成应用程序提供了基本支持。如下例所示:
def binding = new Binding() 1⃣️
def shell = new GroovyShell(binding) 2⃣️
binding.setVariable('x',1) 3⃣️
binding.setVariable('y',3)
shell.evaluate 'z=2*x+y' 4⃣️
assert binding.getVariable('z') == 5 5⃣️
1⃣️ 被用于在脚本和调用类间共享数据的绑定对象。
2⃣️ 与该绑定对象联合使用的 GroovyShell
。
3⃣️ 输入变量从位于绑定对象内部的调用类进行设置。
4⃣️ 然后计算脚本。
5⃣️ z
变量导出到绑定对象中。
在调用者和脚本间共享数据是一种很使用的方式,但在有些情况下却未必有较高的效率或实用性。为了应付那种情况,Groovy 允许我们设置自己的基本脚本类。脚本基类必须扩展自 groovy.lang.Script,属于一个单独的抽象方法类别。
abstract class MyBaseClass extends Script {
String name
public void greet() { println "Hello, $name!" }
}
自定义脚本基类可以在编译器配置中声明,如下所示:
def config = new CompilerConfiguration() 1⃣️
config.scriptBaseClass = 'MyBaseClass' 2⃣️
def shell = new GroovyShell(this.class.classLoader, config) 3⃣️
shell.evaluate """
setName 'Judith' 4⃣️
greet()
"""
1⃣️ 创建一个自定义的编译器配置。
2⃣️ 将脚本基类设为我们自定义的脚本基类。
3⃣️ 然后创建一个使用该配置的 GroovyShell
。
4⃣️ 脚本然后扩展该脚本基类,提供对 name
属性及 greet
方法的直接访问。
3.2 @BaseScript 注释
在脚本中直接使用 @BaseScript
注释也是个不错的办法:
import groovy.transform.BaseScript
@BaseScript MyBaseClass baseScript
setName 'Judith'
greet()
上例中,通过 @BaseScript
的注释,变量类型指定为脚本基类。另一种方法是设置基类是 @BaseScript
注释的成员:
@BaseScript(MyBaseClass)
import groovy.transform.BaseScript
setName 'Judith'
greet()
3.3 替代的抽象方法
由前面的几个例子看到,脚本基类属于一种单独的抽象方法类型,需要实现 run
方法。run
方法由脚本引擎自动执行。在有些情况下,可能会由基类实现 run
方法,而提供了另外一种抽象方法用于脚本体,这是比较有意思的一种做法。例如,脚本基类的 run
方法会在执行 run
方法之前执行一些初始化,如下所示:
abstract class MyBaseClass extends Script {
int count
abstract void scriptBody() 1⃣️
def run() {
count++ 2⃣️
scriptBody() 3⃣️
count 4⃣️
}
}
1⃣️ 脚本基类定义了一个(只有一个)抽象方法。
2⃣️ run
方法可以被重写,并在执行脚本体之前执行某个任务。
3⃣️ run
调用抽象的 scriptBody
方法,后者委托给用户脚本。
4⃣️ 然后它能返回除了脚本值之外的其他内容。
如果执行下列代码:
def result = shell.evaluate """
println 'Ok'
"""
assert result == 1
你就会看到脚本被执行,但计算结果是由基类的 run
方法所返回的 1
。如果使用 parse
代替 evaluate
,你就会更清楚,因为你会在同一脚本实例上执行多次 run
方法。
def script = shell.parse("println 'Ok'")
assert script.run() == 1
assert script.run() == 2
4 为数值添加属性
在 Groovy 中,数值类型是等同于其他类型的。因此我们可以通过添加属性或方法来增强它们的功能。在处理可测量的量时,这样做尤为方便。关于如何增强 Groovy 中已有的类的详细情况,可参见extension modules或 categories。
使用 TimeCategory
可展示 Groovy 中的一个范例:
use(TimeCategory) {
println 1.minute.from.now 1⃣️
println 10.hours.ago
def someDate = new Date() 2⃣️
println someDate - 3.months
}
1⃣️ 使用 TimeCategory
,属性 minute
添加到 Integer
类上。
2⃣️ 同样,months
方法也将返回一个用于计算的 groovy.time.DatumDependentDuration
。
类别有词法绑定,所以非常适合内部 DSL。
5. @DelegatesTo
5.1. 编译时解释委托策略
@groovy.lang.DelegatesTo
是一个文档与编译时注释,它的主要作用在于:
- 记录使用闭包做为参数的 API。
- 为静态类型检查器与编译器提供类型信息。
Groovy 是构建 DSL 的一种选择平台。使用闭包可以非常轻松地创建自定义控制结构,创建构建者也非常方便。比如有下面这样的代码:
email {
from 'dsl-guru@mycompany.com'
to 'john.doe@waitaminute.com'
subject 'The pope has resigned!'
body {
p 'Really, the pope has resigned!'
}
}
使用构建者策略可实现,利用一个参数为闭包的名为 email
的方法,它会将随后的调用委托给一个对象,该对象实现了 from
、to
、subject
及 body
各方法。body
方法使用闭包做参数,使用的是构建者策略。
实现这样的构建者往往要通过下面的方式:
def email(Closure cl) {
def email = new EmailSpec()
def code = cl.rehydrate(email, this, this)
code.resolveStrategy = Closure.DELEGATE_ONLY
code()
}
EmailSpec
类实现了 from
、to
等方法,通过调用 rehydrate
,创建了一个闭包副本,用于为该副本设置 delegate
、owner
及 thisObject
等值。设置 owner
和 thisObject
并不十分重要,因为将使用 DELEGATE_ONLY
策略,解决方法调用只针对的是闭包委托。
class EmailSpec {
void from(String from) { println "From: $from"}
void to(String... to) { println "To: $to"}
void subject(String subject) { println "Subject: $subject"}
void body(Closure body) {
def bodySpec = new BodySpec()
def code = body.rehydrate(bodySpec, this, this)
code.resolveStrategy = Closure.DELEGATE_ONLY
code()
}
}
The EmailSpec
类自身的 body
方法将接受一个复制并执行的闭包,这就是 Groovy 构建者模式的原理。
代码中的一个问题在于,email
方法的用户并不知道他能在闭包内调用的方法。唯一的了解途径大概就是方法文档。不过这样也存在两个问题:首先,有些内容并不一定会写出文档,如果记下文档,人们也不一定总能获得(比如 Javadoc 就是在线的,没有下载版本);其二,也没有帮助 IDE。假如有 IDE 来辅助开发者,比如一旦当他们在闭包体内,就建议采用 email
类中的方法。
如果用户调用了闭包内的一个方法,该方法没有被 EmailSpec
类所定义,IDE 应该至少能提供一个警告(因为这非常有可能会在运行时造成崩溃)。
上面代码还存在的一个问题是,它与静态类型检查不兼容。类型检查会让用户了解方法调用是否在编译时被授权(而不是在运行时),但如果你对下面这种代码执行类型检查的话:
email {
from 'dsl-guru@mycompany.com'
to 'john.doe@waitaminute.com'
subject 'The pope has resigned!'
body {
p 'Really, the pope has resigned!'
}
}
类型检查器当然知道存在一个接收 Closure
的 email
方法,但是它会为闭包内的每个方法都进行解释,比如说 from
,它不是一个定义在类中的方法,实际上它定义在 EmailSpec
类中,但在运行时,没有任何线索能让检查器知道它的闭包委托类型是 EmailSpec
:
@groovy.transform.TypeChecked
void sendEmail() {
email {
from 'dsl-guru@mycompany.com'
to 'john.doe@waitaminute.com'
subject 'The pope has resigned!'
body {
p 'Really, the pope has resigned!'
}
}
}
所以在编译时会失败,错误信息如下:
[Static type checking] - Cannot find matching method MyScript#from(java.lang.String). Please check if the declared type is right and if the method exists.
@ line 31, column 21.
from 'dsl-guru@mycompany.com'
5.2. @DelegatesTo
基于以上这些原因,Groovy 2.1 引入了一个新的注释:@DelegatesTo
。该注释目的在于解决文档问题,让 IDE 了解闭包体内的期望方法。同时,它还能够给编译器提供一些提示,告知编译器闭包体内的方法调用的可能接收者是谁,从而解决类型检查问题。
具体方法是注释 email
方法中的 Closure
参数:
def email(@DelegatesTo(EmailSpec) Closure cl) {
def email = new EmailSpec()
def code = cl.rehydrate(email, this, this)
code.resolveStrategy = Closure.DELEGATE_ONLY
code()
}
上面代码告诉编译器(或者 IDE)闭包内的方法何时被调用,闭包委托被设置为 email
类型对象。但这里仍遗漏了一个问题:默认的委托策略并非方法所使用的那一种。因此,我们还需要提供更多信息,告诉编译器(或 IDE)委托策略也改变了。
def email(@DelegatesTo(strategy=Closure.DELEGATE_ONLY, value=EmailSpec) Closure cl) {
def email = new EmailSpec()
def code = cl.rehydrate(email, this, this)
code.resolveStrategy = Closure.DELEGATE_ONLY
code()
}
现在,IDE 和类型检查器(如果使用 @TypeChecked
)都能知道委托和委托策略了。现在,IDE 不仅可以进行智能补足,而且还能消除编译时出现的错误,而这种错误的产生,通常只是因为程序行为只有到了运行时才被知晓。
下面的代码编译起来没有任何问题了:
@TypeChecked
void doEmail() {
email {
from 'dsl-guru@mycompany.com'
to 'john.doe@waitaminute.com'
subject 'The pope has resigned!'
body {
p 'Really, the pope has resigned!'
}
}
}
5.3. DelegatesTo 模式
@DelegatesTo
支持多种模式,本部分内容将予以详细介绍。
5.3.1. 简单委托
该模式中唯一强制的参数是 value,它指明了委托调用的类。除此之外没有别的。编译器还将知道:委托类型将一直是由 @DelegatesTo
所记录的类型。(注意它可能是个子类,如果是子类的话,对于类型检查器来说,由该子类所定义的方法是可见的。)
void body(@DelegatesTo(BodySpec) Closure cl) {
// ...
}
5.3.2. 委托策略
在该模式中,必须指定委托类和委托策略。如果闭包没有以缺省的委托策略(Closure.OWNER_FIRST
)进行调用,就必须使用该模式。
void body(@DelegatesTo(strategy=Closure.DELEGATE_ONLY, value=BodySpec) Closure cl) {
// ...
}
5.3.3. 委托给参数
在这种形式中,我们将会告诉编译器将委托给方法的另一个参数。如下所示:
def exec(Object target, Closure code) {
def clone = code.rehydrate(target, this, this)
clone()
}
这里所用的委托不是在 exec
方法内创建的。实际上是拿了方法中的一个参数然后委托给它。如下所示:
def email = new Email()
exec(email) {
from '...'
to '...'
send()
}
每个方法调用都委托给了 email
参数。这是一种应用很广的模式,它也能被使用联合注释 @DelegatesTo
所支持。
def exec(@DelegatesTo.Target Object target, @DelegatesTo Closure code) {
def clone = code.rehydrate(target, this, this)
clone()
}
闭包使用了注释 @DelegatesTo
,但这一次,没有指定任何类,而利用 @DelegatesTo.Target
注释了另一个参数。委托类型在编译时进行指定。可能有人会认为使用参数类型,比如在该例中是 Object
,但这是错的。代码如下:
class Greeter {
void sayHello() { println 'Hello' }
}
def greeter = new Greeter()
exec(greeter) {
sayHello()
}
注意,这不需要利用 @DelegatesTo
注释。但是,要想让 IDE 或者类型检查器知道委托类型,我们需要 @DelegatesTo
。本例中,Greeter
变量属于 Greeter
类型,而且即使 exec
方法并没有明显地定义 Greeter
类型的目标,sayHello
方法也不会报出错误。这种功能非常有用,可以避免我们针对不同的接收类型而编写不同的 exec
方法。
该模式下,@DelegatesTo
注释也支持我们上面介绍的 strategy
参数。
5.3.4 多个闭包
前例中,exec
方法只接受一个闭包,但是可能会有接收多个闭包的方法:
void fooBarBaz(Closure foo, Closure bar, Closure baz) {
...
}
利用 @DelegatesTo
注释每个闭包就显得不可避免了:
class Foo { void foo(String msg) { println "Foo ${msg}!" } }
class Bar { void bar(int x) { println "Bar ${x}!" } }
class Baz { void baz(Date d) { println "Baz ${d}!" } }
void fooBarBaz(@DelegatesTo(Foo) Closure foo, @DelegatesTo(Bar) Closure bar, @DelegatesTo(Baz) Closure baz) {
...
}
但更重要的是,如果有多个闭包和多个参数,可以使用一些目标:
void fooBarBaz(
@DelegatesTo.Target('foo') foo,
@DelegatesTo.Target('bar') bar,
@DelegatesTo.Target('baz') baz,
@DelegatesTo(target='foo') Closure cl1,
@DelegatesTo(target='bar') Closure cl2,
@DelegatesTo(target='baz') Closure cl3) {
cl1.rehydrate(foo, this, this).call()
cl2.rehydrate(bar, this, this).call()
cl3.rehydrate(baz, this, this).call()
}
def a = new Foo()
def b = new Bar()
def c = new Baz()
fooBarBaz(
a, b, c,
{ foo('Hello') },
{ bar(123) },
{ baz(new Date()) }
)
这时,你可能会感到困惑:为何我们不把引用作为参数名呢?因为相关信息(参数名)不一定能获取到(只供调试的信息),所以这是 JVM 的一个缺陷。
5.3.5. 委托给基本类型
在一些情况下,可以命令 IDE 或编译器,使委托类型不是参数而是某种基本类型。假设有下面这样运行在一列元素上的配置器:
public <T> void configure(List<T> elements, Closure configuration) {
elements.each { e->
def clone = configuration.rehydrate(e, this, this)
clone.resolveStrategy = Closure.DELEGATE_FIRST
clone.call()
}
}
然后利用任何列表都可以调用该方法:
@groovy.transform.ToString
class Realm {
String name
}
List<Realm> list = []
3.times { list << new Realm() }
configure(list) {
name = 'My Realm'
}
assert list.every { it.name == 'My Realm' }
要想让类型检查器和 IDE 了解 configure
方法在列表的每个元素上调用闭包,你需要换一种方式来使用 @DelegatesTo
:
public <T> void configure(
@DelegatesTo.Target List<T> elements,
@DelegatesTo(strategy=Closure.DELEGATE_FIRST, genericTypeIndex=0) Closure configuration) {
def clone = configuration.rehydrate(e, this, this)
clone.resolveStrategy = Closure.DELEGATE_FIRST
clone.call()
}
@DelegatesTo
获取一个可选的 genericTypeIndex
参数,该参数指出被用作委托类型的基本类型的具体索引。它必须要与 @DelegatesTo.Target
联合使用,并且起始索引要为 0。在上例中,委托类型会根据 List<T>
来判定,因为在索引 0 处的基本类型是 T
,并推断是 Realm
,所以类型检查器也会推断委托类型属于 Realm
类型。
由于 JVM 的限制,我们使用 genericTypeIndex
来代替占位符(T
)。
5.3.6. 委托给任意类型
有可能上述所有方式都无法表示你想要委托的类型。比如,可以定义一个 Mapper
类,它带有一个对象参数,并且定义了一个能够返回其他类型对象的 map
方法:
class Mapper<T,U> { 1⃣️
final T value 2⃣️
Mapper(T value) { this.value = value }
U map(Closure<U> producer) { 3⃣️
producer.delegate = value
producer()
}
}
1⃣️ Mapper
类接受两个通用类型参数:源类型与目标类型。
2⃣️ 源对象保存在一个 final 类型的对象中。
3⃣️ map
方法请求将源对象转换为目标对象。
如你所见,map
上的方法签名并不没有指明是何种对象在受闭包的控制。看看方法体,我们就了解它应该是类型 T
的 value
,但 T
并未在方法签名中,因此没有合适的选项适合 @DelegatesTo
。比如我们打算静态编译下列代码:
def mapper = new Mapper<String,Integer>('Hello')
assert mapper.map { length() } == 5
编译将失败,并且提供了以下失败信息:
Static type checking] - Cannot find matching method TestScript0#length()
在这种情况下,可以使用 @DelegatesTo
注释的 type
成员将 T
引用为类型令牌:
class Mapper<T,U> {
final T value
Mapper(T value) { this.value = value }
U map(@DelegatesTo(type="T") Closure<U> producer) { 1⃣️
producer.delegate = value
producer()
}
}
1⃣️ @DelegatesTo
注释引用了一个方法签名中不存在的基本类型。
注意,这里并不局限于基本类型令牌。type
成员可以用来表示复杂类型,比如说 List<T>
或 Map<T,List<U>>
。 它之所以被用作最后手段的原因在于,只有当类型检查器发现使用了 @DelegatesTo
之时,类型才被检查,而不是在注释方法本身被编译时才这样做。这就意味着类型安全性只有在调用站点时才能保证。另外,编译起来也比较慢(虽然在绝大多数情况下,这一点并不容易觉察出来)。
6 编译自定义器(Compilation customizers)
6.1 简介
无论你使用 groovyc
还是采用 GroovyShell
来编译类,要想执行脚本,实际上都会使用到编译器配置(compiler configuration)信息。这种配置信息保存了源编码或类路径这样的信息,而且还用于执行更多的操作,比如默认添加导入,显式使用 AST 转换,或者禁止全局 AST 转换,等等。
编译自定义器的目标在于使这些常见任务易于实现。CompilerConfiguration
类就是切入点。基本架构通常都会基于下列代码:
import org.codehaus.groovy.control.CompilerConfiguration
// 创建配置信息
def config = new CompilerConfiguration()
// 微调配置信息
config.addCompilationCustomizers(...)
// 运行脚本
def shell = new GroovyShell(config)
shell.evaluate(script)
编译自定义器必须扩展自 org.codehaus.groovy.control.customizers.CompilationCustomizer
类。自定义器适用于:
- 特定的编译过程。
- 正在编译的每个类节点。
当然,你可以实现自己的编译自定义器,但 Groovy 包含了一些最常见的操作。
6.2. 导入自定义器
使用这种编译自定义器,代码可以显式地添加导入。假如脚本想实现一种能够避免用户不得不手动导入的 DSL,那么这就非常有用了。导入自定义器将使你添加 Groovy 所允许的所有导入形式,包括:
- 类导入,可选别名。
- 星号导入。
- 静态导入,可选别名。
- 静态星号导入。
import org.codehaus.groovy.control.customizers.ImportCustomizer
def icz = new ImportCustomizer()
// 通常的导入
icz.addImports('java.util.concurrent.atomic.AtomicInteger', 'java.util.concurrent.ConcurrentHashMap')
// 别名导入
icz.addImport('CHM', 'java.util.concurrent.ConcurrentHashMap')
// 静态导入
icz.addStaticImport('java.lang.Math', 'PI') // import static java.lang.Math.PI
// 别名静态导入
icz.addStaticImport('pi', 'java.lang.Math', 'PI') // import static java.lang.Math.PI as pi
// 星号导入
icz.addStarImports 'java.util.concurrent' // import java.util.concurrent.*
// 静态星号导入
icz.addStaticStars 'java.lang.Math' // import static java.lang.Math.*
详细描述见于 org.codehaus.groovy.control.customizers.ImportCustomizer。
6.3 AST 转换自定义器
AST 转换自定义器可以用来显式地应用 AST 转换。对于全局型 AST 转换而言,只要转换存在于类路径中,被编译的每个类都会应用转换(相应的缺点是增加编译时间,或者转换了不该转换的)。自定义转换器能实现选择应用转换,只针对特定的脚本或类应用转换。
比如想在脚本中能够使用 @Log
,那么问题在于 @Log
一般应用于类节点上,而根据定义,脚本并不需要。但如果实现得好,脚本也就是类,只是你不能把这种隐式的类节点用 @Log
来注释,而使用 AST 自定义器,我们可以进行一个全变措施:
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import groovy.util.logging.Log
def acz = new ASTTransformationCustomizer(Log)
config.addCompilationCustomizers(acz)
只需这样即可!@Log
AST 转换被应用到编译单元的每个类节点上。这意味着它将应用到脚本上,以及脚本内所定义的类上。
如果使用的 AST 转换接收一些参数,也可以在构造函数中使用这些参数:
def acz = new ASTTransformationCustomizer(Log, value: 'LOGGER')
// 使用 'LOGGER' 而非默认的 'log'
config.addCompilationCustomizers(acz)
因为 AST 转换自定义器用于对象而不是 AST 节点,所以并不是所有值都被转换为 AST 转换参数。比如说,原始类型被转换为 ConstantExpression
(LOGGER
被转换为 new ConstantExpression('LOGGER')
),但如果你的 AST 转换将闭包作为参数,那么必须要给它一个 ClosureExpression
,如下例所示:
def configuration = new CompilerConfiguration()
def expression = new AstBuilder().buildFromCode(CompilePhase.CONVERSION) { -> true }.expression[0]
def customizer = new ASTTransformationCustomizer(ConditionalInterrupt, value: expression, thrown: SecurityException)
configuration.addCompilationCustomizers(customizer)
def shell = new GroovyShell(configuration)
shouldFail(SecurityException) {
shell.evaluate("""
// 等于添加了 @ConditionalInterrupt(value={true}, thrown: SecurityException)
class MyClass {
void doIt() { }
}
new MyClass().doIt()
""")
}
完整的选项列表参见:org.codehaus.groovy.control.customizers.ASTTransformationCustomizer。
6.4 安全 AST 自定义器
该自定义器允许 DSL 的开发者限制语言的语法,从而防止用户使用一些结构。它只有在这种意义上说才是安全的,而且重要的是,它不能代替安全管理器。它存在的唯一理由就是为了限制语言的表现力。自定义器只适用于 AST(抽象语法树)级别,而不是在运行时。乍看起来,比较奇怪,但如果把 Groovy 看成是 DSL 的构建平台的话,就顺理成章了。你可能不希望用户利用完整的语言。在下例中,只允许使用运算操作。该自定义器可以实现:
- 允许/不允许创建闭包。
- 允许/不允许导入。
- 允许/不允许包定义。
- 允许/不允许方法定义。
- 限制方法调用的接收者。
- 限制用户所能使用的 AST 表达式种类。
- 限制用户所能使用的令牌(语法明智)。
- 限制代码中常量的类型。
安全 AST 自定义器使用白名单(允许的元素列表)或黑名单(不允许的元素列表)策略来实现这些功能。对于每一类功能(导入、令牌,等等),都可以选择究竟使用白名单还是黑名单。还可以混合使用两种名单来实现一些独特的功能。一般选择白名单(不允许选择还是允许选择)即可。
import org.codehaus.groovy.control.customizers.SecureASTCustomizer
import static org.codehaus.groovy.syntax.Types.* 1⃣️
def scz = new SecureASTCustomizer()
scz.with {
closuresAllowed = false // 用户不能写闭包
methodDefinitionAllowed = false // 用户不能定义方法
importsWhitelist = [] // 白名单为空意味着不允许导入
staticImportsWhitelist = [] // 同样,对于静态导入也是这样
staticStarImportsWhitelist = ['java.lang.Math'] // 只允许 java.lang.Math
// 用户能找到的令牌列表
// org.codehaus.groovy.syntax.Types 中所定义的常量
tokensWhitelist = [ 1⃣️
PLUS,
MINUS,
MULTIPLY,
DIVIDE,
MOD,
POWER,
PLUS_PLUS,
MINUS_MINUS,
COMPARE_EQUAL,
COMPARE_NOT_EQUAL,
COMPARE_LESS_THAN,
COMPARE_LESS_THAN_EQUAL,
COMPARE_GREATER_THAN,
COMPARE_GREATER_THAN_EQUAL,
].asImmutable()
// 将用户所能定义的常量类型限制为数值类型
constantTypesClassesWhiteList = [ 2⃣️
Integer,
Float,
Long,
Double,
BigDecimal,
Integer.TYPE,
Long.TYPE,
Float.TYPE,
Double.TYPE
].asImmutable()
// 如果接收者是其中一种类型,只允许方法调用
// 注意,并不是一个运行时类型!
receiversClassesWhiteList = [ 2⃣️
Math,
Integer,
Float,
Double,
Long,
BigDecimal
].asImmutable()
}
1⃣️ 用于 org.codehaus.groovy.syntax.Types 中的令牌类型
2⃣️ 可以使用类字面量。
如果安全 AST 自定义器满足不了你的需求,那么在创建自己的编译自定义器之前,要考虑一下 AST 自定义器所支持的表达式和语句检查器。一般而言,允许在 AST 树上,表达式上(表达式检查器)或语句(语句检查器)添加自定义检查。为此,必须实现 org.codehaus.groovy.control.customizers.SecureASTCustomizer.StatementChecker
或 org.codehaus.groovy.control.customizers.SecureASTCustomizer.ExpressionChecker
。
这些接口定义了一个 isAuthorized
方法,它能返回一个布尔值,能够接收 Statement
或 Expression
作为参数。该方法可以在表达式或语句上实现复杂的逻辑,是否允许用户去实现
比如说,自定义器上没有能够防止用户使用某个属性表达式的预定义配置标识,那么使用自定义检查器,一切就很简单:
def scz = new SecureASTCustomizer()
def checker = { expr ->
!(expr instanceof AttributeExpression)
} as SecureASTCustomizer.ExpressionChecker
scz.addExpressionCheckers(checker)
然后通过计算一个简单的脚本就可以确保它的有效性:
new GroovyShell(config).evaluate '''
class A {
int val
}
def a = new A(val: 123)
a.@val 1⃣️
'''
1⃣️ 会导致编译失败。
语句检查方面可参见:
org.codehaus.groovy.control.customizers.SecureASTCustomizer.StatementChecker。
表达式检查方面参见:
org.codehaus.groovy.control.customizers.SecureASTCustomizer.ExpressionChecker。
6.5 源识别自定义器
该自定义器可以当做其他自定义器上的过滤器。这种情况下的过滤器是 org.codehaus.groovy.control.SourceUnit
。源识别自定义器将其他自定义器作为一种委托,它只应用委托自定义,并且只有在源单位上的谓词相匹配才进行。
SourceUnit
可以让我们访问多项内容,但主要是针对被编译的文件(如果编译的是文件,理当如此)。可以根据文件名称来实施操作。范例如下:
import org.codehaus.groovy.control.customizers.SourceAwareCustomizer
import org.codehaus.groovy.control.customizers.ImportCustomizer
def delegate = new ImportCustomizer()
def sac = new SourceAwareCustomizer(delegate)
然后就可以使用源识别自定义器上的谓词了:
// 自定义器只应用到名称以 'Bean' 结尾的文件内的类上
sac.baseNameValidator = { baseName ->
baseName.endsWith 'Bean'
}
// 自定义器只应用到扩展名为 '.spec' 的文件内的类上
sac.extensionValidator = { ext -> ext == 'spec' }
// 源单位验证
// 只有当文件包含至少一个类时才允许编译
sac.sourceUnitValidator = { SourceUnit sourceUnit -> sourceUnit.AST.classes.size() == 1 }
// 类验证
// 自定义器只应用于结尾是 `Bean` 的类上
sac.classValidator = { ClassNode cn -> cn.endsWith('Bean') }
6.6 自定义器构建器
如果在 Groovy 代码中使用编译自定义器(如上面那些例子所示),则可以采用替代语法来自定义编译。可以使用一种构建器(org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder
)来简化自定义器的创建,使用的是层级 DSL。
import org.codehaus.groovy.control.CompilerConfiguration
import static org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder.withConfig 1⃣️
def conf = new CompilerConfiguration()
withConfig(conf) {
// ... 2⃣️
}
1⃣️ 构建器方法的静态导入。
2⃣️ 这里放的是相关配置信息。
上例代码展示的是构建器的使用。静态方法 withConfig
获取一个跟构建器相关的闭包,自动将编译自定义器注册到配置信息中。分发的每一个编译自定义器都可以用这种方式来配置:
6.6.1 导入自定义器
withConfig(configuration) {
imports { // imports customizer
normal 'my.package.MyClass' // a normal import
alias 'AI', 'java.util.concurrent.atomic.AtomicInteger' // an aliased import
star 'java.util.concurrent' // star imports
staticMember 'java.lang.Math', 'PI' // static import
staticMember 'pi', 'java.lang.Math', 'PI' // aliased static import
}
}
6.6.2 AST 转换自定义器
withConfig(conf) {
ast(Log) 1⃣️
}
withConfig(conf) {
ast(Log, value: 'LOGGER') 2⃣️
}
1⃣️ 显式使用 @Log
。
2⃣️ 应用 @Log
,并使用 logger 的另一个名字
6.6.3 安全 AST 自定义器
withConfig(conf) {
secureAst {
closuresAllowed = false
methodDefinitionAllowed = false
}
}
6.6.4 源识别自定义器
withConfig(configuration){
source(extension: 'sgroovy') {
ast(CompileStatic) 1⃣️
}
}
withConfig(configuration){
source(extensions: ['sgroovy','sg']) {
ast(CompileStatic) 2⃣️
}
}
withConfig(configuration) {
source(extensionValidator: { it.name in ['sgroovy','sg']}) {
ast(CompileStatic) 2⃣️
}
}
withConfig(configuration) {
source(basename: 'foo') {
ast(CompileStatic) 3⃣️
}
}
withConfig(configuration) {
source(basenames: ['foo', 'bar']) {
ast(CompileStatic) 4⃣️
}
}
withConfig(configuration) {
source(basenameValidator: { it in ['foo', 'bar'] }) {
ast(CompileStatic) 4⃣️
}
}
withConfig(configuration) {
source(unitValidator: { unit -> !unit.AST.classes.any { it.name == 'Baz' } }) {
ast(CompileStatic) 5⃣️
}
}
1⃣️ 在 .sgroovy 文件上应用 AST 注释 CompileStatic
。
2⃣️ 在 .sgroovy 或 .sg 文件上应用 AST 注释 CompileStatic
。
3⃣️ 在名称为 foo
的文件上应用 AST 注释 CompileStatic
。
4⃣️ 在名称为 foo
or bar
的文件上应用 AST 注释 CompileStatic
。
5⃣️ 在不包含名为 Baz
的类的文件上应用 AST 注释 CompileStatic
。
6.6.5 内联自定义器
内联自定义器可以让你直接编写一个编译自定义器,而不必为其创建任何类:
withConfig(configuration) {
inline(phase:'CONVERSION') { source, context, classNode -> 1⃣️
println "visiting $classNode" 2⃣️
}
}
1⃣️ 定义一个能在 CONVERSION 阶段执行的内联自定义器。
2⃣️ 打印正在编辑的类节点名称。
6.6.6 多个自定义器
当然,构建器还可以让你一次构建多个自定义器:
withConfig(configuration) {
ast(ToString)
ast(EqualsAndHashCode)
}
6.7 配置脚本标记
迄今为止,我们介绍了如何利用 CompilationConfiguration
类来自定义编译,但这是有一个前提条件的:内嵌 Groovy,并且创建了自己的 CompilerConfiguration
实例(然后用它来创建GroovyShell
、GroovyScriptEngine
,等等)。
如果想把它用在那些利用普通 Groovy 编译器(也就是说利用 groovyc
、ant
或 gradle
)编译的类上,可以使用一个编译标记 configscript
,它以一个 Groovy 配置脚本作为参数。
该脚本可以让你在文件编译前(以名为 configuration
的变量暴露给配置脚本)访问 CompilerConfiguration
实例,因此还可以微调。
也可以显式地结合上面介绍的编译器配置构建器。下例展示了如何在所有的类上都默认激活静态编译。
6.7.1. 默认静态编译
通常,Groovy 中的类都是在动态运行时进行编译的。可以把 @CompileStatic
注释放在任何类上来激活静态编译。一些人可能喜欢默认激活这种模式,也就是不用手动地去注释类。使用 configscript
就可以。首先需要在 src/conf
上创建一个名为 config.groovy
的文件,内容如下:
withConfig(configuration) { 1⃣️
ast(groovy.transform.CompileStatic)
}
1⃣️ configuration 引用了一个 CompilerConfiguration
实例。
所需的就这么多。不必导入构建器,因为它会自动暴露在脚本中。然后,使用下列命令编译文件即可:
groovyc -configscript src/conf/config.groovy src/main/groovy/MyClass.groovy
我们强烈建议你将类与配置文件分开,这就是我们为何之前建议使用 src/main
和 src/conf
目录的原因。
6.8 AST 转换
如果:
- 运行时元编程不能满足你的要求。
- 需要提升 DSL 的执行效率。
- 想要利用 Groovy 的语法,但语义与之有所不同。
- 改善 DSL 中对于类型检查的支持。
那么使用 AST 转换是一条路子。与迄今为止所使用的技术不同,AST 转换是在编译成字节码之前就进行改变或生成代码。AST 转换能够在运行时添加新的方法,或根据需求彻底改变方法体。它们是很强大的工具,但编写起来比较难。有关 AST 转换的详细信息,可参考本手册的编译时元编程部分。
7 自定义类型检查扩展
有时我们会需要尽可能立即地将出错代码的相关信息反馈给用户,也就是说,假如当 DSL 脚本被编译时,与其等待脚本的执行,不如直接反馈给用户。但对于动态代码来说,这往往并不可行。Groovy 提供了一个实用的方案:类型检查扩展。
8 构建器
(未编写)
8.1. Creating a builder
(未编写)
8.1.1. BuilderSupport
(未编写)
8.1.2. FactoryBuilderSupport
(未编写)
8.2. Existing builders
(未编写)
8.2.1. MarkupBuilder
8.2.2. StreamingMarkupBuilder
参见 创建 Xml - StreamingMarkupBuilder。
8.2.3. SaxBuilder
用来生成 Simple API for XML (SAX) 事件的构建器。
假设现在有下列 SAX 处理器:
class LogHandler extends org.xml.sax.helpers.DefaultHandler {
String log = ''
void startElement(String uri, String localName, String qName, org.xml.sax.Attributes attributes) {
log += "Start Element: $localName, "
}
void endElement(String uri, String localName, String qName) {
log += "End Element: $localName, "
}
}
可以使用 SaxBuilder
为该处理器生成 SAX 事件:
def handler = new LogHandler()
def builder = new groovy.xml.SAXBuilder(handler)
builder.root() {
helloWorld()
}
一切如预期运行:
assert handler.log == 'Start Element: root, Start Element: helloWorld, End Element: helloWorld, End Element: root, '
8.2.4. StaxBuilder
适用于 Streaming API for XML (StAX) 处理程序的 Groovy 构建器。
下面是一个很简单的范例,利用 StAX 的 Java 实现来生成 XML:
def factory = javax.xml.stream.XMLOutputFactory.newInstance()
def writer = new StringWriter()
def builder = new groovy.xml.StaxBuilder(factory.createXMLStreamWriter(writer))
builder.root(attribute:1) {
elem1('hello')
elem2('world')
}
assert writer.toString() == '<?xml version="1.0" ?><root attribute="1"><elem1>hello</elem1><elem2>world</elem2></root>'
类似 Jettison 这样的外部库可以这样使用:
@Grab('org.codehaus.jettison:jettison:1.3.3')
import org.codehaus.jettison.mapped.*
def writer = new StringWriter()
def mappedWriter = new MappedXMLStreamWriter(new MappedNamespaceConvention(), writer)
def builder = new groovy.xml.StaxBuilder(mappedWriter)
builder.root(attribute:1) {
elem1('hello')
elem2('world')
}
assert writer.toString() == '{"root":{"@attribute":"1","elem1":"hello","elem2":"world"}}'
8.2.5 DOMBuilder
将 HTML、XHTML 以及 XML 解析为 W3C DOM 树的构建器。
比如像这个 XML String
:
String recordsXML = '''
<records>
<car name='HSV Maloo' make='Holden' year='2006'>
<country>Australia</country>
<record type='speed'>Production Pickup Truck with speed of 271kph</record>
</car>
<car name='P50' make='Peel' year='1962'>
<country>Isle of Man</country>
<record type='size'>Smallest Street-Legal Car at 99cm wide and 59 kg in weight</record>
</car>
<car name='Royale' make='Bugatti' year='1931'>
<country>France</country>
<record type='price'>Most Valuable Car at $15 million</record>
</car>
</records>'''
可以解析为带有 DOMBuilder
的 DOM 树:
def reader = new StringReader(recordsXML)
def doc = groovy.xml.DOMBuilder.parse(reader)
还可以进一步处理,比如使用 DOMCategory:
def records = doc.documentElement
use(groovy.xml.dom.DOMCategory) {
assert records.car.size() == 3
}
8.2.6 NodeBuilder
NodeBuilder
用来创建能够处理任意数据的 Node
对象的内嵌树。要想创建一个简单的用户列表,可以使用一个 NodeBuilder
:
def nodeBuilder = new NodeBuilder()
def userlist = nodeBuilder.userlist {
user(id: '1', firstname: 'John', lastname: 'Smith') {
address(type: 'home', street: '1 Main St.', city: 'Springfield', state: 'MA', zip: '12345')
address(type: 'work', street: '2 South St.', city: 'Boston', state: 'MA', zip: '98765')
}
user(id: '2', firstname: 'Alice', lastname: 'Doe')
}
还可以进一步处理这些数据,比如使用 GPath 表达式:
assert userlist.user.@firstname.join(', ') == 'John, Alice'
assert userlist.user.find { it.@lastname == 'Smith' }.address.size() == 2
8.2.7 JsonBuilder
Groovy 的 JsonBuilder
可以很方便地创建 JSON 对象。比如创建下列 Json 字符串:
String carRecords = '''
{
"records": {
"car": {
"name": "HSV Maloo",
"make": "Holden",
"year": 2006,
"country": "Australia",
"record": {
"type": "speed",
"description": "production pickup truck with speed of 271kph"
}
}
}
}
'''
还可以这样使用 JsonBuilder
:
JsonBuilder builder = new JsonBuilder()
builder.records {
car {
name 'HSV Maloo'
make 'Holden'
year 2006
country 'Australia'
record {
type 'speed'
description 'production pickup truck with speed of 271kph'
}
}
}
String json = JsonOutput.prettyPrint(builder.toString())
还可以使用 JsonUnit 查看构建器是否产生预期结果:
JsonAssert.assertJsonEquals(json, carRecords)
8.2.8 StreamingJsonBuilder
StreamingJsonBuilder
与 JsonBuilder
不同之处在于,JsonBuilder
在内存中创建数据结构,假如想要在输出前程序性地改变结构,那么这一构建器非常方便,而 StreamingJsonBuilder
则采用流式写入,无需任何中间的内存数据结构。如果不需要修改数据结构,使用一种存储效率更高的方法,则应该使用 StreamingJsonBuilder
。
在用法上,StreamingJsonBuilder
和 JsonBuilder
差不多,以下面这个 Json 字符串为例:
String carRecords = '''
{
"records": {
"car": {
"name": "HSV Maloo",
"make": "Holden",
"year": 2006,
"country": "Australia",
"record": {
"type": "speed",
"description": "production pickup truck with speed of 271kph"
}
}
}
}
'''
使用 StreamingJsonBuilder
:
StringWriter writer = new StringWriter()
StreamingJsonBuilder builder = new StreamingJsonBuilder(writer)
builder.records {
car {
name 'HSV Maloo'
make 'Holden'
year 2006
country 'Australia'
record {
type 'speed'
description 'production pickup truck with speed of 271kph'
}
}
}
String json = JsonOutput.prettyPrint(writer.toString())
使用 JsonUnit 来查看是否符合预期结果:
JsonAssert.assertJsonEquals(json, carRecords)
8.2.9 SwingBuilder
SwingBuilder
能够以一种简洁的描述性方式来实现一种成熟完备的 Swing GUI。这一目的的实现有赖于一种常见的 Groovy 词汇:构建器(builder)。构建器可以帮助我们来处理复杂对象,比如初始化子对象,调用 Swing 方法,将子对象添加到父对象上,等等。因此,代码可读性和可维护性变得更好,同时还能使你访问所有 Swing 组件。
下面是使用 SwingBuilder
的一个简单范例:
import groovy.swing.SwingBuilder
import java.awt.BorderLayout as BL
count = 0
new SwingBuilder().edt {
frame(title: 'Frame', size: [300, 300], show: true) {
borderLayout()
textlabel = label(text: 'Click the button!', constraints: BL.NORTH)
button(text:'Click Me',
actionPerformed: {count++; textlabel.text = "Clicked ${count} time(s)."; println "clicked"}, constraints:BL.SOUTH)
}
}
下面是实际的窗口:
SwingBuilder001
一般说来,层级组件通常是利用一系列重复的实例化 和 setter 方法,最后将子对象添加到相应的父对象上。而通过 SwingBuilder
,利用原生形式来定义这种层级,使界面设计变得更容易理解,只需读读代码即可理解。
利用 Groovy 中许多内建功能,比如说闭包、隐式地调用构造函数、别名式导入以及字符串插值等,可以实现很多灵活性。使用 SwingBuilder
完全不必理解这么多,如上例所示,它的使用是非常直观的。
下面介绍的例子稍微复杂一些,通过闭包来实现 SwingBuilder
代码重用。
import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.*
def swing = new SwingBuilder()
def sharedPanel = {
swing.panel() {
label("Shared Panel")
}
}
count = 0
swing.edt {
frame(title: 'Frame', defaultCloseOperation: JFrame.EXIT_ON_CLOSE, pack: true, show: true) {
vbox {
textlabel = label('Click the button!')
button(
text: 'Click Me',
actionPerformed: {
count++
textlabel.text = "Clicked ${count} time(s)."
println "Clicked!"
}
)
widget(sharedPanel())
widget(sharedPanel())
}
}
}
下面是一种依赖显式的 bean 和绑定的变体形式:
import groovy.swing.SwingBuilder
import groovy.beans.Bindable
class MyModel {
@Bindable int count = 0
}
def model = new MyModel()
new SwingBuilder().edt {
frame(title: 'Java Frame', size: [100, 100], locationRelativeTo: null, show: true) {
gridLayout(cols: 1, rows: 2)
label(text: bind(source: model, sourceProperty: 'count', converter: { v -> v? "Clicked $v times": ''}))
button('Click me!', actionPerformed: { model.count++ })
}
}
@Bindable
是一种核心 AST 转换。它可以生成将简单 bean 转换为显式 bean所需的所有样本文件代码,bind()
节点创建出正确的 PropertyChangeListeners
,用于在 PropertyChangeEvent
触发时更新感兴趣的部分。
8.2.10. AntBuilder
尽管 Apache Ant 主要是一种构建工具,但它其实还是一种实用性极强的文件操作工具,可以进行压缩、复制以及资源处理等文件操作。但如果你使用过 build.xml
文件或一些 Jelly 脚本的话,你就会觉得受到尖括号的限制,或者觉得将 XML 用作脚本语言这一点显得很怪异。你可能想要一种更简洁更直观的东西,那么或许利用 Groovy 和 Ant 编写脚本就是你最好的选择。
Groovy 有一个辅助类:AntBuilder
,它能使利用 Ant 编写脚本这项工作变得非常轻松,而且还能允许使用真正的脚本语言来编写构造(变量、方法、循环、逻辑分支、类,等等)。它看起来还像是 Ant XML 的一种内嵌简化版,不带有任何尖括号——当然,你可以将这种标记混合并配置在脚本中。Ant 本身就是一个 Jar 文件的集合。通过将它们添加到类路径中,可以轻松地在 Groovy 中使用它们。我们相信,使用 AntBuilder
可以形成一种更简洁、可读性更好的语法。
AntBuilder
直接使用我们在 Groovy 中所习惯的构建器表示法来表示 Ant 任务。下面是一个最简单的例子,在标准输出中打印一条消息:
def ant = new AntBuilder() 1⃣️
ant.echo('hello from Ant!') 2⃣️
1⃣️ 创建一个 AntBuilder
实例。
2⃣️ 利用参数中的消息执行 echo
任务。
假设需要创建 Zip 文件:
def ant = new AntBuilder()
ant.zip(destfile: 'sources.zip', basedir: 'src')
下面的例子展示了 AntBuilder
的一个用法,如何在 Groovy 中直接使用典型的 Ant 模式来复制一个文件列表:
// 调用一个任务
ant.echo("hello")
// Groovy 标记中的一个 Ant 代码块
ant.sequential {
echo("inside sequential")
def myDir = "target/AntTest/"
mkdir(dir: myDir)
copy(todir: myDir) {
fileset(dir: "src/test") {
include(name: "**/*.groovy")
}
}
echo("done")
}
// 常见的 Groovy 语句
def file = new File(ant.project.baseDir,"target/AntTest/groovy/util/AntTest.groovy")
assert file.exists()
下例展示的是如何按照特定模式对一个文件列表进行迭代:
// 创建一个文件集合扫描器
def scanner = ant.fileScanner {
fileset(dir:"src/test") {
include(name:"**/Ant*.groovy")
}
}
// 进行迭代
def found = false
for (f in scanner) {
println("Found file $f")
found = true
assert f instanceof File
assert f.name.endsWith(".groovy")
}
assert found
执行一个 JUnit 测试:
// 创建文件集合扫描器
ant.junit {
test(name:'groovy.util.SomethingThatDoesNotExist')
}
我们还可以更进一步地直接在 Groovy 中编译执行 Java 文件:
ant.echo(file:'Temp.java', '''
class Temp {
public static void main(String[] args) {
System.out.println("Hello");
}
}
''')
ant.javac(srcdir:'.', includes:'Temp.java', fork:'true')
ant.java(classpath:'.', classname:'Temp', fork:'true')
ant.echo('Done')
值得注意的是,AntBuilder
隶属于 Gradle ,所以你可以在 Gradle 中使用它,就像在 Groovy 中一样。详情可参看 Gradle 手册。
8.2.11. CliBuilder
(未编写)
8.2.12. ObjectGraphBuilder
针对能够遵循 JavaBean 规范的 bean,ObjectGraphBuilder
可以构建出任意的 bean 图表,特别适用于创建测试数据。
先来看看下面这些类:
package com.acme
class Company {
String name
Address address
List employees = []
}
class Address {
String line1
String line2
int zip
String state
}
class Employee {
String name
int employeeId
Address address
Company company
}
然后使用 ObjectGraphBuilder
构建一个带有三个雇员的 Company
,这非常简单:
def builder = new ObjectGraphBuilder() 1⃣️
builder.classLoader = this.class.classLoader 2⃣️
builder.classNameResolver = "com.acme" 3⃣️
def acme = builder.company(name: 'ACME') { 4⃣️
3.times {
employee(id: it.toString(), name: "Drone $it") { 5⃣️
address(line1:"Post street") 6⃣️
}
}
}
assert acme != null
assert acme instanceof Company
assert acme.name == 'ACME'
assert acme.employees.size() == 3
def employee = acme.employees[0]
assert employee instanceof Employee
assert employee.name == 'Drone 0'
assert employee.address instanceof Address
1⃣️ 创建一个新的对象图表构建器。
2⃣️ 设置用来解决类的类加载器。
3⃣️ 设置要解决类的基本包名。
4⃣️ 创建一个 Company
实例。
5⃣️ 以及 3 个 Employee
实例。
6⃣️ 每个实例都有一个独一无二的 Address
。
其实,对象图表构建器在幕后完成了下列任务:
- 试图匹配节点名为一个
Class
,采用需要包名的默认ClassNameResolver
策略。 - 然后将创建一个合适的类实例,该类使用默认的
NewInstanceResolver
策略,调用一个无参构造函数。 -
解决内嵌节点的父子关系,用到以下两种策略:
-
RelationNameResolver
将输出父节点的子属性名称,以及子节点的父属性名称(在该例中,Employee
的父属性将被命名为company
)。 ChildPropertySetter
会将子节点插入父节点,考虑子节点是否属于Collection
。(本例中,employees
应是Company
下的Employee
实例。)
-
所有这 4 个策略都有一个默认的实现,如果代码遵循编写 JavaBean 的常见规范,这个默认实现就能正常运作。在遇到 bean 或对象并不遵循常见规范时,就需要用你自己的策略实现。比如构建一个不可变的类:
@Immutable
class Person {
String name
int age
}
假如想用构建器创建一个 Person
:
def person = builder.person(name:'Jon', age:17)
那么在运行时就会失败,相关消息为:
Cannot set readonly property: name for class: com.acme.Person
改变新实例的策略就能修补这个问题:
builder.newInstanceResolver = { Class klazz, Map attributes ->
if (klazz.isAnnotationPresent(Immutable)) {
def o = klazz.newInstance(attributes)
attributes.clear()
return o
}
klazz.newInstance()
}
ObjectGraphBuilder
支持每个节点的 id,这即是说可以将节点引用保存在构建器中。这非常适用于多个对象引用一个实例的情况。在一些域模型 ObjectGraphBuilder
可能存在有名为 IdentifierResolver
的策略,以便可以配置以改变默认名称,在这种情况下,名为 id
的属性可能会有其他的业务含义。对于用来引用之前保存的实例的属性来说,这一情况也可能存在,ReferenceResolver
策略可能会产生正确的值(默认是 refId
):
def company = builder.company(name: 'ACME') {
address(id: 'a1', line1: '123 Groovy Rd', zip: 12345, state: 'JV') 1⃣️
employee(name: 'Duke', employeeId: 1, address: a1) 2⃣️
employee(name: 'John', employeeId: 2 ){
address( refId: 'a1' ) 3⃣️
}
}
1⃣️ 创建的地址都有个 id
。
2⃣️ employee
能根据 id
来引用 address
。
3⃣️ 或者使用相关地址的 refId
属性。
值得注意的是,对于被引用的 Bean 而言,你不能改变它的属性值。
8.2.13. JmxBuilder
详情参见 Working with JMX - JmxBuilder。
8.2.14. FileTreeBuilder
FileTreeBuilder 构建器用来生成符合特定规范的文件目录结构。比如创建下面这样的树:
src/
|--- main
| |--- groovy
| |--- Foo.groovy
|--- test
|--- groovy
|--- FooTest.groovy
可以使用一个 FileTreeBuilder
:
tmpDir = File.createTempDir()
def fileTreeBuilder = new FileTreeBuilder(tmpDir)
fileTreeBuilder.dir('src') {
dir('main') {
dir('groovy') {
file('Foo.groovy', 'println "Hello"')
}
}
dir('test') {
dir('groovy') {
file('FooTest.groovy', 'class FooTest extends GroovyTestCase {}')
}
}
}
还可以使用 assert
来检查一切是否如我们预期所愿:
assert new File(tmpDir, '/src/main/groovy/Foo.groovy').text == 'println "Hello"'
assert new File(tmpDir, '/src/test/groovy/FooTest.groovy').text == 'class FooTest extends GroovyTestCase {}'
FileTreeBuilder
还支持一种简写语法:
tmpDir = File.createTempDir()
def fileTreeBuilder = new FileTreeBuilder(tmpDir)
fileTreeBuilder.src {
main {
groovy {
'Foo.groovy'('println "Hello"')
}
}
test {
groovy {
'FooTest.groovy'('class FooTest extends GroovyTestCase {}')
}
}
}
产生的目录结构与之前的那些 assert
所展示的相同:
assert new File(tmpDir, '/src/main/groovy/Foo.groovy').text == 'println "Hello"'
assert new File(tmpDir, '/src/test/groovy/FooTest.groovy').text == 'class FooTest extends GroovyTestCase {}'