运行时和编译时元编程—编译时元编程

原文链接    译文链接     译者:JackWang

运行时和编译时元编程 第二部分

2 编译时元编程

Groovy的编译时元编程支持编译时生成代码。这些变换(译者注:原文该专有名词是transformations,译者直译为变换,也许不准确。如果有知道准确翻译的读者恳请不吝赐教,待译者修正)叫做程序的抽象语法树(AST),在Groovy里,我们叫做AST变换。AST变换支持在编译过程中植入钩子,修改抽象语法树之后继续编译生成正常的字节码流。和运行时元编程相比,这种转换可以在类文件的修改可见(或者说是字节码流)。如果你想要你的转换成为类协议的一部分(类协议是指实现接口,继承抽象类等等…)甚至你需要你的类可以被Java(或其他JVM语言)调用,在字节码流的修改可见是非常重要的。比如说,一个AST转换可以添加方法到一个类。在运行时元编程里,你需要使你的新方法在Groovy里可见,在编译时元编程要实现同样的功能,这个方法仅仅需要对Java可见即可。最后也同样重要的是,编译时元编程相比运行时元编程能活的更好的性能(因为没有初始化阶段)。

注:译者也是第一次接触Groovy,由于时间和水平有限(姑且让译者使用这个理由吧,对待知识本应该一丝不苟)部分专有名词可能翻译不准确甚至有误(读者阅读的过程中最好能参考原文),恳请读者不吝留言指出,谢谢!

在这个章节里,我们基于Groovy发布版本为基础,阐述各种编译时转换。我们将会说明如何实现你自己的AST转换以及这种技术的局限性。

2.1 AST转换能做什么

Groovy有各种各样的AST转换来满足不同的需求:删除模板(译者注:原文是reducing boilerplate)(代码生成),实现设计模式(委托,…),日志,同步,克隆,安全脚本,tweaking the compilation(译者实在不知道怎么翻译这句,直译太别扭),实现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 空列表(EmptyList) 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 空值是否显示
@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) // same instance

@groovy.transform.EqualsAndHashCode

@EqualsAndHashCode AST转换就是为了自动帮你生成equals和hashCode方法。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 Empty list 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 Empty list 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计算中
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) // same instance
useCanEqual true 是否equals可以调用canEquals帮助类方法 参见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
}

// traditional map-style constructor
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
// generated tuple constructor
def p2 = new Person('Jack', 'Nicholson')
// generated tuple constructor with default value for second property
def p3 = new Person('Jack')

第一个构造函数是无参构造函数,允许map-style风格构造。如果第一个属性(或字段)是LinkedHashMap或是单个Map就没有用了。AbstractMap或HashMap属性(或字段)在map-style的映射中不可用。
其他构造函数依照他们被定义的顺序进行构造。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) {
    // will fail because properties are not included
}
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,只要你保证不会重复定义,也会自动生成构造函数 参考 JavaDoc

@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)' // Effect of @ToString

def p2 = new Person('Jack','Nicholson') // Effect of @TupleConstructor
assert p2.toString() == 'Person(Jack, Nicholson)'

assert p1==p2 // Effect of @EqualsAndHashCode
assert p1.hashCode()==p2.hashCode() // Effect of @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)' // Effect of @ToString

def p2 = new Person('Jack') // Effect of @TupleConstructor
assert p2.toString() == 'Person(Jack)'

assert p1==p2 // Effect of @EqualsAndHashCode
assert p1.hashCode()==p2.hashCode() // Effect of @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)' // Effect of @ToString

def p2 = new Person('Jack') // Effect of @TupleConstructor
assert p2.toString() == 'Person(Jack)'

assert p1==p2 // Effect of @EqualsAndHashCode
assert p1.hashCode()==p2.hashCode() // Effect of @EqualsAndHashCode

@groovy.transform.InheritConstructors

@InheritConstructors AST 转换的作用就是自动为你生成匹配超类构造函数的构造函数。当重载异常类时很有用:

import groovy.transform.InheritConstructors

@InheritConstructors
class CustomException extends Exception {}

// all those are generated constructors
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.IndexProperty

@IndexedProperty注解用于为list/array类型的属性生成索引化的getters/setters。如果你从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
    }
}

如果字段被声明为volatile,初始化就会使用双重检查锁模式来同步。
使用soft=true参数,帮助类字段将会使用软引用。提供一种简单的方式来实现缓存。在这个例子里,如果垃圾回收器决定回收引用,下次访问该字段时将会再一次初始化。

