2 编译时元编程
在 Groovy 中,编译时元编程能够容许编译时生成代码。这种转换会影响程序的抽象语法树(AST,Abstract Syntax Tree),这也就是我们在 Groovy 中把它成为 AST 转换的原因。AST 转换能使我们实时了解编译过程,继而修改 AST,从而继续编译过程,生成常规的字节码。与运行时元编程相比,在类文件自身中(也就是说,在字节码内)就可以看到变化。这一点是非常重要的,比如说当你想让转换成为类抽象一部分时(实现接口,继承抽象类,等等),或者甚至当需要让类可从 Java (或其他的 JVM 语言)中调用时。例如,AST 转换可以为一个类添加一些方法。如果用运行时元编程来实现的话,新方法只能可见于 Groovy;而用编译时元编程来实现,新方法也可以在 Java 中显现出来。最后一点也同样重要,编译时元编程的性能要好过运行时元编程(因为不再需要初始化过程)。
本节中,我们将要探讨与 Groovy 分发版所绑定的各种编译时转换,而在随后的一节中,再来介绍 如何实现自定义的 AST 转换 ,以及这一技术的优点。
2.1 可用的 AST 转换
Groovy 有很多可用的 AST 转换,它们可满足不同的需求:减少样板文件(代码生成),实现设计模式(委托等模式),记录日志,声明并发,克隆,更安全地记录脚本,编译微调,实现 Swing 模式,测试并最终管理各种依赖。如果发现没有任何一个转换能够满足特定需求,还可以自定义转换,详情请看: 开发自定义 AST 转换 。
AST 转换可分为两大类:
- 全局 AST 转换。它们的应用是透明的,具有全局性,只要能在类路径上找到它们,就可以使用它们。
- 本地 AST 转换。利用标记来注解源代码。与全局 AST 转换不同,本地 AST 转换可能支持形式参数。
Groovy 并不带有任何的全局 AST 转换,但你可以在这里找到一些可用的本地 AST 转换:
2.1.1 代码生成转换
这一类转换包含能够去除样板文件代码的 AST 转换。样板文件代码通常是一种必须编写然而又没有任何有用信息的代码。通过自动生成这种样板文件代码,剩下必须要写的代码就变得清晰而简洁起来,从而就减少了因为样板文件代码不正确而引入的错误。
@groovy.transform.ToString
@ToString
AST 转换能够生成人类可读的类的 toString
形式。比如,像下面这样注解 Person
类会自动为你生成 toString
方法。
import groovy.transform.ToString
@ToString
class Person {
String firstName
String lastName
}
根据这种定义,下列断言就得以通过,意味着已经生成了一个 toString
方法,它会从类中获取字段值,并将它们打印出来。
def p = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p.toString() == 'Person(Jack, Nicholson)'
@ToString
标注接受以下列表中显示的几个参数。
属性 | 默认值 | 描述 | 范例 |
---|---|---|---|
includeNames | false | 是否在生成的 toString 中包含属性名 | @ToString(includeNames=true) class Person { String firstName String lastName } def p = new Person(firstName: 'Jack', lastName: 'Nicholson') assert p.toString() == 'Person(firstName:Jack, lastName:Nicholson)' |
excludes | 空列表 | 从 toString 中排除的属性列表 | @ToString(excludes=['firstName']) class Person { String firstName String lastName } def p = new Person(firstName: 'Jack', lastName: 'Nicholson') assert p.toString() == 'Person(Nicholson)' |
includes | 空列表 | toString 中包含的字段列表 | @ToString(includes=['lastName']) class Person { String firstName String lastName } def p = new Person(firstName: 'Jack', lastName: 'Nicholson') assert p.toString() == 'Person(Nicholson)' |
includeSuper | False | 超类是否应在 toString 中 | @ToString class Id { long id } @ToString(includeSuper=true) class Person extends Id { String firstName String lastName } def p = new Person(id:1, firstName: 'Jack', lastName: 'Nicholson') assert p.toString() == 'Person(Jack, Nicholson, Id(1))' |
includeSuperProperties | False | 超属性是否应包含在 toString 中 | class Person { String name } @ToString(includeSuperProperties = true, includeNames = true) class BandMember extends Person { String bandName } def bono = new BandMember(name:'Bono', bandName: 'U2').toString() assert bono.toString() == 'BandMember(bandName:U2, name:Bono)' |
includeFields | False | 除了属性之外,字段是否应包括在 toString 中 | @ToString(includeFields=true) class Person { String firstName String lastName private int age void test() { age = 42 } } def p = new Person(firstName: 'Jack', lastName: 'Nicholson') p.test() assert p.toString() == 'Person(Jack, Nicholson, 42)' |
ignoreNulls | False | 是否应显示带有 null 值的属性/字段 | @ToString(ignoreNulls=true) class Person { String firstName String lastName } def p = new Person(firstName: 'Jack') assert p.toString() == 'Person(Jack)' |
includePackage | False | 在 toString 中使用完全限定的类名,而非简单类名 | @ToString(includePackage=true) class Person { String firstName String lastName } def p = new Person(firstName: 'Jack', lastName:'Nicholson') assert p.toString() == 'acme.Person(Jack, Nicholson)' |
cache | False | 缓存 toString 字符串。如果类不可变,是否应只设为 true | @ToString(cache=true) class Person { String firstName String lastName } def p = new Person(firstName: 'Jack', lastName:'Nicholson') def s1 = p.toString() def s2 = p.toString() assert s1 == s2 assert s1 == 'Person(Jack, Nicholson)' assert s1.is(s2) // 同一实例 |
@groovy.transform.EqualsAndHashCode
@EqualsAndHashCode
AST 转换主要目的是为了生成 equals
和 hashCode
方法。生成的散列码遵循 Josh Bloch 所著的 Effective Java 中所介绍的最佳实践:
import groovy.transform.EqualsAndHashCode
@EqualsAndHashCode
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
def p2 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1==p2
assert p1.hashCode() == p2.hashCode()
下面是一些用来调整 @EqualsAndHashCode
行为的选项:
属性 | 默认值 | 描述 | 范例 |
excludes | 空列表 | 从 equals / hashCode 中需要排除的属性列表 | import groovy.transform.EqualsAndHashCode @EqualsAndHashCode(excludes=['firstName']) class Person { String firstName String lastName } def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson') def p2 = new Person(firstName: 'Bob', lastName: 'Nicholson') assert p1==p2 assert p1.hashCode() == p2.hashCode() |
includes | 空列表 | equals/hashCode 所包括的字段列表 | import groovy.transform.EqualsAndHashCode @EqualsAndHashCode(includes=['lastName']) class Person { String firstName String lastName } def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson') def p2 = new Person(firstName: 'Bob', lastName: 'Nicholson') assert p1==p2 assert p1.hashCode() == p2.hashCode() |
callSuper | False | 在 equals 或 hashcode 计算中是否包含 super | import groovy.transform.EqualsAndHashCode @EqualsAndHashCode class Living { String race } @EqualsAndHashCode(callSuper=true) class Person extends Living { String firstName String lastName } def p1 = new Person(race:'Human', firstName: 'Jack', lastName: 'Nicholson') def p2 = new Person(race: 'Human beeing', firstName: 'Jack', lastName: 'Nicholson') assert p1!=p2 assert p1.hashCode() != p2.hashCode() |
includeFields | False | 除了属性之外,是否应将字段包含在 equals / hashCode 之中 | @ToString(includeFields=true) class Person { String firstName String lastName private int age void test() { age = 42 } } def p = new Person(firstName: 'Jack', lastName: 'Nicholson') p.test() assert p.toString() == 'Person(Jack, Nicholson, 42)' |
cache | False | 缓存 hashCode 计算。如果类不可改变,是否只应将其设为 true。 | @ToString(cache=true) class Person { String firstName String lastName } def p = new Person(firstName: 'Jack', lastName:'Nicholson') def s1 = p.toString() def s2 = p.toString() assert s1 == s2 assert s1 == 'Person(Jack, Nicholson)' assert s1.is(s2) // 同一实例 |
useCanEqual | True | equals 是否应调用 canEqual 辅助方法 | 参看 http://www.artima.com/lejava/articles/equality.html |
@groovy.transform.TupleConstructor
@TupleConstructor
标注主要用处在于,通过生成构造函数消除样板文件代码。为每个属性创建一个元组构造函数,并配置默认值(使用的是 Java 默认值)。比如,下面的代码就会生成 3 个构造函数:
import groovy.transform.TupleConstructor
@TupleConstructor
class Person {
String firstName
String lastName
}
// 传统的映射样式的构造函数
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
// 生成的元组构造函数
def p2 = new Person('Jack', 'Nicholson')
// 生成的元组构造函数,带有第二个属性的默认值
def p3 = new Person('Jack')
第一个构造函数是一个不带实际参数的构造函数,能够实现传统的映射样式的构造。值得一提的是,如果第一个属性(或字段)类型为 LinkedHashMap,或者如果存在一个单一的 Map,AbstractMap 或 HashMap 类型的属性(或字段),则映射样式的变换不可用。
另一个构造函数则是按照属性定义顺序来获取属性从而生成的。Groovy 会生成与属性(或字段,具体是什么则取决于选项)相对应的构造函数。
@TupleConstructor
AST 转换接受以下几种配置选项:
属性 | 默认值 | 描述 | 范例 |
excludes | 空列表 | 元组构造函数生成过程中排除的属性列表 | import groovy.transform.TupleConstructor @TupleConstructor(excludes=['lastName']) class Person { String firstName String lastName } def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson') def p2 = new Person('Jack') try { // will fail because the second property is excluded def p3 = new Person('Jack', 'Nicholson') } catch (e) { assert e.message.contains ('Could not find matching constructor') } |
includes | 空列表 | 元组构造函数生成过程中包括的字段列表 | import groovy.transform.TupleConstructor @TupleConstructor(includes=['firstName']) class Person { String firstName String lastName } def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson') def p2 = new Person('Jack') try { // will fail because the second property is not included def p3 = new Person('Jack', 'Nicholson') } catch (e) { assert e.message.contains ('Could not find matching constructor') } |
includeFields | False | 除了属性之外,元组构造函数生成过程中应包含的字段 | import groovy.transform.TupleConstructor @TupleConstructor(includeFields=true) class Person { String firstName String lastName private String occupation public String toString() { "$firstName $lastName: $occupation" } } def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson', occupation: 'Actor') def p2 = new Person('Jack', 'Nicholson', 'Actor') assert p1.firstName == p2.firstName assert p1.lastName == p2.lastName assert p1.toString() == 'Jack Nicholson: Actor' assert p1.toString() == p2.toString() |
includeProperties | True | 元组构造函数生成过程中应包括的属性 | import groovy.transform.TupleConstructor @TupleConstructor(includeProperties=false) class Person { String firstName String lastName } def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson') try { def p2 = new Person('Jack', 'Nicholson') } catch(e) { // 因为没有包括进属性,所以将失败 } |
includeSuperFields | False | 元组构造函数生成过程中应包括的超级类中的字段 | import groovy.transform.TupleConstructor class Base { protected String occupation public String occupation() { this.occupation } } @TupleConstructor(includeSuperFields=true) class Person extends Base { String firstName String lastName public String toString() { "$firstName $lastName: ${occupation()}" } } def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson', occupation: 'Actor') def p2 = new Person('Actor', 'Jack', 'Nicholson') assert p1.firstName == p2.firstName assert p1.lastName == p2.lastName assert p1.toString() == 'Jack Nicholson: Actor' assert p2.toString() == p1.toString() |
includeSuperProperties | True | 元组构造函数生成过程中应包含的超级类中的属性 | import groovy.transform.TupleConstructor class Base { String occupation } @TupleConstructor(includeSuperProperties=true) class Person extends Base { String firstName String lastName public String toString() { "$firstName $lastName: $occupation" } } def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson') def p2 = new Person('Actor', 'Jack', 'Nicholson') assert p1.firstName == p2.firstName assert p1.lastName == p2.lastName assert p1.toString() == 'Jack Nicholson: null' assert p2.toString() == 'Jack Nicholson: Actor' |
callSuper | False | 在对父构造函数调用中,超级属性究竟是被调用,还是被设置为属性 | import groovy.transform.TupleConstructor class Base { String occupation Base() {} Base(String job) { occupation = job?.toLowerCase() } } @TupleConstructor(includeSuperProperties = true, callSuper=true) class Person extends Base { String firstName String lastName public String toString() { "$firstName $lastName: $occupation" } } def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson') def p2 = new Person('ACTOR', 'Jack', 'Nicholson') assert p1.firstName == p2.firstName assert p1.lastName == p2.lastName assert p1.toString() == 'Jack Nicholson: null' assert p2.toString() == 'Jack Nicholson: actor' |
force | False | 默认,如果构造函数已经定义,转换将不起作用。将该属性设为 true,将生成构造函数,需要人工检查没有定义重复的构造函数 | 参见 java 文档 |
@groovy.transform.Canonical
@Canonical
AST 转换结合了 @ToString 、 @EqualsAndHashCode 和 @TupleConstructor 这三个标记的效果。
import groovy.transform.Canonical
@Canonical
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1.toString() == 'Person(Jack, Nicholson)' // @ToString 的效果
def p2 = new Person('Jack','Nicholson') // @TupleConstructor 的效果
assert p2.toString() == 'Person(Jack, Nicholson)'
assert p1==p2 // @EqualsAndHashCode 的效果
assert p1.hashCode()==p2.hashCode() // @EqualsAndHashCode 的效果
类似的不可变类可以通过 @Immutable AST 转换来生成。 @Canonical
AST 转换支持以下几种配置选项:
属性 | 默认值 | 描述 | 范例 |
excludes | 空列表 | 元组构造函数生成过程中排除的属性列表 | import groovy.transform.Canonical @Canonical(excludes=['lastName']) class Person { String firstName String lastName } def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson') assert p1.toString() == 'Person(Jack)' // @ToString 的效果 def p2 = new Person('Jack') // @TupleConstructor 的效果 assert p2.toString() == 'Person(Jack)' assert p1==p2 // @EqualsAndHashCode 的效果 assert p1.hashCode()==p2.hashCode() // @EqualsAndHashCode 的效果 |
includes | 空列表 | 元组构造函数生成过程中应包括的字段列表 | import groovy.transform.Canonical @Canonical(includes=['firstName']) class Person { String firstName String lastName } def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson') assert p1.toString() == 'Person(Jack)' // @ToString 的效果 def p2 = new Person('Jack') // @TupleConstructor 的效果 assert p2.toString() == 'Person(Jack)' assert p1==p2 // @EqualsAndHashCode 的效果 assert p1.hashCode()==p2.hashCode() // @EqualsAndHashCode 的效果 |
@groovy.transform.InheritConstructors
@InheritConstructor
AST 转换意在生成匹配超级构造函数的构造函数。在重写异常类时,这种标记非常有用。
import groovy.transform.InheritConstructors
@InheritConstructors
class CustomException extends Exception {}
// 所有这些都生成构造函数
new CustomException()
new CustomException("A custom message")
new CustomException("A custom message", new RuntimeException())
new CustomException(new RuntimeException())
// Java 7 only
// new CustomException("A custom message", new RuntimeException(), false, true)
@InheritConstructor
AST 转换支持以下几种配置选项:
属性 | 默认值 | 描述 | 范例 |
constructorAnnotations | False | 是否在拷贝时携带构造函数的标记 | @Retention(RetentionPolicy.RUNTIME) @Target([ElementType.CONSTRUCTOR]) public @interface ConsAnno {} class Base { @ConsAnno Base() {} } @InheritConstructors(constructorAnnotations=true) class Child extends Base {} assert Child.constructors[0].annotations[0].annotationType().name == 'ConsAnno' |
parameterAnnotations | False | 在复制构造函数时,是否携带构造函数参数中的标记 | @Retention(RetentionPolicy.RUNTIME) @Target([ElementType.PARAMETER]) public @interface ParamAnno {} class Base { Base(@ParamAnno String name) {} } @InheritConstructors(parameterAnnotations=true) class Child extends Base {} assert Child.constructors[0].parameterAnnotations[0][0].annotationType().name == 'ParamAnno' |
@groovy.lang.Category
@Category
AST 转换简化了 Groovy 类别的创建工作。过去,Groovy 创建类别的方法如下所示:
class TripleCategory {
public static Integer triple(Integer self) {
3*self
}
}
use (TripleCategory) {
assert 9 == 3.triple()
}
通过 @Category
转换,我们能通过实例样式(而不必采用静态样式类)的类来实现。从而不必让每个方法的第一个参数是接收者。类型可以写成下面这样:
@Category(Integer)
class TripleCategory {
public Integer triple() { 3*this }
}
use (TripleCategory) {
assert 9 == 3.triple()
}
注意,在类中可以通过 this
引用。值得一提的是,在类别类中使用实例字段这一做法本身并不安全:类并不具有状态性(与特征不同)。
@groovy.transform.IndexedProperty
@IndexedProperty
标记用于为列表或数组类型的属性生成索引化的 getter/setter 方法。如果像利用 Java 来使用一个 Groovy 类,这就显得特别有用。Groovy 支持利用 Gpath 去访问属性,而这一点不适用于 Java。 @IndexedProperty
标记生成索引化属性的方式如下:
class SomeBean {
@IndexedProperty String[] someArray = new String[2]
@IndexedProperty List someList = []
}
def bean = new SomeBean()
bean.setSomeArray(0, 'value')
bean.setSomeList(0, 123)
assert bean.someArray[0] == 'value'
assert bean.someList == [123]
@groovy.lang.Lazy
@Lazy
AST 转换实现了字段的惰性初始化。例如下列代码:
class SomeBean {
@Lazy LinkedList myField
}
它将产生如下代码:
List $myField
List getMyField() {
if ($myField!=null) { return $myField }
else {
$myField = new LinkedList()
return $myField
}
}
用于初始化字段的默认值是具有声明类型的默认构造函数。使用定义一个默认值,
class SomeBean {
@Lazy LinkedList myField = { ['a','b','c']}()
}
在这种情况下,生成的代码如下所示:
List $myField
List getMyField() {
if ($myField!=null) { return $myField }
else {
$myField = { ['a','b','c']}()
return $myField
}
}
如果字段声明多变,初始化可以通过 双重检查锁定 模式来同步。
使用 soft=true
参数,辅助字段将转而使用 SoftReference
,从而较为简单地实现了缓存。在这种情况下,如果垃圾回收器决定收集引用,会在下次访问字段之时进行初始化。
@groovy.lang.Newify
@Newify
AST 转换用于为构造对象提供替代语法:
- 使用
Python
风格的语法:
@Newify([Tree,Leaf])
class TreeBuilder {
Tree tree = Tree(Leaf('A'),Leaf('B'),Tree(Leaf('C')))
}
- 使用
Ruby
风格的语法:
@Newify([Tree,Leaf])
class TreeBuilder {
Tree tree = Tree.new(Leaf.new('A'),Leaf.new('B'),Tree.new(Leaf.new('C')))
}
将 auto
标志设为 false
,可禁用 Ruby
风格的语法表达形式。
@groovy.transform.Sortable
@Sortable
AST 转换被用于帮助编写能够实现 Comparable
接口并可按照多种属性快速进行排序的类。下面的范例展示了它的易用性,其中,我们注释了 Person
类:
import groovy.transform.Sortable
@Sortable class Person {
String first
String last
Integer born
}
所产生的类具有下列属性:
- 实现了
Comparable
接口。 - 包含一个
compareTo
方法,以及根据first
、last
、born
属性自然排序的一个实现。 - 拥有返回比较器的三个方法:
comparatorByFirst
、comparatorByLast
和comparatorByBorn
。
生成的 compareTo
方法如下所示:
public int compareTo(java.lang.Object obj) {
if (this.is(obj)) {
return 0
}
if (!(obj instanceof Person)) {
return -1
}
java.lang.Integer value = this.first <=> obj.first
if (value != 0) {
return value
}
value = this.last <=> obj.last
if (value != 0) {
return value
}
value = this.born <=> obj.born
if (value != 0) {
return value
}
return 0
}
作为生成的比较器之一, comparatorByFirst
拥有的 compare
方法应如下所示:
public int compare(java.lang.Object arg0, java.lang.Object arg1) {
if (arg0 == arg1) {
return 0
}
if (arg0 != null && arg1 == null) {
return -1
}
if (arg0 == null && arg1 != null) {
return 1
}
return arg0.first <=> arg1.first
}
Person
类可以用在希望出现 Comparable
的地方,生成的比较器则出现在希望出现 Comparator
的任何地方,如下所示:
def people = [
new Person(first: 'Johnny', last: 'Depp', born: 1963),
new Person(first: 'Keira', last: 'Knightley', born: 1985),
new Person(first: 'Geoffrey', last: 'Rush', born: 1951),
new Person(first: 'Orlando', last: 'Bloom', born: 1977)
]
assert people[0] > people[2]
assert people.sort()*.last == ['Rush', 'Depp', 'Knightley', 'Bloom']
assert people.sort(false, Person.comparatorByFirst())*.first == ['Geoffrey', 'Johnny', 'Keira', 'Orlando']
assert people.sort(false, Person.comparatorByLast())*.last == ['Bloom', 'Depp', 'Knightley', 'Rush']
assert people.sort(false, Person.comparatorByBorn())*.last == ['Rush', 'Depp', 'Bloom', 'Knightley']
通常,所有的属性(properties)都会按照它们在定义时的优先顺序应用于生成的 compareTo
方法中。通过提供在 includes
或 excludes
注释的属性(attribute)中的一列属性(property)名,可以从生成的 compareTo
方法中包括或排除某些特定的属性(property)。如果使用 include
,在对比时,属性(property)名的顺序将决定属性的优先级别。为了说明这一点,请看下列这个 Person
类定义:
@Sortable(includes='first,born') class Person {
String last
int born
String first
}
其中包含两个对比方法: comparatorByFirst
和 comparatorByBorn
。生成的 compareTo
方法如下所示:
public int compareTo(java.lang.Object obj) {
if (this.is(obj)) {
return 0
}
if (!(obj instanceof Person)) {
return -1
}
java.lang.Integer value = this.first <=> obj.first
if (value != 0) {
return value
}
value = this.born <=> obj.born
if (value != 0) {
return value
}
return 0
}
Person
类可以这样用:
def people = [
new Person(first: 'Ben', last: 'Affleck', born: 1972),
new Person(first: 'Ben', last: 'Stiller', born: 1965)
]
assert people.sort()*.last == ['Stiller', 'Affleck']
@groovy.transform.builder.Builder
@Builder
AST 转换用来辅助编写能够使用 fluent API 调用所创建的类。该转换支持多种构建策略,以期涵盖多种用例,而且还可以采用一些配置选项来自定义构建过程。如果你非常擅长 AST,也可以定义自己的策略类。下面这张表列出了所有可能用到的与 Groovy 捆绑的策略,以及每个策略支持的配置选项。
策略 | 描述 | 构建类名 | 构建器方法名 | 构建方法名 | 前缀 | 包含与排除 |
---|---|---|---|---|---|---|
SimpleStrategy | 链接的 setter | n/a | n/a | n/a | 有,默认是'set' | 有 |
ExternalStrategy | 显式构建器类 | n/a | n/a | 默认是 'build' | 有,默认是 "" | 有 |
DefaultStrategy | 创建内嵌辅助类 | 存在,默认是<类型名>Builder | 有,默认是 'builder' | 有,默认是 'build' | 有,默认是 'default' | 有 |
InitializerStrategy | 创建提供类型安全 fluent 创建的内嵌辅助类 | 存在,默认是<类型名>Initializer | 有,默认是 'createInitializer' | 有,默认 'create',但往往只用于内部。 | 有,默认是 "" | 有 |
SimpleStrategy
为了使用 SimpleStrategy
,可以使用 @Builder
注释 Groovy 类,并指定策略。如下所示:
import groovy.transform.builder.*
@Builder(builderStrategy=SimpleStrategy)
class Person {
String first
String last
Integer born
}
用链接的方式来调用 setter 方法,如下所示:
def p1 = new Person().setFirst('Johnny').setLast('Depp').setBorn(1963)
assert "$p1.first $p1.last" == 'Johnny Depp'
对于每个属性(property)将会创建一个 setter 方法:
public Person setFirst(java.lang.String first) {
this.first = first
return this
}
然后指定一个前缀:
import groovy.transform.builder.*
@Builder(builderStrategy=SimpleStrategy, prefix="")
class Person {
String first
String last
Integer born
}
调用链接的 setter 方法:
def p = new Person().first('Johnny').last('Depp').born(1963)
assert "$p.first $p.last" == 'Johnny Depp'
可以联合使用 SimpleStrategy
与 @Canonical
。如果 @Builder
注释并没有显式的 includes
或 excludes
注释属性,而 @Canonical
注释却有这样的属性,那么 @Canonical
的这些属性将会重用于 @Builder
。
该策略并不支持注释属性 builderClassName
、 buildMethodName
、 builderMethodName
和 forClass
。
Groovy 已经有了内建的构建机制,如果内建机制不能满足你的要求,不要急于使用 @Builder
。以下是一些范例:
def p2 = new Person(first: 'Keira', last: 'Knightley', born: 1985)
def p3 = new Person().with {
first = 'Geoffrey'
last = 'Rush'
born = 1951
}
ExternalStrategy
为了使用 ExternalStrategy
,使用 @Builder
创建并注释一个 Groovy 构建器类,使用 forClass
指定构建器所针对的类,指定使用 ExternalStrategy
。假设构建器应用于下列类:
class Person {
String first
String last
int born
}
需要显式地创建并使用构建器类:
import groovy.transform.builder.*
@Builder(builderStrategy=ExternalStrategy, forClass=Person)
class PersonBuilder { }
def p = new PersonBuilder().first('Johnny').last('Depp').born(1963).build()
assert "$p.first $p.last" == 'Johnny Depp'
注意,你所提供的构建器类(通常为空)就会被传入正确的 setter 及一个构建方法。生成的构建方法如下所示:
public Person build() {
Person _thePerson = new Person()
_thePerson.first = first
_thePerson.last = last
_thePerson.born = born
return _thePerson
}
构建器所应用的类可以是任何 Java 或 Groovy 类,只要它们满足通常的 JavaBean 语法规范即可,比如一个无参构造函数和用于属性的 setter 方法。下面是一个使用 Java 类的例子:
import groovy.transform.builder.*
@Builder(builderStrategy=ExternalStrategy, forClass=javax.swing.DefaultButtonModel)
class ButtonModelBuilder {}
def model = new ButtonModelBuilder().enabled(true).pressed(true).armed(true).rollover(true).selected(true).build()
assert model.isArmed()
assert model.isPressed()
assert model.isEnabled()
assert model.isSelected()
assert model.isRollover()
使用 prefix
、 includes
、 excludes
及 buildMethodName
注释属性可以自定义生成的构建器。下面是一个自定义设置的例子:
import groovy.transform.builder.*
import groovy.transform.Canonical
@Canonical
class Person {
String first
String last
int born
}
@Builder(builderStrategy=ExternalStrategy, forClass=Person, includes=['first', 'last'], buildMethodName='create', prefix='with')
class PersonBuilder { }
def p = new PersonBuilder().withFirst('Johnny').withLast('Depp').create()
assert "$p.first $p.last" == 'Johnny Depp'
用于 @Builder
的注释方法 builderMethodName
和 builderClassName
并不适用于该策略。
可以联合使用 ExternalStrategy
与 @Canonical
。如果 @Builder
注释并没有显式的 includes
或 excludes
注释属性,而 @Canonical
注释却有这样的属性,那么 @Canonical
的这些属性将会重用于 @Builder
。
DefaultStrategy
要想使用 DefaultStrategy
,就必须使用注释 @Builder
来注释 Groovy 类:
import groovy.transform.builder.Builder
@Builder
class Person {
String firstName
String lastName
int age
}
def person = Person.builder().firstName("Robert").lastName("Lewandowski").age(21).build()
assert person.firstName == "Robert"
assert person.lastName == "Lewandowski"
assert person.age == 21
如果愿意,可以使用 builderClassName
、 buildMethodName
、 builderMethodName
、 prefix
、 includes
和 excludes
注释属性来自定义构建过程的各个环节。下例展示了其中的一些用法:
import groovy.transform.builder.Builder
@Builder(buildMethodName='make', builderMethodName='maker', prefix='with', excludes='age')
class Person {
String firstName
String lastName
int age
}
def p = Person.maker().withFirstName("Robert").withLastName("Lewandowski").make()
assert "$p.firstName $p.lastName" == "Robert Lewandowski"
这种策略还支持注释静态方法及构造函数。在这种情况下,静态方法或构造函数会成为用于构建的属性,而对于静态方法的情况而言,方法的返回类型会成为将要构建的目标类。如果在类中(可以位于类、方法或者构造函数内)用到了多个 @Builder
注释,那么就要由你来保证辅助类和工厂方法的名称唯一性(也就是使用默认名称值的不能多于一个)。下例展示了方法与构造函数的用法(还展示如何为了保证名称唯一性而所需进行的重命名)。
import groovy.transform.builder.*
import groovy.transform.*
@ToString
@Builder
class Person {
String first, last
int born
Person(){}
@Builder(builderClassName='MovieBuilder', builderMethodName='byRoleBuilder')
Person(String roleName) {
if (roleName == 'Jack Sparrow') {
this.first = 'Johnny'; this.last = 'Depp'; this.born = 1963
}
}
@Builder(builderClassName='NameBuilder', builderMethodName='nameBuilder', prefix='having', buildMethodName='fullName')
static String join(String first, String last) {
first + ' ' + last
}
@Builder(builderClassName='SplitBuilder', builderMethodName='splitBuilder')
static Person split(String name, int year) {
def parts = name.split(' ')
new Person(first: parts[0], last: parts[1], born: year)
}
}
assert Person.splitBuilder().name("Johnny Depp").year(1963).build().toString() == 'Person(Johnny, Depp, 1963)'
assert Person.byRoleBuilder().roleName("Jack Sparrow").build().toString() == 'Person(Johnny, Depp, 1963)'
assert Person.nameBuilder().havingFirst('Johnny').havingLast('Depp').fullName() == 'Johnny Depp'
assert Person.builder().first("Johnny").last('Depp').born(1963).build().toString() == 'Person(Johnny, Depp, 1963)'
该策略并不支持 forClass
注释属性。
InitializerStrategy
要想使用 InitializerStrategy
,需要使用 @Builder
注释你的 Groovy 类,然后指定策略,如下所示:
import groovy.transform.builder.*
import groovy.transform.*
@ToString
@Builder(builderStrategy=InitializerStrategy)
class Person {
String firstName
String lastName
int age
}
你的类可能会被锁定为包含一个配置有完整初始化器的公开构造函数。还包含一个用来创建初始化器的工厂方法。如下所示:
@CompileStatic
def firstLastAge() {
assert new Person(Person.createInitializer().firstName("John").lastName("Smith").age(21)).toString() == 'Person(John, Smith, 21)'
}
firstLastAge()
如果初始化器并不会涉及设置所有的属性(虽然次序并不重要),那么一旦使用初始化器,就会编译失败。如果不需要这么严格,就不需要使用 @CompileStatic
。
可以联合使用 InitializerStrategy
、 @Canonical
与 @Immutable
。如果 @Builder
注释并没有明显的 includes
或 excludes
注释属性但 @Canonical
注释却存在这样的属性,则 @Canonical
的这些属性就会被重用于 @Builder
。下面就是使用 @Builder
和 @Immutable
的范例:
import groovy.transform.builder.*
import groovy.transform.*
@Builder(builderStrategy=InitializerStrategy)
@Immutable
class Person {
String first
String last
int born
}
@CompileStatic
def createFirstLastBorn() {
def p = new Person(Person.createInitializer().first('Johnny').last('Depp').born(1963))
assert "$p.first $p.last $p.born" == 'Johnny Depp 1963'
}
createFirstLastBorn()
这一策略也支持注释静态方法与构造函数。在本例中,静态方法或构造函数参数成为构建过程所需的属性。对于静态方法而言,方法的返回类型正是正在构建的目标类。如果在类中有多个 @Builder
注释(可能位于类、方法或构造函数多个位置),那么要确保生成的辅助类及工厂方法的名称都具有唯一性(默认名称值只能使用一次,不能被多次使用)。关于使用 DefaultStrategy
的方法与构造函数的相关用法范例,可参见该策略的文档。
该策略并不支持注释属性 forClass
。
2.1.2 类设计注释
这一类别的注释主要用于简化一些知名模式(委托、单例,等等)的实现,采用的是一种声明式的风格。
@groovy.lang.Delegate
@Delegate
AST 转换主要用于实现委托设计模式。以下列类为例:
class Event {
@Delegate Date when
String title
}
利用 @Delegate
注释 when
字段,意味着 Event
类将把对 Date
方法的所有调用都委托给 when
字段。该例中,生成的代码如下所示:
class Event {
Date when
String title
boolean before(Date other) {
when.before(other)
}
// ...
}
然后就可以直接在 Event
类中调用 before
方法了:
def ev = new Event(title:'Groovy keynote', when: Date.parse('yyyy/MM/dd', '2013/09/10'))
def now = new Date()
assert ev.before(now)
@Delegate
AST 转换行为可以通过下列参数来修改:
属性 | 默认值 | 描述 | 范例 |
interfaces | True | 由字段所实现的接口是否也能由类来实现 | interface Greeter { void sayHello() } class MyGreeter implements Greeter { void sayHello() { println 'Hello!'} } class DelegatingGreeter { // 没有明显的接口 @Delegate MyGreeter greeter = new MyGreeter() } def greeter = new DelegatingGreeter() assert greeter instanceof Greeter // 显式地添加接口 |
deprecated | false | 如果为真,也会对由@Deprecated注释的方法进行委托 | class WithDeprecation { @Deprecated void foo() {} } class WithoutDeprecation { @Deprecated void bar() {} } class Delegating { @Delegate(deprecated=true) WithDeprecation with = new WithDeprecation() @Delegate WithoutDeprecation without = new WithoutDeprecation() } def d = new Delegating() d.foo() // 成功的原因在于 deprecated=true d.bar() // 由于 @Deprecated 而失败 |
methodAnnotations | False | 是否要将受托类的方法的所有注释都留给请托类的方法 | class WithAnnotations { @Transactional void method() { } } class DelegatingWithoutAnnotations { @Delegate WithAnnotations delegate } class DelegatingWithAnnotations { @Delegate(methodAnnotations = true) WithAnnotations delegate } def d1 = new DelegatingWithoutAnnotations() def d2 = new DelegatingWithAnnotations() assert d1.class.getDeclaredMethod('method').annotations.length==0 assert d2.class.getDeclaredMethod('method').annotations.length==1 |
parameterAnnotations | False | 是否将受托类方法参数的所有注释都留给请托方法 | class WithAnnotations { void method(@NotNull String str) { } } class DelegatingWithoutAnnotations { @Delegate WithAnnotations delegate } class DelegatingWithAnnotations { @Delegate(parameterAnnotations = true) WithAnnotations delegate } def d1 = new DelegatingWithoutAnnotations() def d2 = new DelegatingWithAnnotations() assert d1.class.getDeclaredMethod('method',String).parameterAnnotations[0].length==0 assert d2.class.getDeclaredMethod('method',String).parameterAnnotations[0].length==1 |
excludes | 空数组 | 一列要从委托中去除的方法。要想实现细粒度更高的操控,可以试试 excludeTypes | class Worker { void task1() {} void task2() {} } class Delegating { @Delegate(excludes=['task2']) Worker worker = new Worker() } def d = new Delegating() d.task1() // 通过 d.task2() // 失败,因为方法被排除 |
includes | 空数组 | 包含在委托内的一列方法,要想实现细粒度更高的操控,可以试试 includeTypes | class Worker { void task1() {} void task2() {} } class Delegating { @Delegate(includes=['task1']) Worker worker = new Worker() } def d = new Delegating() d.task1() // 通过 d.task2() // 失败,是因为方法未被包含 |
excludeTypes | 空数组 | 含有将要排除出委托外的方法签名的接口 | interface AppendStringSelector { StringBuilder append(String str) } class UpperStringBuilder { @Delegate(excludeTypes=AppendStringSelector) StringBuilder sb1 = new StringBuilder() @Delegate(includeTypes=AppendStringSelector) StringBuilder sb2 = new StringBuilder() String toString() { sb1.toString() + sb2.toString().toUpperCase() } } def usb = new UpperStringBuilder() usb.append(3.5d) usb.append('hello') usb.append(true) assert usb.toString() == '3.5trueHELLO' |
includeTypes | 空数组 | 含有将要被委托包含的方法签名的接口 | interface AppendBooleanSelector { StringBuilder append(boolean b) } interface AppendFloatSelector { StringBuilder append(float b) } class NumberBooleanBuilder { @Delegate(includeTypes=AppendBooleanSelector, interfaces=false) StringBuilder nums = new StringBuilder() @Delegate(includeTypes=[AppendFloatSelector], interfaces=false) StringBuilder bools = new StringBuilder() String result() { "${nums.toString()} |
@groovy.transform.Immutable
@Immutable
AST 转换简化了不可变类(类的成员被认为是不可变的)的创建工作。为了实现这样的目的,只需像下面这样注释类即可:
import groovy.transform.Immutable
@Immutable
class Point {
int x
int y
}
利用 @Immutable
注释生成的不可变类都是 final 类型的类。要想使类不可变,必须确保属性的类型是不可变(原始类型或装箱类型),或某种知名的不可变类型,或者是其他用 @Immutable
注释过的类。 @Immutable
施加于类上的效果与应用 @Canonical
AST 转换非常相似,但却带有一种不可变类:自动生成的 toString
、 equals
与 hashCode
方法,而且在该例中如果修改属性就会抛出 ReadOnlyPropertyException
异常。
既然 @Immutable
依赖一组预定义的已知不可变类(比如 java.net.URI
或 java.lang.String
),而且如果使用一种不再该组内的类型,就会导致失败,那么一定要了解下面这些参数。
属性 | 默认值 | 描述 | 范例 |
---|---|---|---|
knownImmutableClasses | 空列表 | 被认为不可变的一列类 | import groovy.transform.Immutable import groovy.transform.TupleConstructor @TupleConstructor final class Point { final int x final int y public String toString() { "($x,$y)" } } @Immutable(knownImmutableClasses=[Point]) class Triangle { Point a,b,c } |
knownImmutables | 空列表 | 被认为不可变的一列属性名 | import groovy.transform.Immutable import groovy.transform.TupleConstructor @TupleConstructor final class Point { final int x final int y public String toString() { "($x,$y)" } } @Immutable(knownImmutables=['a','b','c']) class Triangle { Point a,b,c } |
copyWith | false | 用来确定是否生成 copyWith( Map ) 方法的一个布尔值 | import groovy.transform.Immutable @Immutable( copyWith=true ) class User { String name Integer age } def bob = new User( 'bob', 43 ) def alice = bob.copyWith( name:'alice' ) assert alice.name == 'alice' assert alice.age == 43 |
@groovy.transform.Memoized
@Memoized
AST 转换简化了缓存实现,只需通过 @Memoized
注释方法,就使方法调用结果能够得到缓存。考虑下面这个方法:
long longComputation(int seed) {
// 延缓计算
Thread.sleep(1000*seed)
System.nanoTime()
}
该例基于方法的实际参数,模拟了一个大型计算。如果没有 @Memoized
,每个方法调用就将占去几秒钟的时间,返回一个随机结果:
def x = longComputation(1)
def y = longComputation(1)
assert x!=y
添加 @Memoized
后,由于加入了缓存,根据以下参数,改变了方法的语义:
@Memoized
long longComputation(int seed) {
// 延缓计算
Thread.sleep(1000*seed)
System.nanoTime()
}
def x = longComputation(1) // 1 秒后返回结果
def y = longComputation(1) // 立刻返回结果
def z = longComputation(2) // 2 秒后返回结果
assert x==y
assert x!=z
缓存的大小可以通过 2 个可选参数来配置:
- protectedCacheSize 结果数目,这些结果不会被垃圾回收。
- maxCacheSize 存入内存中的最大结果数目。
默认情况下,缓存数目并没有限制,并且没有缓存结果能够避免被垃圾回收。protectedCacheSize>0 将会创建一个无限制的缓存,其中一些结果能够避免被回收。若设置 maxCacheSize>0
,则将创建一个受限的缓存,无法避免被垃圾回收。将两个参数都进行设置,则可以创建一种受限而又受保护的缓存。
@groovy.lang.Singleton
@Singleton
注释用于在一个类中实现单例模式。默认时,使用类初始化会马上定义单例实例,或者会延迟(字段通过双重检查锁定来初始化):
@Singleton
class GreetingService {
String greeting(String name) { "Hello, $name!" }
}
assert GreetingService.instance.greeting('Bob') == 'Hello, Bob!'
默认时,当类被初始化时,就会马上创建单例,并可通过 instance
属性被访问。通过 property
参数还可以改变单例的名称:
@Singleton(property='theOne')
class GreetingService {
String greeting(String name) { "Hello, $name!" }
}
assert GreetingService.theOne.greeting('Bob') == 'Hello, Bob!'
另外,还可以使用 lazy
参数来延迟初始化:
class Collaborator {
public static boolean init = false
}
@Singleton(lazy=true,strict=false)
class GreetingService {
static void init() {}
GreetingService() {
Collaborator.init = true
}
String greeting(String name) { "Hello, $name!" }
}
GreetingService.init() // 确保类被初始化
assert Collaborator.init == false
GreetingService.instance
assert Collaborator.init == true
assert GreetingService.instance.greeting('Bob') == 'Hello, Bob!'
在该例中,将 strict
参数设置为 false,从而能够定义我们自己的构造函数。
@groovy.transform.Mixin
废弃使用。可以考虑使用特性。
2.1.3 日志改进
Groovy 提供的 AST 转换可以帮助集成那些广泛使用的日志框架。值得一提的是,利用这些注释来注释类,并不会妨碍在类路径上添加合适的日志框架。
所有的转换的工作方式都差不多:
- 添加与日志记录器相关的静态 final
log
字段。 - 根据底层框架,将所有的对
log.level()
的调用封装为正确的log.isLevelEnabled
防护(guard)。
这些转换支持两种参数:
- 与日志记录器字段名称相关的
value
(默认为log
)。 - 表示日志记录器类别名称的
category
(默认为类名)。
@groovy.util.logging.Log
首先要介绍的日志 AST 转换是 @Log
注释,依赖的是 JDK 日志框架:
@groovy.util.logging.Log
class Greeter {
void greet() {
log.info 'Called greeter'
println 'Hello, world!'
}
}
上面这样写与下面这样写是等同的:
import java.util.logging.Level
import java.util.logging.Logger
class Greeter {
private static final Logger log = Logger.getLogger(Greeter.name)
void greet() {
if (log.isLoggable(Level.INFO)) {
log.info 'Called greeter'
}
println 'Hello, world!'
}
}
@groovy.util.logging.Commons
Groovy 支持 Apache Commons Logging 框架,用到了 @Commons
注释。
@groovy.util.logging.Commons
class Greeter {
void greet() {
log.debug 'Called greeter'
println 'Hello, world!'
}
}
上面这样写与下面这样写是等同的:
import org.apache.commons.logging.LogFactory
import org.apache.commons.logging.Log
class Greeter {
private static final Log log = LogFactory.getLog(Greeter)
void greet() {
if (log.isDebugEnabled()) {
log.debug 'Called greeter'
}
println 'Hello, world!'
}
}
@groovy.util.logging.Log4j
Groovy 还支持 Apache Log4j 1.x 框架,使用的是 @Log4j
注释:
@groovy.util.logging.Log4j
class Greeter {
void greet() {
log.debug 'Called greeter'
println 'Hello, world!'
}
}
上面这样写与下面这样写是等同的:
import org.apache.log4j.Logger
class Greeter {
private static final Logger log = Logger.getLogger(Greeter)
void greet() {
if (log.isDebugEnabled()) {
log.debug 'Called greeter'
}
println 'Hello, world!'
}
}
@groovy.util.logging.Log4j2
Groovy 还支持 Apache Log4j 2.x 框架,用的是 @Log4j2
注释:
@groovy.util.logging.Log4j2
class Greeter {
void greet() {
log.debug 'Called greeter'
println 'Hello, world!'
}
}
上面这样写与下面这样写是等同的:
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
class Greeter {
private static final Logger log = LogManager.getLogger(Greeter)
void greet() {
if (log.isDebugEnabled()) {
log.debug 'Called greeter'
}
println 'Hello, world!'
}
}
@groovy.util.logging.Slf4j
Groovy 支持 Simple Logging Facade for Java (SLF4J) 框架,使用 @Slf4j
注释:
@groovy.util.logging.Slf4j
class Greeter {
void greet() {
log.debug 'Called greeter'
println 'Hello, world!'
}
}
上面这样写与下面这样写是等同的:
import org.slf4j.LoggerFactory
import org.slf4j.Logger
class Greeter {
private static final Logger log = LoggerFactory.getLogger(Greeter)
void greet() {
if (log.isDebugEnabled()) {
log.debug 'Called greeter'
}
println 'Hello, world!'
}
}
2.1.4 声明式并发
Groovy 提供一系列注释,以声明式的方式来简化常见的并发模式。
@groovy.transform.Synchronized
@Synchronized
AST 转换与 synchronized
关键字运作方式相似,但为了更安全的并发,更关注不同对象。它可以用于任何方法或静态方法:
import groovy.transform.Synchronized
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
class Counter {
int cpt
@Synchronized
int incrementAndGet() {
cpt++
}
int get() {
cpt
}
}
上面这种写法等于创建一个锁定对象,然后将整个方法封装进一个同步块:
class Counter {
int cpt
private final Object $lock = new Object()
int incrementAndGet() {
synchronized($lock) {
cpt++
}
}
int get() {
cpt
}
}
@Synchronized
默认创建一个名为 $lock
(对于静态方法而言是 $LOCK
),但是通过指定值属性,可以使用任何想用的字段,如下所示:
import groovy.transform.Synchronized
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
class Counter {
int cpt
private final Object myLock = new Object()
@Synchronized('myLock')
int incrementAndGet() {
cpt++
}
int get() {
cpt
}
}
@groovy.transform.WithReadLock and @groovy.transform.WithWriteLock
@WithReadLock
AST 转换一般与 @WithWriteLock
转换协同使用,利用 JDK 提供的 ReentrantReadWriteLock
实现读/写同步功能。可以为方法或静态方法添加注释。显式创建一个 final 类型的 $reentrantLock
字段(对于静态方法来说是 $REENTRANTLOCK
)并且添加正确的同步代码。范例如下所示:
import groovy.transform.WithReadLock
import groovy.transform.WithWriteLock
class Counters {
public final Map<String,Integer> map = [:].withDefault { 0 }
@WithReadLock
int get(String id) {
map.get(id)
}
@WithWriteLock
void add(String id, int num) {
Thread.sleep(200) // emulate long computation
map.put(id, map.get(id)+num)
}
}
上面写法等同于下面这样:
import groovy.transform.WithReadLock as WithReadLock
import groovy.transform.WithWriteLock as WithWriteLock
public class Counters {
private final Map<String, Integer> map
private final java.util.concurrent.locks.ReentrantReadWriteLock $reentrantlock
public int get(java.lang.String id) {
$reentrantlock.readLock().lock()
try {
map.get(id)
}
finally {
$reentrantlock.readLock().unlock()
}
}
public void add(java.lang.String id, int num) {
$reentrantlock.writeLock().lock()
try {
java.lang.Thread.sleep(200)
map.put(id, map.get(id) + num )
}
finally {
$reentrantlock.writeLock().unlock()
}
}
}
@WithReadLock
和 @WithWriteLock
都支持指定一种替代性的锁定对象。在那种情况下,用户必须声明引用的字段,如下所示:
import groovy.transform.WithReadLock
import groovy.transform.WithWriteLock
import java.util.concurrent.locks.ReentrantReadWriteLock
class Counters {
public final Map<String,Integer> map = [:].withDefault { 0 }
private final ReentrantReadWriteLock customLock = new ReentrantReadWriteLock()
@WithReadLock('customLock')
int get(String id) {
map.get(id)
}
@WithWriteLock('customLock')
void add(String id, int num) {
Thread.sleep(200) // emulate long computation
map.put(id, map.get(id)+num)
}
}
详情查看:
- groovy.transform.WithReadLock 的 Javadoc 文档。
- groovy.transform.WithWriteLock 的 Javadoc 文档。
2.1.5 更简便的克隆(cloning)与具体化(externalizing)
Groovy 提供了两种注释来改善 Clonable
和 Externalizable
接口的实现,分别是 @AutoClone
和 @AutoExternalize
。
@groovy.transform.AutoClone
@AutoClone
注释着重使用多种策略来实现 @java.lang.Cloneable
接口,其中 style
参数发挥了极大的作用:
- 默认的
AutoCloneStyle.CLONE
策略,在每个可克隆的属性上,首先调用super.clone()
然后是clone()
。 AutoCloneStyle.SIMPLE
策略使用正则构造函数,将源对象的属性调用并复制到克隆对象上。AutoCloneStyle.COPY_CONSTRUCTOR
策略创建并使用一个复制构造函数。AutoCloneStyle.SERIALIZATION
策略使用序列化(或具体化)来克隆对象。
关于每个策略的优缺点的论述可参考相关 Javadoc 文档: groovy.transform.AutoClone 和 groovy.transform.AutoCloneStyle 。
下面是一个范例:
import groovy.transform.AutoClone
@AutoClone
class Book {
String isbn
String title
List<String> authors
Date publicationDate
}
它等同于下面这种写法:
class Book implements Cloneable {
String isbn
String title
List<String> authors
Date publicationDate
public Book clone() throws CloneNotSupportedException {
Book result = super.clone()
result.authors = authors instanceof Cloneable ? (List) authors.clone() : authors
result.publicationDate = publicationDate.clone()
result
}
}
注意字符串属性都没有被显式地处理,这时因为字符串是不可变的,而且 Object
的 clone()
方法会复制字符串的引用。同样也适用于原始字段以及 java.lang.Number
绝大多数的具体子类。
除了克隆方式, @AutoClone
还支持多种选项:
属性 | 默认值 | 描述 | 范例 |
---|---|---|---|
excludes | 空列表 | 需要从克隆中排除的一列属性或字段名称。也允许使用包含由逗号分隔的字段/属性名的字符串。详情参看 groovy.transform.AutoClone#excludes 。 | import groovy.transform.AutoClone import groovy.transform.AutoCloneStyle @AutoClone(style=AutoCloneStyle.SIMPLE,excludes='authors') class Book { String isbn String title List authors Date publicationDate } |
includeFields | false | 默认只克隆属性。该表示为 true 时,也能克隆字段。 | import groovy.transform.AutoClone import groovy.transform.AutoCloneStyle @AutoClone(style=AutoCloneStyle.SIMPLE,includeFields=true) class Book { String isbn String title List authors protected Date publicationDate } |
@groovy.transform.AutoExternalize
@AutoExternalize
AST 转换可帮助创建 java.io.Externalizable
类。自动为类添加接口,生成 writeExternal
and readExternal
方法。比如下面这个范例:
import groovy.transform.AutoExternalize
@AutoExternalize
class Book {
String isbn
String title
float price
}
可以转换为:
class Book implements java.io.Externalizable {
String isbn
String title
float price
void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(isbn)
out.writeObject(title)
out.writeFloat( price )
}
public void readExternal(ObjectInput oin) {
isbn = (String) oin.readObject()
title = (String) oin.readObject()
price = oin.readFloat()
}
}
@AutoExternalize
注释支持的两个参数能使我们稍微对它的行为进行自定义:
属性 | 默认值 | 描述 | 范例 |
---|---|---|---|
excludes | 空列表 | 需要从具体化过程中排除的一列属性或字段名称。也允许使用包含由逗号分隔的字段/属性名的字符串。详情参看 groovy.transform.AutoExternalize#excludes 。 | import groovy.transform.AutoExternalize @AutoExternalize(excludes='price') class Book { String isbn String title float price } |
includeFields | false | 默认只具体化属性。该表示为 true 时,也能克隆字段。 | import groovy.transform.AutoExternalize @AutoExternalize(includeFields=true) class Book { String isbn String title protected float price } |
2.1.6 更安全的脚本
利用 Groovy,在运行时可以更容易地执行用户的脚本(比如使用 groovy.lang.GroovyShell
),但我们如何判定脚本不会耗光所有的 CPU 资源(无限循环)或者并发脚本不会逐渐消耗光线程池中的所有可用线程呢?为了打造更安全的脚本,Groovy 提供了几个注释,它们有多种功能,比如可以自动中断执行。
@groovy.transform.ThreadInterrupt
JVM 中经常遇到一种复杂情况:线程无法停止。虽然有一个 Thread#stop
方法,但它已经是不建议采用或者说弃用的(不可靠),所以唯一的机会就在于使用 Thread#interrupt
。调用这个方法会在线程中设置 interrupt
标记,但却不会 停止 线程。这就会造成麻烦:需要由线程中执行的代码负责检查该标记并正确退出。只有当开发者确切地知道执行的代码要在一个独立的线程中执行,这才有意义,但一般来说开发者无从得知。更糟糕的是,用户的脚本甚至不知道用于执行代码的线程是哪一个(想想 DSL)。
@ThreadInterrupt
注释简化了这些操作,在下面这些关键结构中添加了线程中断检查:
- 循环结构(for、while)。
- 方法的首指令。
- 闭包体的首指令。
比如下面这个用户脚本:
while (true) {
i++
}
显然这是一个无限循环。如果这段代码在自己的线程中执行,中断就没有任何意义:如果在线程上使用 join
,调用代码还能继续运行,但线程依然是活跃的,在后台运行,你根本没法终止它,慢慢地就会把资源用尽。
设置自己的 shell 是一种解决方法:
def config = new CompilerConfiguration()
config.addCompilationCustomizers(
new ASTTransformationCustomizer(ThreadInterrupt)
)
def binding = new Binding(i:0)
def shell = new GroovyShell(binding,config)
配置该 shell ,自动在所有脚本上使用 @ThreadInterrupt
AST 转换。然后就可以这样执行用户脚本了:
def t = Thread.start {
shell.evaluate(userCode)
}
t.join(500) // 用来使脚本结束的时间最多不超过 500 毫秒
if (t.alive) {
t.interrupt()
}
转换会自动修改用户代码:
while (true) {
if (Thread.currentThread().interrupted) {
throw new InterruptedException('The current thread has been interrupted.')
}
i++
}
循环中引入的检查可以保证如果 interrupt
标记设置在当前线程中,就会抛出一个异常,打断线程执行。
@ThreadInterrupt
支持多种选项,能够进一步自定义转换的行为:
属性 | 默认值 | 描述 | 范例 |
---|---|---|---|
thrown | java.lang.InterruptedException | 指定线程中断时应抛出的异常的类型 | class BadException extends Exception { BadException(String message) { super(message) } } def config = new CompilerConfiguration() config.addCompilationCustomizers( new ASTTransformationCustomizer(thrown:BadException, ThreadInterrupt) ) def binding = new Binding(i:0) def shell = new GroovyShell(this.class.classLoader,binding,config) def userCode = """ try { while (true) { i++ } } catch (BadException e) { i = -1 } """ def t = Thread.start { shell.evaluate(userCode) } t.join(1000) // 结束脚本的时间最多为 1 秒 assert binding.i > 0 if (t.alive) { t.interrupt() } Thread.sleep(500) assert binding.i == -1''' |
checkOnMethodStart | true | 每个方法体开始处是否插入中断检查。详情参看: groovy.transform.ThreadInterrupt | @ThreadInterrupt(checkOnMethodStart=false) |
applyToAllClasses | true | 同一源单位(在同一源文件中)的所有类是否该应用这个转换。详情参看: groovy.transform.ThreadInterrupt | @ThreadInterrupt(applyToAllClasses=false) class A { ... } // 添加的中断检查 class B { ... } // 没有中断检查 |
applyToAllMembers | true | 类的所有成员是否该应用同一转换。详情参见: groovy.transform.ThreadInterrupt | class A { @ThreadInterrupt(applyToAllMembers=false) void method1() { ... } // 添加的中断检查 void method2() { ... } // 没有中断检查 } |
@groovy.transform.TimedInterrupt
@TimedInterrupt
AST 转换所要解决的问题与 @groovy.transform.ThreadInterrupt 稍微有所不同:不是检查线程的 interrupt
标记,而是当线程运行时间过长时,会自动抛出异常。
该注释并 不产生 监控线程。它的工作方式类似于 @ThreadInterrupt
:在代码中合适的位置处放置检查。这意味着如果出现 I/O 导致的线程阻塞,线程就 不会 被中断。
考虑下面这样的代码:
def fib(int n) { n<2?n:fib(n-1)+fib(n-2) }
result = fib(600)
这种斐波那契数列计算的实现还远远称不上完美。如果以较高的 n
值调用,所需的响应时间就会长达几分钟。利用 @TimedInterrupt
,你可以选择允许脚本运行的时间。下面的设置代码使用户代码最多只能运行 1 秒钟:
def config = new CompilerConfiguration()
config.addCompilationCustomizers(
new ASTTransformationCustomizer(value:1, TimedInterrupt)
)
def binding = new Binding(result:0)
def shell = new GroovyShell(this.class.classLoader, binding,config)
上面的代码相当于利用 @TimedInterrupt
来注释类:
@TimedInterrupt(value=1, unit=TimeUnit.SECONDS)
class MyClass {
def fib(int n) {
n<2?n:fib(n-1)+fib(n-2)
}
}
该转换的行为可以通过 @TimedInterrupt
的几个选项来进行自定义:
属性 | 默认值 | 描述 | 范例 |
---|---|---|---|
value | Long.MAX_VALUE | 与 unit 一起使用,用来指定执行超时时间。 | @TimedInterrupt(value=500L, unit= TimeUnit.MILLISECONDS, applyToAllClasses = false) class Slow { def fib(n) { n |
unit |
| 与 value 一起使用,用来指定执行超时时间。 | @TimedInterrupt(value=500L, unit= TimeUnit.MILLISECONDS, applyToAllClasses = false) class Slow { def fib(n) { n |
thrown | java.util.concurrent.TimeoutException | 指定如果超时后抛出的异常类型。 | @TimedInterrupt(thrown=TooLongException, applyToAllClasses = false, value=1L) class Slow { def fib(n) { Thread.sleep(100); n |
checkOnMethodStart | true | 中断检查是否应该在每个方法体开始处插入。详情参见: groovy.transform.TimedInterrupt 。 | @TimedInterrupt(checkOnMethodStart=false) |
applyToAllClasses | true | 同源单位内的所有类是否应该使用同一转换。详情参见: groovy.transform.TimedInterrupt 。 | @TimedInterrupt(applyToAllClasses=false) class A { ... } // 添加中断检查 class B { ... } // 无中断检查 |
applyToAllMembers | true | 转换是否应该应用于类的所有成员。详情参见: groovy.transform.TimedInterrupt | class A { @TimedInterrupt(applyToAllMembers=false) void method1() { ... } // 添加中断检查 void method2() { ... } // 无中断检查 } |
注意: @TimedInterrupt
目前并不兼容静态方法!
@groovy.transform.ConditionalInterrupt
为了创建更安全的脚本,最后还有介绍一个基本注释,使用自定义策略中断脚本时会用到它,尤其是在使用资源管理(限制对 API 的调用次数)时,更是应该使用该注释。在下例中,虽然用户代码使用的是无限循环,但 @ConditionalInterrupt
还是能允许我们检查配额管理并自动打断脚本。
@ConditionalInterrupt({Quotas.disallow('user')})
class UserCode {
void doSomething() {
int i=0
while (true) {
println "Consuming resources ${++i}"
}
}
}
下面这个配额检查非常简单,但可以采用更复杂的逻辑来实现:
class Quotas {
static def quotas = [:].withDefault { 10 }
static boolean disallow(String userName) {
println "Checking quota for $userName"
(quotas[userName]--)<0
}
}
确保 @ConditionalInterrupt
能够正常运作于下面的代码:
assert Quotas.quotas['user'] == 10
def t = Thread.start {
new UserCode().doSomething()
}
t.join(5000)
assert !t.alive
assert Quotas.quotas['user'] < 0
当然,在实际运用中, @ConditionalInterrupt
不太可能自动添加到用户代码中。它的注入方式类似于 ThreadInterrupt 一节中范例所采用的那种方式,使用 org.codehaus.groovy.control.customizers.ASTTransformationCustomizer :
def config = new CompilerConfiguration()
def checkExpression = new ClosureExpression(
Parameter.EMPTY_ARRAY,
new ExpressionStatement(
new MethodCallExpression(new ClassExpression(ClassHelper.make(Quotas)), 'disallow', new ConstantExpression('user'))
)
)
config.addCompilationCustomizers(
new ASTTransformationCustomizer(value: checkExpression, ConditionalInterrupt)
)
def shell = new GroovyShell(this.class.classLoader,new Binding(),config)
def userCode = """
int i=0
while (true) {
println "Consuming resources \\${++i}"
}
"""
assert Quotas.quotas['user'] == 10
def t = Thread.start {
shell.evaluate(userCode)
}
t.join(5000)
assert !t.alive
assert Quotas.quotas['user'] < 0
@ConditionalInterrupt
支持的多种选项可以使我们深入自定义该转换的行为:
属性 | 默认值 | 描述 | 范例 |
---|---|---|---|
value | --- | 调用的闭包会检查是否允许执行。如果闭包返回 false,允许执行,否则抛出异常。 | @ConditionalInterrupt({ ... }) |
thrown | java.lang.InterruptedException | 如果执行应被终止,指定抛出的异常类型。 | config.addCompilationCustomizers( new ASTTransformationCustomizer(thrown: QuotaExceededException,value: checkExpression, ConditionalInterrupt) ) assert Quotas.quotas['user'] == 10 def t = Thread.start { try { shell.evaluate(userCode) } catch (QuotaExceededException) { Quotas.quotas['user'] = 'Quota exceeded' } } t.join(5000) assert !t.alive assert Quotas.quotas['user'] == 'Quota exceeded' |
checkOnMethodStart | true | 中断检查是否应该在每个方法体开始处插入。详情参见: groovy.transform.ConditionalInterrupt 。 | @ConditionalInterrupt(checkOnMethodStart=false) |
applyToAllClasses | true | 同源单位内(位于同一源文件)的所有类是否应该使用同一转换。详情参见: groovy.transform.ConditionalInterrupt 。 | @ConditionalInterrupt(applyToAllClasses=false) class A { ... } // 添加中断检查 class B { ... } // 无中断检查 |
applyToAllMembers | true | 转换是否应该应用于类的所有成员。详情参见: groovy.transform.ConditionalInterrupt | class A { @ConditionalInterrupt(applyToAllMembers=false) void method1() { ... } // 添加中断检查 void method2() { ... } // 没有中断检查 } |
2.1.7 编译器指令
这一类 AST 转换所包含的注释主要对代码语义进行直接影响,而不是用于代码生成。因此,它们似乎更应被视为能够在编译时或运行时改变程序行为的编译器指令。
@groovy.transform.Field
@Field
注释只适用于脚本,目的在于解决脚本中的常见范围错误。比如下面的范例在运行时就会出错:
def x
String line() {
"="*x
}
x=3
assert "===" == line()
x=5
assert "=====" == line()
这里抛出的错误可能难以解释: groovy.lang.MissingPropertyException: No such property: x
。其中的原因可能在于:脚本被编译为类,脚本体本身被编译为一个 run()
方法。脚本中定义的方法是独立的,所以上面的代码等同于:
class MyScript extends Script {
String line() {
"="*x
}
public def run() {
def x
x=3
assert "===" == line()
x=5
assert "=====" == line()
}
}
因此 def x
实际上被解析为一个本地变量,超出了 line
方法的作用范围。通过将变量的作用范围改为闭合脚本的字段, @Field
AST 转换可以修复这个问题。
@Field def x
String line() {
"="*x
}
x=3
assert "===" == line()
x=5
assert "=====" == line()
最后等同的结果代码就会变成这样:
class MyScript extends Script {
def x
String line() {
"="*x
}
public def run() {
x=3
assert "===" == line()
x=5
assert "=====" == line()
}
}
@groovy.transform.PackageScope
Groovy 可见性规则默认规定,如果没有为创建的字段指定修饰符,那么该字段就会被解释为属性(property):
class Person {
String name // 这是一个属性
}
如果你想创建一个包私有字段,而不是属性(私有字段+ getter/setter),那么利用 @PackageScope
来注释字段:
class Person {
@PackageScope String name // not a property anymore
}
@PackageScope
注释也可以用于类、方法及构造函数。另外,假如在类级别上将一列 PackageScopeTarget
值指定为注释属性,那么对于该类中所有无明确修饰符并且匹配所提供的 PackageScopeTarget
的成员而言,它们依旧属于包保护类型。下例中对类中的一些字段应用一些注释:
import static groovy.transform.PackageScopeTarget.FIELDS
@PackageScope(FIELDS)
class Person {
String name // 不是属性,包保护
Date dob // 不是属性,包保护
private int age // 明确修饰符,所以不会被修改
}
@PackageScope
注释几乎不属于 Groovy 的通常规范,但在有些情况下它也非常有用:需要在包内保持可见的工厂方法,或用于测试的方法或构造函数,再或者与需要这样的可见性规范的第三方库进行集成时。
@groovy.transform.AnnotationCollector
@AnnotationCollector
允许创建元注释(meta-annotation),这个概念曾在 专有小节 中介绍过。
@groovy.transform.TypeChecked
@TypeChecked
能够启用 Groovy 代码上的编译时类型检查,详情参见: 类型检查小节 。
@groovy.transform.CompileStatic
@CompileStatic
能够启用 Groovy 代码上的静态编译。详情参见: 类型检查小节 。
@groovy.transform.CompileDynamic
@CompileDynamic
能够禁止在部分 Groovy 代码上的编译。详情参见: 类型检查小节 。
@groovy.lang.DelegatesTo
从技术角度上说, @DelegatesTo
并不属于 AST 转换。它着重于为代码建立文档,并且当你使用 类型检查 或 静态编译 时帮助编译器。该注释的详细说明参见本文档的 DSL 部分 。
2.1.8 Swing 模式
@groovy.beans.Bindable
@Bindable
是一种能够将正则属性转换为绑定属性(根据 JavaBean 规范 )的 AST 转换。 @Bindable
注释可以放在属性或类上。假如想把类中的所有属性都转换为绑定属性,则可以像下例这样来注释类:
import groovy.beans.Bindable
@Bindable
class Person {
String name
int age
}
它其实等同于:
import java.beans.PropertyChangeListener
import java.beans.PropertyChangeSupport
class Person {
final private PropertyChangeSupport this$propertyChangeSupport
String name
int age
public void addPropertyChangeListener(PropertyChangeListener listener) {
this$propertyChangeSupport.addPropertyChangeListener(listener)
}
public void addPropertyChangeListener(String name, PropertyChangeListener listener) {
this$propertyChangeSupport.addPropertyChangeListener(name, listener)
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
this$propertyChangeSupport.removePropertyChangeListener(listener)
}
public void removePropertyChangeListener(String name, PropertyChangeListener listener) {
this$propertyChangeSupport.removePropertyChangeListener(name, listener)
}
public void firePropertyChange(String name, Object oldValue, Object newValue) {
this$propertyChangeSupport.firePropertyChange(name, oldValue, newValue)
}
public PropertyChangeListener[] getPropertyChangeListeners() {
return this$propertyChangeSupport.getPropertyChangeListeners()
}
public PropertyChangeListener[] getPropertyChangeListeners(String name) {
return this$propertyChangeSupport.getPropertyChangeListeners(name)
}
}
然而, @Bindable
会去掉类中的很多样本文件,极大提高可读性。如果注释放在一个属性上,那么只有这个属性才是绑定的:
import groovy.beans.Bindable
class Person {
String name
@Bindable int age
}
@groovy.beans.ListenerList
@ListenerList
AST 转换生成的代码用于添加、去除与获取与某类相关的一列侦听器,只需注释一个集合属性:
import java.awt.event.ActionListener
import groovy.beans.ListenerList
class Component {
@ListenerList
List<ActionListener> listeners;
}
该转换会根据列表的基本属性,生成适当的添加/去除方法。另外,它还根据在该类中声明的公开方法,创建 fireXXX
方法。
import java.awt.event.ActionEvent
import java.awt.event.ActionListener as ActionListener
import groovy.beans.ListenerList as ListenerList
public class Component {
@ListenerList
private List<ActionListener> listeners
public void addActionListener(ActionListener listener) {
if ( listener == null) {
return
}
if ( listeners == null) {
listeners = []
}
listeners.add(listener)
}
public void removeActionListener(ActionListener listener) {
if ( listener == null) {
return
}
if ( listeners == null) {
listeners = []
}
listeners.remove(listener)
}
public ActionListener[] getActionListeners() {
Object __result = []
if ( listeners != null) {
__result.addAll(listeners)
}
return (( __result ) as ActionListener[])
}
public void fireActionPerformed(ActionEvent param0) {
if ( listeners != null) {
ArrayList<ActionListener> __list = new ArrayList<ActionListener>(listeners)
for (def listener : __list ) {
listener.actionPerformed(param0)
}
}
}
}
通过几个选项,可以进一步定制 @Bindable
的行为:
属性 | 默认值 | 描述 | 范例 |
---|---|---|---|
name | 基本类型名称 | 默认,添加到添加/删除等方法中的后缀是列表基本类型的简单类名。 | class Component { @ListenerList(name='item') List listeners; } |
synchronize | false | 如果设为 true,生成的方法将会同步 | class Component { @ListenerList(synchronize = true) List listeners; } |
@groovy.beans.Vetoable
@Vetoable
注释的运作方式与 @Bindable
很像,根据 JavaBean 规范生成限制属性,而不是绑定属性。它可以放在属性上,也可以放在类上,放在类上时意味着所有属性就都转变成了受限属性。下例中,利用 @Vetoable
注释一个类:
import groovy.beans.Vetoable
import java.beans.PropertyVetoException
import java.beans.VetoableChangeListener
@Vetoable
class Person {
String name
int age
}
这等同于:
public class Person {
private String name
private int age
final private java.beans.VetoableChangeSupport this$vetoableChangeSupport
public void addVetoableChangeListener(VetoableChangeListener listener) {
this$vetoableChangeSupport.addVetoableChangeListener(listener)
}
public void addVetoableChangeListener(String name, VetoableChangeListener listener) {
this$vetoableChangeSupport.addVetoableChangeListener(name, listener)
}
public void removeVetoableChangeListener(VetoableChangeListener listener) {
this$vetoableChangeSupport.removeVetoableChangeListener(listener)
}
public void removeVetoableChangeListener(String name, VetoableChangeListener listener) {
this$vetoableChangeSupport.removeVetoableChangeListener(name, listener)
}
public void fireVetoableChange(String name, Object oldValue, Object newValue) throws PropertyVetoException {
this$vetoableChangeSupport.fireVetoableChange(name, oldValue, newValue)
}
public VetoableChangeListener[] getVetoableChangeListeners() {
return this$vetoableChangeSupport.getVetoableChangeListeners()
}
public VetoableChangeListener[] getVetoableChangeListeners(String name) {
return this$vetoableChangeSupport.getVetoableChangeListeners(name)
}
public void setName(String value) throws PropertyVetoException {
this.fireVetoableChange('name', name, value)
name = value
}
public void setAge(int value) throws PropertyVetoException {
this.fireVetoableChange('age', age, value)
age = value
}
}
如果该注释被加于一个属性上,那么只有该属性受限。
import groovy.beans.Vetoable
class Person {
String name
@Vetoable int age
}
2.1.9 测试辅助
@groovy.lang.NotYetImplemented
@NotYetImplemented
用于转换 JUnit 3/4 测试用例的结果。该注释非常适用于这样的情况:如果某个功能还未实现,但测试中实现了。在这种情况下,期望测试也会失败。 @NotYetImplemented
注释可反转测试结果,如下例所示:
import groovy.transform.NotYetImplemented
class Maths {
static int fib(int n) {
// 稍后实现的逻辑
}
}
class MathsTest extends GroovyTestCase {
@NotYetImplemented
void testFib() {
def dataTable = [
1:1,
2:1,
3:2,
4:3,
5:5,
6:8,
7:13
]
dataTable.each { i, r ->
assert Maths.fib(i) == r
}
}
}
使用该技术的另一优点是,可以在明白如何 bug 之前,为这些 bug 编写测试用例。假如未来某一时刻,某个代码改动意外地修补这个 Bug,你就会得到通知,因为原本希望测试是不能通过的。
@groovy.transform.ASTTest
@ASTTest
是一种比较特殊的 AST 转换,它能帮助调试其他的 AST 转换或 Groovy 编译器本身。有了它,开发者能够在编译时仔细研究 AST,在 AST 上执行断言,而不是在编译结果上。这意味着这一 AST 信息能使我们在执行字节码之前就访问 AST。 @ASTTest
可以放置在任意可注释的节点上,它需要两个参数:
- phase:设置在能够触发
@ASTTest
的阶段。测试代码将在该阶段的末尾在 AST 树上运行。 - value:当可注释节点一旦到达相应阶段就执行的代码。
编译阶段只能从 org.codehaus.groovy.control.CompilePhase 中选择。但是,因为不可能用同一注释对一个节点注释两次,所以你不能对两个不同编译阶段的同一节点使用 @ASTTest
。
value
是一个能够访问已注释节点的特殊变量 node
的闭包表达式。辅助 lookup
方法详细说明见下文。比如,我们可以像下面这样注释类节点:
import groovy.transform.ASTTest
import org.codehaus.groovy.ast.ClassNode
import static org.codehaus.groovy.control.CompilePhase.*
@ASTTest(phase=CONVERSION, value={ 1⃣️
assert node instanceof ClassNode 2⃣️
assert node.name == 'Person' 3⃣️
})
class Person {
}
1⃣️ 检查在 CONVERSION 阶段后的抽象语法树的状态。
2⃣️ 节点引用了由 @ASTTest
注释的 AST 节点。
3⃣️ 可以用于在编译时执行断言。
@ASTTest
有趣的一个功能是,假如断言失败,则 编译也失败 。假设要在编译时检查一个 AST 转换的行为。这里就可以使用 @PackageScope
。假如想验证 @PackageScope
注释的属性成为一个包私有字段。为了达到这个目的,就需要知道转换是在哪个阶段进行的,可以在这里找到: org.codehaus.groovy.transform.PackageScopeASTTransformation :语义分析。所以可以用下面这样的代码来写测试:
import groovy.transform.ASTTest
import groovy.transform.PackageScope
import static org.codehaus.groovy.control.CompilePhase.*
@ASTTest(phase=SEMANTIC_ANALYSIS, value= {
def nameNode = node.properties.find { it.name == 'name' }
def ageNode = node.properties.find { it.name == 'age' }
assert nameNode
assert ageNode == null // 再也不应该是一个属性了
def ageField = node.getDeclaredField 'age'
assert ageField.modifiers == 0
})
class Person {
String name
@PackageScope int age
}
@ASTTest
注释可以放在语法所容许的任何地方。有时你可以需要测试不可注释的 AST 节点的内容。这时 @ASTTest
会提供一个便利的 lookup
方法,用来为用特殊令牌标记的节点搜索 AST:
def list = lookup('anchor') 1⃣️
Statement stmt = list[0] 2⃣️
1⃣️ 返回包含标签是 anchor
的 AST 节点的列表。 2⃣️ 通常有必要选择处理的元素,因为 lookup
经常会返回一个列表。
假设需要测试一个 for 循环变量的声明类型,可以这样做:
import groovy.transform.ASTTest
import groovy.transform.PackageScope
import org.codehaus.groovy.ast.ClassHelper
import org.codehaus.groovy.ast.expr.DeclarationExpression
import org.codehaus.groovy.ast.stmt.ForStatement
import static org.codehaus.groovy.control.CompilePhase.*
class Something {
@ASTTest(phase=SEMANTIC_ANALYSIS, value= {
def forLoop = lookup('anchor')[0]
assert forLoop instanceof ForStatement
def decl = forLoop.collectionExpression.expressions[0]
assert decl instanceof DeclarationExpression
assert decl.variableExpression.name == 'i'
assert decl.variableExpression.originType == ClassHelper.int_TYPE
})
void someMethod() {
int x = 1;
int y = 10;
anchor: for (int i=0; i<x+y; i++) {
println "$i"
}
}
}
@ASTTest
通常会在测试闭包内暴露以下这些变量:
node
与通常一样,对应已注释的节点。compilationUnit
可访问当前的org.codehaus.groovy.control.CompilationUnit
。compilePhase
返回当前的编译阶段(org.codehaus.groovy.control.CompilePhase
)。
还有一个好玩的情况,假如没有指定 phase
属性。在这种情况下,闭包将会在每个编译阶段后 SEMANTIC_ANALYSIS
后(并包括)得以执行。转换的上下文在每个阶段后还保留着,以便检查在两次阶段中所改变的内容。
下面的范例介绍了如何转储在一个类节点上注册的一列 AST 信息:
import groovy.transform.ASTTest
import groovy.transform.CompileStatic
import groovy.transform.Immutable
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.control.CompilePhase
@ASTTest(value={
System.err.println "Compile phase: $compilePhase"
ClassNode cn = node
System.err.println "Global AST xforms: ${compilationUnit?.ASTTransformationsContext?.globalTransformNames}"
CompilePhase.values().each {
def transforms = cn.getTransforms(it)
if (transforms) {
System.err.println "Ast xforms for phase $it:"
transforms.each { map ->
System.err.println(map)
}
}
}
})
@CompileStatic
@Immutable
class Foo {
}
下例展示了如何记住在两个阶段之间用于测试的变量:
import groovy.transform.ASTTest
import groovy.transform.ToString
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.control.CompilePhase
@ASTTest(value={
if (compilePhase==CompilePhase.INSTRUCTION_SELECTION) { 1⃣️
println "toString() was added at phase: ${added}"
assert added == CompilePhase.CANONICALIZATION 2⃣️
} else {
if (node.getDeclaredMethods('toString') && added==null) { 3⃣️
added = compilePhase 4⃣️
}
}
})
@ToString
class Foo {
String name
}
1⃣️ 如果当前编译器阶段是指令选择阶段。
2⃣️ 那么我们要保证 toString
添加到 CANONICALIZATION
中。
3⃣️ 否则,如果 toString
存在, added
这个从上下文而来的变量就会为 null。
4⃣️ 那么,编译器阶段是添加 toString
的那个阶段。
2.1.10. Grape handling
@groovy.lang.Grab
@groovy.lang.GrabConfig
@groovy.lang.GrabExclude
@groovy.lang.GrabResolver
@groovy.lang.Grapes
Grape
是一个内嵌入 Groovy 中的依赖项管理引擎,它依赖于一些前文介绍过的注释。
2.2 开发 AST 转换
有两类转换:全局和局部。
- 全局转换 是由编译器应用于被编译的代码上的。实现全局转换的已编译类位于一个添加到编译器类路径的 Jar 文件中,含有服务定位器文件
META-INF/services/org.codehaus.groovy.transform.ASTTransformation
,其中含有转换类名。转换类必须是一个无参构造函数,并且实现了org.codehaus.groovy.transform.ASTTransformation
接口。它将针对编译时的每个源来运行,所以为提高编译速度起见,千万不要用粗放而且耗费时间的方式来创建能够扫描所有 AST 的转换。 - 局部转换 是一种通过注释那些想要转换的代码元素,从而实现局部应用的转换。为此,我们需要重用注释标记,这些注释标记应该实现
org.codehaus.groovy.transform.ASTTransformation
。编译器自然会发现它们,然后将转换应用于这些代码元素。
2.2.1 编译阶段指南
Groovy AST 转换只能应用于 9 个编译阶段( org.codehaus.groovy.control.CompilePhase )之一。
全局转换可以应用于任何一个阶段,而局部转换则只能应用于语义分析阶段或其后阶段。编译器阶段为:
- 初始化阶段 (Initialization):打开源文件,配置环境参数。
- 语法解析阶段 (Parsing):使用语法来产生表示源代码的令牌树。
- 转换阶段 (Conversion):从令牌树中创建抽象语法树(AST)。
- 语义分析阶段 (Semantic Analysis):针对一致性及有效性进行检查,这是语法所检查不了的,然后解析类。
- 规范化阶段 (Canonicalization):完整地构建 AST。
- 指令选择阶段 (Instruction Selection):选择指令集合,比如:Java 6 或 Java 7 字节码级别。
- 类生成阶段 (Class Generation):在内存中创建类的字节码。
- 输出阶段 (Output):编写二进制输出到文件系统。
- 终止阶段 (Finalization):执行最后的垃圾回收及清理工作。
总体来说,在阶段后期会获得更多的类型信息。如果转换涉及到读取 AST,稍后一些的阶段信息会更丰富,因此会更合适一些;如果转换涉及到写入 AST,则适宜采用较早的阶段,因为此时树的结构更稀疏。
2.2.2 局部转换
局部 AST 转换是相对于它们应用的上下文而言的。在多数情况下,上下文是由定义转换范围的注释所定义的。比如,定义一个字段意味著转换将应用到该字段上,而定义一个类也意味着转换将应用到整个类上。
下面来考虑一个简单的例子,假设想编写一个 @WithLogging
转换,在方法调用过程的起始阶段和结束阶段添加控制台消息。下面的 “Hello World” 范例会打印出 "Hello World" 以及起始和结束消息。
@WithLogging
def greet() {
println "Hello World"
}
greet()
使用局部 AST 转换就能轻松实现,它需要两个要素:
@WithLogging
注释的定义。org.codehaus.groovy.transform.ASTTransformation
的实现,将日志表达式添加到方法上。
ASTTransformation
是一个能够让你访问 org.codehaus.groovy.control.SourceUnit 的回调函数,通过它你可以获得一个 org.codehaus.groovy.ast.ModuleNode(AST) 的引用。
AST(抽象语法树)是一个树状结构,其中大多数是 org.codehaus.groovy.ast.expr.Expression(表达式) 或 org.codehaus.groovy.ast.expr.Statement(语句) 。学习 AST 的最佳途径是在调试器中探索它的用法。一旦有了 AST,就可以解析它,来了解代码信息或者重新编写加入新的功能。
局部转换注释是非常简单的,下面来看看 @WithLogging
:
import org.codehaus.groovy.transform.GroovyASTTransformationClass
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.METHOD])
@GroovyASTTransformationClass(["gep.WithLoggingASTTransformation"])
public @interface WithLogging {
}
注释的保持性是 SOURCE
,因为你不需要注释通过它。这里的元素类型是 METHOD
,因为注释 @WithLogging
应用到方法上。
但最重要的还是 @GroovyASTTransformationClass
注释。它将注释 @WithLogging
与你要编写的 ASTTransformation
类链接到一起。 gep.WithLoggingASTTransformation
是我们要编写的 ASTTransformation
完整限定类名。这行代码将注释链接到了转换中。
做好上面这些之后,每当源单元中出现 @WithLogging
时,Groovy 编译器就开始调用 gep.WithLoggingASTTransformation
。当运行这个简单脚本时,任何设置在 LoggingASTTransformation
中的断点都会在 IDE 中被触发。
ASTTransformation
类稍微复杂一些。下面是一个非常简单的转换,将为 @WithLogging
添加方法起始和结束的消息:
@CompileStatic 1⃣️
@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS) 2⃣️
class WithLoggingASTTransformation implements ASTTransformation { 3⃣️
@Override
void visit(ASTNode[] nodes, SourceUnit sourceUnit) { 4⃣️
MethodNode method = (MethodNode) nodes[1] 5⃣️
def startMessage = createPrintlnAst("Starting $method.name") 6⃣️
def endMessage = createPrintlnAst("Ending $method.name") 7⃣️
def existingStatements = ((BlockStatement)method.code).statements 8⃣️
existingStatements.add(0, startMessage) 9⃣️
existingStatements.add(endMessage) ⑩
}
private static Statement createPrintlnAst(String message) { ⑪
new ExpressionStatement(
new MethodCallExpression(
new VariableExpression("this"),
new ConstantExpression("println"),
new ArgumentListExpression(
new ConstantExpression(message)
)
)
)
}
}
1⃣️ 在 Groovy 中编写 AST 转换时,即使并不强制,也强烈建议使用 CompileStatic
,因为这能提高编译器的性能。
2⃣️ 利用 org.codehaus.groovy.transform.GroovyASTTransformation 来注释,从而得知转换运行的具体编译阶段。这里正处于语义解析阶段。
3⃣️ 实现 ASTTransformation
接口。
4⃣️ 该接口只有一个 visit
方法。
5⃣️ nodes
参数是一个包含 2 个 AST 节点的数组。第一个是注释节点( @WithLogging
),第二个是已被注释节点(方法节点)。
6⃣️ 创建一个语句,当进入方法时,打印消息。 7⃣️ 创建一个语句,当退出方法时,打印消息。 8⃣️ 方法体,在该例中是 BlockStatement
。
9⃣️ 在已有代码的第一个语句之前添加进入方法消息。
⑩ 在已有代码的最后一个语句之后添加结束方法消息。
⑪ 创建一个 ExpressionStatement
,用来封装一个对应于 this.println("message")
的 MethodCallExpression
。
重点是要注意本例的简洁风格,这里没有制定必要的检查,比如检查已注释节点是否真的是 MethodNode
,或者检查方法体是 BlockStatement
的实例,等等。完善该例的工作就留给读者来完成了。
createPrintlnAst(String)
方法中新打印语句的创建过程值得我们注意下。创建代码的 AST 并不总是非常简单的。本例中,我们需要构建一个新的方法调用,传入接收者/变量、方法名称,以及一个参数列表。在创建 AST 时,将要创建的代码用 Groovy 文件保存是很有用的,在调试器中调查代码的 AST 也能让你学会应该创建的内容。从而学会如何编写类似 createPrintlnAst
这样的函数。
最后:
@WithLogging
def greet() {
println "Hello World"
}
greet()
产生:
Starting greet
Hello World
Ending greet
一定要知道,AST 转换直接参与到了编译过程中。初学者常犯的一个错误是将同一源树的 AST 转换代码当成是使用转换的类。位于同一源树一般意味着它们会在同一时间被编译。因为转换本身会在阶段中被编译,每个编译阶段都会处理同一源单位的所有文件,然后交由下一阶段继续处理。直接后果就是:转换无法在使用它的类之前获得编译!总之,AST 转换需要在使用前进行预编译。一般来说,将它们放在不同的源树会更方便一些。
2.2.3 全局转换
全局 AST 转换跟局部转换的一个重大区别在于:不需要注释,这意味着它们是 全局 应用的,也就是要应用到每个被编译的类上。由于全局转换会对编译器效率产生极大影响,所以一定要把它们最后的手段,不到万不得已不可使用。
沿袭使用上文中 局部转换 的范例,追踪所有的方法,而不仅仅是那些受 @WithLogging
注释的方法。基本上,我们希望这种代码的行为类似于之前经过 @WithLogging
的方法:
def greet() {
println "Hello World"
}
greet()
实现这一点需要完成以下两步工作:
- 在
META-INF/services
目录中创建org.codehaus.groovy.transform.ASTTransformation
描述符。 - 创建
ASTTransformation
实现。
描述符文件是必需的,必须位于类路径中。它将包含这行代码:
META-INF/services/org.codehaus.groovy.transform.ASTTransformation
gep.WithLoggingASTTransformation
转换代码看起来与局部用例没什么区别,但我们使用的是 SourceUnit
,而不是 ASTNode[]
参数:
gep/WithLoggingASTTransformation.groovy
@CompileStatic 1⃣️
@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS) 2⃣️
class WithLoggingASTTransformation implements ASTTransformation { 3⃣️
@Override
void visit(ASTNode[] nodes, SourceUnit sourceUnit) { 4⃣️
def methods = sourceUnit.AST.methods 5⃣️
methods.each { method -> 6⃣️
def startMessage = createPrintlnAst("Starting $method.name") 7⃣️
def endMessage = createPrintlnAst("Ending $method.name") 8⃣️
def existingStatements = ((BlockStatement)method.code).statements 9⃣️
existingStatements.add(0, startMessage)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论