@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的类,使其非常容易按数字大小排序。下面的代码提供了示例:

import groovy.transform.Sortable

@Sortable class Person {
    String first
    String last
    Integer born
}

产生的类具有一些属性:

  • 实现了Comparable接口
  • 包含一个基于自然排序的compareTo方法
  • 有三个方法可以返回比较器:compartorByFirst,compartorByLast和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
}

作为一个自动生成的比较器,compartorByFirst比较器将会有一个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的地方,生成的比较器如下面的例子所示:

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']

正常情况下,所有属性都会用于产生compareTo方法,按照它们定义的先后顺序排优先级。你可以通过添加includes或excludes注解在给定的属性列表前面来使compareTo方法包含或排除某些特定的属性。如果使用includes,列表中属性顺序决定了比较时的优先级。作为示例,考虑下面的Person类的定义:

@Sortable(includes='first,born') class Person {
    String last
    int born
    String first
}

compareTo方法将会产生两个比较器方法comparatorByFirst和comparatorByBorn方法:

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转换用于帮助写那些可用生成流API调用的类。这个转换支持多种构造策略来满足各种使用场景。同时支持许多配置选项给用户来配置构造过程。如果你是一个AST骇客,你可以定义自己的策略类。下表列出了Groovy的全部可用测策略,每条策略都支持配置选项。

策略 描述 builderClassName builderMethodName buildMethodNameprefix includes/excludes
SimpleStrategy chained setters n/a n/a n/a yes, default “set” yes
ExternalStrategy explicit builder class, class being built untouched n/a n/a yes, default “build” yes, default “” yes
DefaultStrategy creates a nested helper class yes, default Builder yes, default “builder” yes, default “build” yes, default “” yes
InitializerStrategy creates a nested helper class providing type-safe fluent creation yes, default Initializer yes, default “createInitializer” yes, default “create” but usually only used internally yes, default “” yes

(译者注:该表译者也不是很明白,暂时没有翻译,后续研究清楚再修改,若有高人明白,恳请留言指点)

SimpleStrategy

要使用SimpleStrategy,你需要在你的Groovy类前添加@Builder注解,示例如下:

import groovy.transform.builder.*

@Builder(builderStrategy=SimpleStrategy)
class Person {
    String first
    String last
    Integer born
}

然后可以使用链式风格调用setters方法:

def p1 = new Person().setFirst('Johnny').setLast('Depp').setBorn(1963)
assert "$p1.first $p1.last" == 'Johnny Depp'

对于每一个属性,自动生成的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
}

链式风格调用setters如下:

def p = new Person().first('Johnny').last('Depp').born(1963)
assert "$p.first $p.last" == 'Johnny Depp'

你可以配合着@Canonical注解来使用SimpleStrategy。如果你的@Builder注解没有明确的includes或excludes注解属性但是你的@Canonical有,则@Builder注解会重用@Canonical注解的属性。
这个策略不支持builderClassName,builderMethodName,builderMethodName和forClass注解属性。
Groovy已经支持有内置的building机制,如果内置的机制满足你的需求就不要使用@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 builder类,需要使用forClass属性指明使用ExternalStrategy策略的类。假如你有下面的一个Person类需要构建:

class Person {
    String first
    String last
    int born
}

你创建并这样使用你的builder:

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'

注意到你提供的builder类(通常是空)将会自动生成setters。生成的代码如下:

public Person build() {
    Person _thePerson = new Person()
    _thePerson.first = first
    _thePerson.last = last
    _thePerson.born = born
    return _thePerson
}

你创建的builder类可以是遵循普通JavaBean的Groovy类或Java类,比如说一个无参构造器和属性setters方法。这有一个使用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()

生成的builder可以通过prefix,includes,excludes和builderMethodName等注解属性来自由化定制,这有一个示例:

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'

你可以配合@Canonical注解来使用ExternalStrategy。如果你的@Builder注解没有包含includes或excludes注解属性但是你创建的builder的@Canonical注解声明了,@Builder会自动重用@Canonical的这些属性。

DefaultStrategy

要使用DefaultStrategy你需要在你的Groovy类前添加@Builder注解,示例如下:

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,builderMethodName,buildMethodName,prefix,includes和excludes注解属性来个性化你的building处理过程。其中一些属性示例如下:

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"

这个策略也支持注解静态方法和构造函数。这种情况下,静态方法或构造函数参数变成了要使用的building的属性,对于静态方法,方法的返回类型两变成要构造的目标类。如果你在一个类(要么是类,要么是方法或构造函数的位置)中使用超过一个的@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,你需要在你的Groovy类前添加@Builder注解。示例如下:

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注解。

你可以配合@Canonical和@Immutable来使用InitializerStrategy。如果你的@Builder注解没有明确includes和excludes注解属性但是你的@Canonical注解有,那么@Builder注解会重用@Canonical的。这有一个使用@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()

这个策略也支持注解静态方法和构造函数。这种情况下,静态方法或构造函数参数变成了要使用的building的属性,对于静态方法,方法的返回类型两变成要构造的目标类。如果你在一个类(要么是类,要么是方法或构造函数的位置)中使用超过一个的@Builder注解,将有你来确保生成的帮助类或工厂方法名字的唯一。
这个策略不支持forClass注解属性。

2.1.2 类设计注解

这个类别的注解目标是通过使用声明风格来简化那些著名的设计模式(委托,单例…)的实现。

@groovy.lang.Delegate
@Delegate AST转换时为了实现委托设计模式。具体见下面的示例:

class Event {
    @Delegate Date when
    String title
}

when 这个字段声明为了@Delegate,意味着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 { // no explicit interface
    @Delegate MyGreeter greeter = new MyGreeter()
}
def greeter = new DelegatingGreeter()
assert greeter instanceof Greeter // interface was added transparently
deprecated false 如果为true,魏国方法将会注解为@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() // passes thanks to deprecated=true
d.bar() // fails because of @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() // passes
d.task2() // fails because method is excluded
Includes 空列表 从委托机制包含的方法,需更加聚焦的控制,可以参考includeTypes
class Worker {
    void task1() {}
    void task2() {}
}
class Delegating {
    @Delegate(includes=['task1']) Worker worker = new Worker()
}
def d = new Delegating()
d.task1() // passes
d.task2() // fails because method is not included
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)如果你使用的类型不是在那个类别将会失败,你可以使用下表的一些参数将一些类型转为不可变类:

参数 默认值 描述 示例
knowImmutableClass 空列表 要视为不可变类的列表
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
}
knowImmutables 空列表 要视为不可变的属性列表
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.Memorized

@Memoized AST转换通过支持方法调用结果的缓存简化了缓存的实现。仅仅需要在方法前面添加@Memoized注解。想象下面的方法:

long longComputation(int seed) {
    // slow computation
    Thread.sleep(1000*seed)
    System.nanoTime()
}

基于实际的方法参数,这个方法可能需要一段长时间的计算。没有@Memoized,每个方法需要几秒钟来返回几个随机数。

def x = longComputation(1)
def y = longComputation(1)
assert x!=y

添加一个@Memoized就基于入参添加了缓存从而改变了方法的语义:

@Memoized
long longComputation(int seed) {
    // slow computation
    Thread.sleep(1000*seed)
    System.nanoTime()
}

def x = longComputation(1) // returns after 1 second
def y = longComputation(1) // returns immediatly
def z = longComputation(2) // returns after 2 seconds
assert x==y
assert x!=z

缓存的大小可以通过两个可选参数来配置:

  • protectedCacheSize,保证不被垃圾回收器回收掉的结果大小
  • maxCacheSize,可以保存的最大缓存大小

默认情况下,缓存的大小没有限制,垃圾回收器也可以回收全部缓存结果。通过将protectCacheSize设置大于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() // make sure class is initialized
assert Collaborator.init == false
GreetingService.instance
assert Collaborator.init == true
assert GreetingService.instance.greeting('Bob') == 'Hello, Bob!'

在这个例子里,我们设置了strict参数为false,可以允许我们自己创建自己的构造函数。

@groovy.transform.Mixin

已经废弃了,考虑使用traits来代替。

2.1.3 日志模块

Groovy提供了AST转换来帮助集成最广泛使用的日志框架。通过使用注解可以在一个类里添加日志信息。
所有的转换工作模式都类似:

  • 添加了一个static final log字段作为logger
  • 包装调用log.level()到合适的log.isLevelEnable,取决于具体的框架。

这些转换支持两个参数

  • value(默认是log)logger字段的名字
  • category(默认是类名)logger类别的名字

@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通过使用@Commons注解来支持Apache Commons Logging框架,示例如下:

@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通过@Log4j注解来支持Apache Log4j 1.x框架,示例如下:

@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通过@Log4j2注解来支持Apache Log4j 2.x框架。示例如下:

@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通过使用@Slf4j注解来支持Simple Logging Facade for Java(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和@groovy.transform.WithWriteLock

@WithReadLock AST转换配合着@WithWriteLock转换使用可以提供一个类似于JDK提供的ReentrantReadWriteLock锁的读写同步功能。这个注解可以加在方法或静态方法前面。它将会常见一个$reentrantLock final字段(对于静态方法是$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)
    }
}

详细的信息:

2.1.5 更简单的cloning和externalizing

Groovy 提供了两种注解来支持Cloneable和Exteranlizable接口的实现。分别叫做@AutoClone和@AutoExternalize

@groovy.transform.AutoClone
@AutoClone注解使用各种策略实现了@java.lang.Cloneable接口,由于有style参数:

  • 默认的AutoCloneStyle.CLONE策略先调用super.clone()方法然后调用每个可克隆属性的clone()方法
  • AutoCloneStyle.SIMPLE策略使用普通构造函数来复制所有属性
  • AutoCloneStyle.COPY_CONSTRUCTOR策略生成和使用一个copy构造函数
  • AutoCloneStyle.SERIALIZATION策略使用序列化来克隆一个对象

每种策略都有其优缺点,详细探讨可以在groovy.transform.AutoClonegroovy.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的大多数子类也一样。

除了克隆风格,@AutoStyle还支持多种参数:

参数 默认值 描述 示例
excludes 空列表 不需要clone的属性列表,可以使用逗号分隔多个属性,详见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 默认只会clone属性值,将这个参数设置为true,将会使字段也clone
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和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 Safer scripting

Groovy语言使得在运行时运行用户脚本变得非常容易。但是你怎么保证你的脚本运行不会耗掉全部CPU(死循环)或者同步运行的脚本不会降低线程池中线程的运行效率呢?Groovy提供了几个注解用于更加安全地运行脚本并且支持自动中断执行。

@groovy.transform.ThreadInterrupt

在一般情况下JVM是不能中断一个线程的执行。Thread#stop方法虽然存在但是已经被废弃,因此你唯一能依赖的就是Thread#interrupt。调用后者将会将线程的interrupt标志置为true,但是不会停止线程的执行。这是有问题的。因为代码执行需要检查中断标志并且正常退出。作为开发,知道你执行的代码运行在一个独立的线程上是有用的。但是通常你可能并不知道它。对于一些用户脚本可能情况更糟,他可能甚至不知道有线程这个概念。

@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)

这个转换会自动修改用户代码成这样:

def t = Thread.start {
    shell.evaluate(userCode)
}
t.join(500) // give at most 500ms for the script to complete
if (t.alive) {
    t.interrupt()
}

循环里面的中断检查保证了一旦发现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) // give at most 1s for the script to complete
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 { ... } // interrupt checks added
class B { ... } // no interrupt checks
applyToAllMembers true 是否转换可以用于这个类的所有成员,详见groovy.transform.ThreadInterrupt
class A {
    @ThreadInterrupt(applyToAllMembers=false)
    void method1() { ... } // interrupt checked added
    void method2() { ... } // no interrupt checks
}

@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<2?n:fib(n-1)+fib(n-2) }
}
def result
def t = Thread.start {
    result = new Slow().fib(500)
}
t.join(5000)
assert result == null
assert !t.alive
unit TimeUnit.SECONDS 配合value使用来指定一个方法的超时时间
@TimedInterrupt(value=500L, unit= TimeUnit.MILLISECONDS, applyToAllClasses = false)
class Slow {
    def fib(n) { n<2?n:fib(n-1)+fib(n-2) }
}
def result
def t = Thread.start {
    result = new Slow().fib(500)
}
t.join(5000)
assert result == null
assert !t.alive
thrown java.util.concurrent.TimeoutException 如果超时时间到了将会抛出特定的异常
@TimedInterrupt(thrown=TooLongException, applyToAllClasses = false, value=1L)
class Slow {
    def fib(n) { Thread.sleep(100); n<2?n:fib(n-1)+fib(n-2) }
}
def result
def t = Thread.start {
    try {
        result = new Slow().fib(50)
    } catch (TooLongException e) {
        result = -1
    }
}
t.join(5000)
assert result == -1
checkOnMethodStart true 是否应该在每个方法执行前检查中断标志,详见groovy.transform.TimedInterrupt
@TimedInterrupt(checkOnMethodStart=false)
applyToAllClasses true 是否转换应用于这个源文件中的所有类,详见groovy.transform.TimedInterrupt
@TimedInterrupt(applyToAllClasses=false)
class A { ... } // interrupt checks added
class B { ... } // no interrupt checks
applyToAllMembers true 是否转换应用于这个类的所有成员,详见groovy.transform.TimedInterrupt
class A {
    @TimedInterrupt(applyToAllMembers=false)
    void method1() { ... } // interrupt checked added
    void method2() { ... } // no interrupt checks
}

@TimedInterrupt暂时还不支持静态方法。

@groovy.transform.ConditionalInterrput
最后一个基于安全脚本的注解是支持你使用自定义策略来中断一个脚本执行,这个注解可以用于资源管理(限制调用某个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 { ... } // interrupt checks added
class B { ... } // no interrupt checks
applyToAllMembers true 是否转换应用于这个类的全部成员,详见groovy.transform.ConditionalInterrupt
class A {
    @ConditionalInterrupt(applyToAllMembers=false)
    void method1() { ... } // interrupt checked added
    void method2() { ... } // no interrupt checks
}

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的可见性规则规定了如果你创建一个不加修饰符的变量,姜维当成一个属性:

class Person {
    String name // this is a property
}

你是否只是想创建一个包私有的变量而不是一个属性(private field+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     // not a property, package protected
  Date dob        // not a property, package protected
  private int age // explicit modifier, so won't be touched
}

@PackageScope注解很少用作常规的Groovy转换但是有时候用来指定包范围可见性的工厂方法或提供测试用的构造函数非常有用。或者继承第三方库的时候要这种可见性转换也非常有用。

@groovy.transform.AnnotationCollector
@AnnotationCollector用于创建元注解,详见dedicated section的描述。
@groovy.transform.TypeChecked
@TypeChecked在你的Groovy代码中激活编译时的类型检查。详见section on type checking
@groovy.transform.CompileStatic
@CompileStatic在你的Groovy代码中激活静态编译。详见section on type checking
@groovy.transform.CompileDynamic
@CompileDynamic可以用于在你的部分代码中禁用静态编译。详见section on type checking
@groovy.lang.DelegatesTo
@DelegatesTo技术上来讲,不是一个AST转换。它主要用于文档化代码,如果你使用type checkingstatic compilation可以给提供帮助信息。这个注解在DSL章节有详细描述。

2.1.8 Swing patterns

@groovy.beans.Bindable

@Bindable是一个将常规属性变成边界属性的AST转换(参考JavaBeans规范)。@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可以移除你代码中的很多范例代码(boilerplate),增强代码的可读性。如果这个注解放在单个属性上,那么只在这个属性行起作用。

import groovy.beans.Bindable

class Person {
    String name
    @Bindable int age
}

@groovy.beans.ListenerList

@ListenerList AST转换用于在一个类中生成list的添加,删除以及获取监听器代码。仅仅需要将这个注解放在一个集合属性上。

import java.awt.event.ActionListener
import groovy.beans.ListenerList

class Component {
    @ListenerList
    List<ActionListener> listeners;
}

这个转换将基于list的泛型指定会生成恰当的add/remove方法。除此之外还会基于这个类声明的公共方法生成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 泛型名 默认情况下,将会在一个有泛型的简单类的add/remove方法前添加前缀
class Component {
    @ListenerList(name='item')
    List<ActionListener> listeners;
}
synchronize false 如果为true,将会同步方法
class Component {
    @ListenerList(synchronize = true)
    List<ActionListener> listeners;
}

@groovy.beansVetoable
@Vetoable注解和@Bindable的工作方式相似。但是生成约束属性(constrained property)而不是边界属性。这个注解可以用于类,意味着这个类的属性将都会转换为约束属性,也可以用于单个属性,示例如下:

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
    }
}

如果这个注解放在单个属性上,仅仅将一个属性职位vetoable:

import groovy.beans.Vetoable

class Person {
    String name
    @Vetoable int age
}

2.1.9 Test assistance

@NotYetImplemented用来反转Junit3/4测试用例的结果。如果你需要测试还没有实现的功能特性这个注解是相当有用的。在这种情况下,期望的测试结果是失败,注明@NotYetImplemented将会反转测试结果,示例如下:

import groovy.transform.NotYetImplemented

class Maths {
    static int fib(int n) {
        // todo: implement later
    }
}

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带来一些其他影响将会被知道,因为测试用例通不过。
@groovy.transform.ASTTest
@ASTTest是一个特殊的注解,用于帮助调试其他AST注解或者Groovy编译器。它可以让开发者观察到AST的编译和性能问题。这意味着这个AST转换可以在代码转换为字节码之前切入代码。@ASTTest可以用在注解节点的任何地方,需要两个参数:

  • Phase:当@ASTTest触发的时候将会设置这个阶段,测试代码将会停在这个阶段的结束位置。
  • Value:一旦阶段到达,将会执行的代码

编译阶段必须设置org.codehaus.groovy.control.CompilePhase中的一个值,当然,因为不能在同一个地方设置两个相同的注解,因此不能够使用@ASTTest在同一个节点设置两个不同的编译阶段。
Value是一段闭包代码,根据注解的位置可以访问特定的变量节点。辅助类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注解指定一个节点
(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 // shouldn't be a property anymore
    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)返回被标记为锚点的AST节点
(2)因为lookup方法总是返回一个list,因此有必要选择一个来处理
想象一下,你想测试一个循环变量,你可以这样写代码:

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阶段执行。每个阶段将会保持转换的上线文,你可以用来检查两个阶段的变化。
下面的示例演示了如何dump一个类节点中注册的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)如果当前阶段是instruction
(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转换

有两种类型的转换,全局和本地转换。

  • Global transformations被编译器用于在任意转换应用时编译代码。实现了全局转换的类会在一个jar包中,同时包含一个META-INF/services/org.codehaus.groovy.transform.ASTTransformation文件,转换的类名用下划线隔开。转换类必须有一个无参构造函数并且实现了org.codehaus.groovy.transform.ASTTransformation接口。每次编译源码的时候将会运行,因此不要创建那些扫描全部AST,这将会非常耗时。
  • Local transformations用在那些你想通过转换来注解的类,因为这个特点,我们会重用注解,这些注解应该实现了org.codehaus.groovy.transform.ASTTransformation接口。编译器将会在应用这些转换的时候发现它们。

2.2.1 编译阶段指导

Groovy AST转换必须在已定义的9种编译阶段中运行(org.codehaus.groovy.control.CompilePhase)。
全局转换可能在任意阶段,但是本地转换只能在语法分析阶段或后面的阶段。简单地说,编译器阶段有:

  • 初始阶段(Initialization):源文件以及环境配置文件
  • 编码阶段(Parsing):根据语法将源文件编码为节点树
  • 转换阶段(Conversion):从节点树种转换出抽象语法树
  • 语法分析阶段(Semantic Analysis):一致性和可用性验证,解析类
  • 规范化阶段(Canonicalization):完成抽象语法树的构建
  • 指令选择阶段(Instruction Selection):指令集的选择,比如Java6或Java7的二进制
  • 类生成阶段(Class Generation):在内存中生成类的二进制字节码
  • 输出阶段(Output):将二进制输出到文件系统中
  • 终结阶段(Finalization):最后一个清理阶段

通常来说,在晚期阶段有很多类型信息可以用。如果你的转换关注的是读AST,在晚期阶段获得更多的信息是一个不错的选择,如果你的转换关注的是写AST,早期阶段可能更加方便。

2.2.2 本地转换

本地转换和它们应用的上下文是相关的。多数情况下,上下文由一个注解定义,同样也定义了转换的作用范围。举个例子,注解一个字段意味着转换应用于这个字段,注解一个类的话意味着转换应用于这个类。

一个简单的例子,假如想写一个@WithLogging转换,可以在一个方法被调用前后打印信息。下面的"Hello World"示例将打印”Hello World”的开始和结束信息。

一般人写的面向切面的程序:

@WithLogging
def greet() {
    println "Hello World"
}

greet()

一个本地转换很容易就能做这个功能,它需要两个东西:

一个ASTTransformation是一个可以访问org.codehaus.groovy.control.SourceUnit的回调,通过它你可以拿到一个org.codehaus.groovy.ast.ModuleNode的引用。
AST(抽象语法树)是一个树结构,和org.codehaus.groovy.ast.expr.Expression(expressions)以及org.codehaus.groovy.ast.expr.Statement(statements)类似。一个学习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,这个注解应用于方法上。
这里最重要的部分是@GroovyASTTransformationClass注解。它将@WithLogging注解链接到ASTTransformation类。gep.WithLoggingASTTransformation是我们即将要写的ASTTransformation注解的全称。这一行绑定注解到转换上。
在这个地方,在源码单元每发现一个@WithLogging注解,Groovy编译器就会调用一次gep.WithLoggingASTTransformation。在LoggingASTTransformation里设置的断点将会在IDE运行的时候停止。
ASTTransformation有一点复杂。这里的示例非常简单:

@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)                 //(10)                   

    }

    private static Statement createPrintlnAst(String message) {  //(11)         
        new ExpressionStatement(
            new MethodCallExpression(
                new VariableExpression("this"),
                new ConstantExpression("println"),
                new ArgumentListExpression(
                    new ConstantExpression(message)
                )
            )
        )
    }
}

(1)尽管这不是必须的,但是你写一个AST转换,强烈建议你使用CompileStatic,因为它可以提高编译器性能
(2)注明org.codehaus.groovy.transform.GroovyASTTransformation明确哪个编译阶段需要运行转换,这里是语法分析阶段。
(3)实现ASTTransformation转换
(4)仅仅有一个visit方法
(5)nodes参数是一个2个AST节点的数组,第一个是注解节点(@WithLogging)第二个也是注解节点(the method node)
(6)创建一个语句在进入方法的时候打印
(7)创建一个语句在结束方法的时候打印
(8)得到方法体,这里是BlockStatement
(9)在第一条语句退出前添加进入方法的信息
(10)在最后一条语句退出前追加退出方法的信息
(11)创建一个ExpressionStatement,使用MethodCallExpression包裹

注意到这个例子的简洁性是非常重要的,我们没有做必要的检查,比如检查注解节点是否是一个真正的MethodNode,以及方法体是BlockStatement的实例。这个练习留给读者补充。
注意到在createPrintlnAst(String)方法里创建一个新的打印语句。创建代码的AST并不总是很简单。在这个例子里,我们需要构造一个新的方法调用,传递参数值,方法名字和一个参数列表。当我们创建AST的时候,先将代码写到一个Groovy文件,然后在调试模式下将AST代码写进去将对我们学习创建AST非常有帮助。通过调试模式,我们可以写出createPrintlnAst这样的功能。

@WithLogging
def greet() {
    println "Hello World"
}

greet()

产生

Starting greet
Hello World
Ending greet

AST转换直接参与到编译的过程中,理解到这一点是非常重要的。初学者犯的一个常见错误是将AST转换代码放到同一个源码树来使用转换。通常,在同一个源码树意味着它们将在同一个时间编译。因为转换自身也会编译进编译阶段,每个编译阶段都会处理全部源文件才能进入下一个阶段,这里有一个直接冲突:在类还没有使用前转换不会编译,AST转换需要你使用它们才能编译。通常,将它们和源码树分开始比较容易的做法。

2.2.3 全局转换

全局转换和本地转换有些类似除了一个主要的不同点:它们不需要注解,意味着它们应用于全局。也就是说每个类都会编译进去。因此限制过多依赖它们是非常重要的,因为它们会对编译性能有明显的影响。
下面是一个local AST transformation的例子,假设我们需要跟踪全部方法,不仅仅是被注解为@WithLogging的方法。通常,我们需要在每个方法添加一个@WithLogging:

def greet() {
    println "Hello World"
}

greet()

要使这个生效,需要两步:
1.在META-INF/services创建一个org.codehaus.groovy.transform.ASTTransformation描述
2.创建一个ASTTransformation的实现
描述文件必须在Classpath路径下能找到,通常将会包含一个单行:

META-INF/services/org.codehaus.groovy.transform.ASTTransformation
gep.WithLoggingASTTransformation

转换的代码看起来像本地转换的例子,但是不是使用ASTNode[]参数,我们需要使用SourceUnit来替代。
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)  //(10)                     
            existingStatements.add(endMessage)         //(11)                      
        }
    }

    private static Statement createPrintlnAst(String message) {     //(12)          
        new ExpressionStatement(
            new MethodCallExpression(
                new VariableExpression("this"),
                new ConstantExpression("println"),
                new ArgumentListExpression(
                    new ConstantExpression(message)
                )
            )
        )
    }
}

(1)尽管不是必须的,如果你在Groovy里写转换,强烈推荐你使用CompileStatic,因为可以提高编译器性能
(2)使用org.codehaus.groovy.transform.GroovyASTTransformation注解来标识转换运行时的编译阶段,这里是语法分析阶段
(3)实现ASTTransformation接口
(4)只有单个visit方法
(5)sourceUnit参数使得编译的时候可以访问源码,因此我们拿到AST当前的源码并且返回这个文件的参数类表
(6)遍历源文件的每个方法
(7)当进入方法的时候创建打印信息语句
(8)当退出方法的时候创建打印信息语句
(9)得到方法体,这里是BlockStatement
(10)在退出代码的第一条语句上增加进入方法信息
(11)在退出代码的最后一条语句上追加退出方法信息
(12)创建一个ExpressionStatement包括MethodCallExpression,配合this.println(“message”)

2.2.4AST API指导

AbstractASTTransformation

你已经知道你可以直接实现ASTTransformation接口了,大多数情况是你不必这样做而是继承org.codehaus.groovy.transform.AbstractASTTransformation类。这个类提供了几个工具方法使得AST转换实现更加简单。Groovy里几乎所有的AST转换都继承了这个类。

ClassCodeExpressionTransformer

常见的一种用法是转换一个表达式到另外一种表达式。Groovy提供了一种类是这个变得很简单:org.codehaus.groovy.ast.ClassCodeExpressionTransformer
为了演示这个,让我们创建一个@Shout转换,它将转换方法里所有的String常量到大写的形式。示例如下:

@Shout
def greet() {
    println "Hello World"
}

greet()

应该打印的是:
HELLO WORLD
代码的转换可以使用ClassCodeExpressionTransformer来使这个变得更加容易:

@CompileStatic
@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS)
class ShoutASTTransformation implements ASTTransformation {
//(1)
    @Override
    void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
        ClassCodeExpressionTransformer trn = new ClassCodeExpressionTransformer() {         
            private boolean inArgList = false
            @Override
            protected SourceUnit getSourceUnit() {
                sourceUnit         //(2)                                                         
            }

            @Override
            Expression transform(final Expression exp) {
                if (exp instanceof ArgumentListExpression) {
                    inArgList = true
                } else if (inArgList &&
                    exp instanceof ConstantExpression && exp.value instanceof String) {
                    return new ConstantExpression(exp.value.toUpperCase())         //(3)         
                } 
                def trn = super.transform(exp)
                inArgList = false
                trn
            }
        }
        trn.visitMethod((MethodNode)nodes[1])        //(4)                                       
    }
}

(1)内部转换创建一个ClassCodeExpressionTransformer
(2)转换需要返回源码单元
(3)如果参数列表中检查到一个String类型的表达式,转换将其变成大写形式
(4)在注解方法中调用转换

AST Nodes

写一个AST转换需要对Groovy内部API有着非常深入的理解。尤其需要掌握AST类知识。因为这些类是内部的,API未来也有可能会变化,因此它们也有可能会变化。意味着你的转换可能会打断,有告警。AST已经稳定相当长一段时间了。这种事情也比较少发生。
AST的类属于org.codehaus.groovy.ast这个包。推荐读者使用Groovy Console,是一个AST浏览工具。可以通过它看到这些类。当然一种好的资源是学习AST Builder测试套件。

2.2.5测试AST转换

Separating source trees
这节是关于测试AST转换的一个好的实践。前面的章节主要在于如何执行AST转换,必须预编译。这听起来很自然但是大多数人却不知道,试图将AST转换用在相同的源码树上。
测试AST转换的第一条提示是将测试源码和转换源码分开。这是一条非常好的实践,但是你必须确认你的构建和编译是分开的。Apache MavenGradle默认是这样的。

调试AST转换
在AST转换中设置断点是非常有帮助的,这样你可以在你的IDE中调试代码。当然,你可能会奇怪你的IDE并没有停在断点处。原因很简单:如果你的IDE使用Groovy编译器来编译AST转换的单元测试,编译将有IDE触发,但是编译文件的进程并没有调试选项。只有当测试用例执行的时候调试选项才能在虚拟机中生效。这时候已经太晚了,类早已经编译好了。你的转换也应用上了。

一个非常简单的方式是使用 GroovyTestCase类,它提供了一个assertScript方法。这意味着你不必写这样的测试用例:

static class Subject {
    @MyTransformToDebug
    void methodToBeTested() {}
}

void testMyTransform() {
    def c = new Subject()
    c.methodToBeTested()
}
void testMyTransformWithBreakpoint() {
    assertScript '''
        import metaprogramming.MyTransformToDebug

        class Subject {
            @MyTransformToDebug
            void methodToBeTested() {}
        }
        def c = new Subject()
        c.methodToBeTested()
    '''
}

不同点是当你使用assertScript的时候,assertScript块里的代码在单元测试执行的时候才编译。也就是说这个时候,Subject类将会激活调试模式,断点将生效。

ASTTest

最后但同样重要的是,测试一个AST转换同样也将测试AST编译时的状态。Groovy提供了一个@ASTTest工具:这个注解可以让你添加断言在你的AST里。详细可以参考ASTTest文档。

2.2.6其他

如果你想要一步一步学习AST转换,可以学习这个文档

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/117929.html

(0)
上一篇 2021年8月28日 04:19
下一篇 2021年8月28日

相关推荐

发表回复

登录后才能评论