Groovy元编程

参考官方文档:http://www.groovy-lang.org/metaprogramming.html

运行时及编译时元编程

Groovy 支持两种元编程:运行时元编程和编译时元编程。第一种方式允许在运行时改变类模式和程序行为,第二种方式则只发生在编译时。两种方式都有一定的优缺点,下面就来详细介绍一下它们。

1. 运行时元编程

运行时元编程,可以将一些决策(诸如解析、注入甚至合成类和接口的方法)推迟到运行时来完成。为了深入了解 Groovy 的 MOP,我们需要理解 Groovy 的对象以及 Groovy 处理方法。在 Groovy 中,我们主要与三类对象打交道:POJO、POGO,还有 Groovy 拦截器。Groovy 的元编程支持所有类型的对象,但是它们采用的方式却各不相同。

  • POJO —— 普通的 Java 对象,它的类可以用 Java 或其他任何 JVM 上的语言来编写。
  • POGO —— Groovy 对象,它的类使用 Groovy 编写而成,继承自 java.lang.Object 且默认实现了 groovy.lang.GroovyObject 接口。
  • Groovy 拦截器 —— 实现了 groovy.lang.GroovyInterceptable 接口的 Groovy 对象,并具有方法拦截功能。稍后将在 GroovyInterceptable 一节中详细介绍。

每当调用一个方法时,Groovy 会判断该方法是 POJO 还是 POGO。对于 POJO 对象,Groovy 会从 groovy.lang.MetaClassRegistry 读取它的 MetaClass,并委托方法调用;对于 POGO 对象,Groovy 将要采取更多的执行步骤,如下图所示:

Groovy拦截机制

图 1 Groovy 拦截机制

1.1 GroovyObject 接口

groovy.lang.GroovyObject 是 Groovy 中的关键接口,地位类似于 Java 中的 Object 类。在 groovy.lang.GroovyObjectSupport 类中有一个 GroovyObject 的默认实现,负责将调用传输给 groovy.lang.MetaClass 对象。GroovyObject 源看起来如下所示:

package groovy.lang;

public interface GroovyObject {

    Object invokeMethod(String name, Object args);

    Object getProperty(String propertyName);

    void setProperty(String propertyName, Object newValue);

    MetaClass getMetaClass();

    void setMetaClass(MetaClass metaClass);
}
1.1.1 invokeMethod

根据运行时元编程的Schema,当你所调用的方法没有在 Groovy 对象中提供的时候,调用该方法。下面这个例子中,使用了一个重写的 invokeMethod() 方法:

class SomeGroovyClass {

    def invokeMethod(String name, Object args) {
        return "called invokeMethod $name $args"
    }

    def test() {
        return 'method exists'
    }
}

def someGroovyClass = new SomeGroovyClass()

assert someGroovyClass.test() == 'method exists'
assert someGroovyClass.someMethod() == 'called invokeMethod someMethod []'
1.1.2 getProperty 与 setProperty

每次对属性的读取都可以通过重写当前对象的 getProperty() 来拦截,下面是一个简单的例子:


class SomeGroovyClass {

    def property1 = 'ha'
    def field2 = 'ho'
    def field4 = 'hu'

    def getField1() {
        return 'getHa'
    }

    def getProperty(String name) {
        if (name != 'field3')
            return metaClass.getProperty(this, name)     // 1⃣️   
        else
            return 'field3'
    }
}

def someGroovyClass = new SomeGroovyClass()

assert someGroovyClass.field1 == 'getHa'   
assert someGroovyClass.field2 == 'ho'  
assert someGroovyClass.field3 == 'field3'  
assert someGroovyClass.field4 == 'hu'   
1.1.3 getMetaClass 和 setMetaClass

可以访问一个对象 metaClass ,或者自定义 MetaClass 实现来改变默认的拦截机制。比如,你可以自己编写 MetaClass接口的实现,并将它赋予对象,从而改变拦截机制。

// getMetaclass
someObject.metaClass

// setMetaClass
someObject.metaClass = new OwnMetaClassImplementation()

你可以在下文的 GroovyInterceptable 主题中看到更多的范例。

1.2 get/setAttribute

该功能与 MetaClass 实现有关。在默认的实现中,可以不用调用 getter 与 setter 而访问字段。下列例子就反映了这种方法。

class SomeGroovyClass {

    def field1 = 'ha'
    def field2 = 'ho'

    def getField1() {
        return 'getHa'
    }
}

def someGroovyClass = new SomeGroovyClass()

assert someGroovyClass.metaClass.getAttribute(someGroovyClass, 'field1') == 'ha'
assert someGroovyClass.metaClass.getAttribute(someGroovyClass, 'field2') == 'ho'
class POGO {

    private String field
    String property1

    void setProperty1(String property1) {
        this.property1 = "setProperty1"
    }
}

def pogo = new POGO()
pogo.metaClass.setAttribute(pogo, 'field', 'ha')
pogo.metaClass.setAttribute(pogo, 'property1', 'ho')

assert pogo.field == 'ha'
assert pogo.property1 == 'ho'

1.3 methodMissing

Groovy 支持 methodMissing 这一概念。该方法与 invokeMethod 的不同之处在于:只有当方法分派失败,找不到指定名称或带有指定实参的方法时,才会调用该方法。

class Foo {

   def methodMissing(String name, def args) {
        return "this is me"
   }
}

assert new Foo().someUnknownMethod(42l) == 'this is me'

通常,在使用 methodMissing 时,可能会将结果缓存起来,以备下次调用同样方法时使用。

比如像下面这样在 GORM 类中的动态查找器。它们是根据 methodMissing 来实现的:

class GORM {

   def dynamicMethods = [...] // 一些利用正则表达式的动态方法  

   def methodMissing(String name, args) {
       def method = dynamicMethods.find { it.match(name) }
       if(method) {
          GORM.metaClass."$name" = { Object[] varArgs ->
             method.invoke(delegate, name, varArgs)
          }
          return method.invoke(delegate,name, args)
       }
       else throw new MissingMethodException(name, delegate, args)
   }
}

注意,假如找到一个调用的方法,就会立刻使用 ExpandoMetaClass 动态地注册一个新方法。这样当下次调用同一方法时就会更方便。使用 methodMissing,并不会产生像调用 invokeMethod 那么大的开销,第二次调用代价也并不昂贵。

1.4 propertyMissing

Groovy 支持 propertyMissing 的概念,用来拦截失败的属性解析尝试。对 getter 方法而言,propertyMissing 接受一个包含属性名的 String 参数:

class Foo {
   def propertyMissing(String name) { name }
}

assert new Foo().boo == 'boo'

当 Groovy 运行时无法找到指定属性的 getter 方法时,才会调用 propertyMissing(String) 方法。

对于 setter 方法,可以添加第二个 propertyMissing 定义来接收一个附加值参数。

class Foo {
   def storage = [:]
   def propertyMissing(String name, value) { storage[name] = value }
   def propertyMissing(String name) { storage[name] }
}

def f = new Foo()
f.foo = "bar"

assert f.foo == "bar"

对于 methodMissing 来说,最佳实践应该是在运行时动态注册新属性,从而改善总体的查找性能。

另外,处理静态方法和属性的 methodMissing 和 propertyMissing 方法可以通过 ExpandoMetaClass 来添加。

1.5 GroovyInterceptable

groovy.lang.GroovyInterceptable 接口是一种标记接口,继承自超接口 GroovyObject,用于通知 Groovy 运行时通过方法分派器机制时应拦截的方法。

package groovy.lang;

public interface GroovyInterceptable extends GroovyObject {
}

当 Groovy 对象实现了 GroovyInterceptable 接口时,它的 invokeMethod() 方法就会在任何方法调用时调用。

下面就列举一个这种类型的方法:

class Interception implements GroovyInterceptable {

    def definedMethod() { }

    def invokeMethod(String name, Object args) {
        'invokedMethod'
    }
}

下面这段代码测试显示,无论方法是否存在,调用方法都将返回同样的值。

class InterceptableTest extends GroovyTestCase {

    void testCheckInterception() {
        def interception = new Interception()

        assert interception.definedMethod() == 'invokedMethod'
        assert interception.someMethod() == 'invokedMethod'
    }
}

我们不能使用默认的 Groovy 方法(比如 println),因为这些方法已经被注入到了 Groovy 所有的对象中,自然会被拦截。

如果想要拦截所有的方法调用,但又不想实现 GroovyInterceptable 这个接口,那么我们可以在一个对象的 MetaClass上实现 invokeMethod()。该方法同时适于 POGO 与 POJO 对象,如下所示:

class InterceptionThroughMetaClassTest extends GroovyTestCase {

    void testPOJOMetaClassInterception() {
        String invoking = 'ha'
        invoking.metaClass.invokeMethod = { String name, Object args ->
            'invoked'
        }

        assert invoking.length() == 'invoked'
        assert invoking.someMethod() == 'invoked'
    }

    void testPOGOMetaClassInterception() {
        Entity entity = new Entity('Hello')
        entity.metaClass.invokeMethod = { String name, Object args ->
            'invoked'
        }

        assert entity.build(new Object()) == 'invoked'
        assert entity.someMethod() == 'invoked'
    }
}

参看MetaClasses 一节内容了解 MetaClass 的更多内容。

1.6 类别(Categories)

如果一个不受控制的类有额外的方法,在某些情况下反而是有用的。为了实现这种功能,Groovy 从 Objective-C 那里借用并实现了一个概念,叫做:类别Categories)。

类别功能是利用类别类category classes)来实现的。类别类的特殊之处在于,需要遵循特定的预定义规则才能定义扩展方法。

系统已经包括了一些类别,可以为类添加相应功能,从而使它们在 Groovy 环境中更为实用。

类别类默认是不能启用的。要想使用定义在类别类中的方法,必须要使用 GDK 所提供的 use 范围方法,并且可用于每一个 Groovy 对象实例内部。

use(TimeCategory)  {
    println 1.minute.from.now   //1⃣️       
    println 10.hours.ago

    def someDate = new Date()    //2⃣️  
    println someDate - 3.months
}

1⃣️ TimeCategory 为 Integer 添加了方法
2⃣️ TimeCategory 为 Date 添加了方法

use 方法将类别类作为第一个形式参数,将一个闭包代码段作为第二个形式参数。在 Category 中,可以访问类别的任何方法。如上述代码所示,甚至 JDK 的 java.lang.Integer 或 java.util.Date 类都可以通过用户定义方法来丰富与增强。

类别不需要直接暴露给用户代码,如下所示:

class JPACategory{
  // 下面让我们无需通过 JSR 委员会的支持来增强JPA EntityManager   
  static void persistAll(EntityManager em , Object[] entities) { //添加一个接口保存所有   
    entities?.each { em.persist(it) }
  }
}

def transactionContext = {
  EntityManager em, Closure c ->
  def tx = em.transaction
  try {
    tx.begin()
    use(JPACategory) {
      c()
    }
    tx.commit()
  } catch (e) {
    tx.rollback()
  } finally {
    //清除所有资源   
  }
}

// 用户代码。他们经常会在出现异常时忘记关闭资源,有些甚至会忘记提交,所以不能指望他们。
EntityManager em; //probably injected
transactionContext (em) {
 em.persistAll(obj1, obj2, obj3)
 // 在这里制定一些逻辑代码,使范例更合理。  
 em.persistAll(obj2, obj4, obj6)
}

通过查看 groovy.time.TimeCategory 类,我们就会发现,扩展方法都声明为 static 方法。实际上,要想使类别类的方法能成功地添加到 use 代码段内的类中,这是类别类必须满足的条件之一。

public class TimeCategory {

    public static Date plus(final Date date, final BaseDuration duration) {
        return duration.plus(date);
    }

    public static Date minus(final Date date, final BaseDuration duration) {
        final Calendar cal = Calendar.getInstance();

        cal.setTime(date);
        cal.add(Calendar.YEAR, -duration.getYears());
        cal.add(Calendar.MONTH, -duration.getMonths());
        cal.add(Calendar.DAY_OF_YEAR, -duration.getDays());
        cal.add(Calendar.HOUR_OF_DAY, -duration.getHours());
        cal.add(Calendar.MINUTE, -duration.getMinutes());
        cal.add(Calendar.SECOND, -duration.getSeconds());
        cal.add(Calendar.MILLISECOND, -duration.getMillis());

        return cal.getTime();
    }

    // ...

另外一个必备条件是静态方法的第一个实参必须定义方法一旦被启用时,该方法所连接的类型;而另一个实参则是常见的方法用于形参的实参。

由于形参和静态方法的规范,类别方法定义可能会比普通方法定义稍微不太直观。因此,作为替代方案,Groovy 引入了 @Category 标记,利用这一标记,可在编译时将标注的类转化为类别类。

class Distance {
    def number
    String toString() { "${number}m" }
}

@Category(Number)
class NumberCategory {
    Distance getMeters() {
        new Distance(number: this)
    }
}

use (NumberCategory)  {
    assert 42.meters.toString() == '42m'
}

使用 @Category 标记的优点在于,在使用实例方法时,可以不需要把目标类别当做第一个形参。目标类别类作为实参提供给标记使用。

关于 @Category 的另外介绍,可参看 编译时元编程

1.7 MetaClasses

(待定)

1.7.1 自定义 metaclass 类

(待定)

授权 metaclass

(待定)

魔法包(Magic package)

(待定)

1.7.2 每个实例的 metaclass

(待定)

1.7.3 ExpandoMetaClass

Groovy 提供了一种叫做 ExpandoMetaClass 的特殊 MetaClass。其特殊之处在于,它允许可以使用灵活的闭包语法来动态添加或改变方法、构造函数、属性,甚至静态方法。

对于测试向导中所展示的模拟或存根情况,使用这些修改会特别有用。

每一个 Groovy 所提供的 java.lang.Class 都带有一个特殊的 metaClass 属性,它将提供一个 ExpandoMetaClass 实例的引用。该实例可用于添加方法或改变已有方法的行为。

默认情况下,ExpandoMetaClass 不支持继承。为了启用继承,必须在应用程序开始运作前(比如在 main 方法或 servlet bootstrap 中)就调用 ExpandoMetaClass#enableGlobally()

下面这些内容详细介绍了 ExpandoMetaClass 在不同情况下的应用。

方法

一旦通过调用 metaClass 属性访问了 ExpandoMetaClass,就可以通过左移(<<)或等于号(=)操作符来添加方法。

注意,左移操作符是用于追加append)一个新的方法。如果方法已经存在,则会抛出一个异常。如果需要替代replace)一个方法,则需要使用 = 操作符。

下例展示了操作符是如何应用于 metaClass 的一个不存在的属性上,从而传入 Closure 代码块的一个实例的。

class Book {
   String title
}

Book.metaClass.titleInUpperCase << {-> title.toUpperCase() }

def b = new Book(title:"The Stand")

assert "THE STAND" == b.titleInUpperCase()

上例显示,通过访问 metaClass 属性,可将一个新方法添加到一个类上,并可使用 << 或 = 操作符来指定一个 Closure 代码块。Closure 形参被解析为方法形参。形参方法可以通过 {→ …​} 格式来添加。

属性

ExpandoMetaClass 支持两种方式来添加或重写属性。

首先,只需通过为 metaClass 赋予一个值,就可以声明一个可变属性mutable property):

class Book {
   String title
}

Book.metaClass.author = "Stephen King"
def b = new Book()

assert "Stephen King" == b.author

另一个方式是,通过使用添加实例方法的标准机制来添加 getter 和(或)setter 方法:

class Book {
  String title
}
Book.metaClass.getAuthor << {-> "Stephen King" }

def b = new Book()

assert "Stephen King" == b.author

在上述源代码实例中,属性由闭包所指定,并且是一个只读属性。添加一个相等的 setter 方法也是可行的,但属性值需要存储起来以备后续使用。这种做法可以参照下面的例子:

class Book {
  String title
}

def properties = Collections.synchronizedMap([:])

Book.metaClass.setAuthor = { String value ->
   properties[System.identityHashCode(delegate) + "author"] = value
}
Book.metaClass.getAuthor = {->
   properties[System.identityHashCode(delegate) + "author"]
}

但这并不是唯一的办法。比如在一个 servlet 容器中,将当前执行请求中的值当作请求属性保存起来(就像 Grails 中的某些情况一样)。

构造函数

构造函数可以通过特殊的 constructor 属性来添加。<< 或 = 操作符都可以用于指定 Closure 代码段。当代码在运行时执行时,Closure 实参会成为构造函数的实参。

class Book {
    String title
}
Book.metaClass.constructor << { String title -> new Book(title:title) }

def book = new Book('Groovy in Action - 2nd Edition')
assert book.title == 'Groovy in Action - 2nd Edition'

但在添加构造函数时要格外注意,因为这极易造成栈溢出。

静态方法

添加静态方法的方法与添加实例方法基本一样,只不过要在方法名前加上 static 修饰符。

class Book {
   String title
}

Book.metaClass.static.create << { String title -> new Book(title:title) }

def b = Book.create("The Stand")

借用方法

利用 ExpandoMetaClass,可以使用 Groovy 方法点标记法从其他类中借用方法。

class Person {
    String name
}
class MortgageLender {
   def borrowMoney() {
      "buy house"
   }
}

def lender = new MortgageLender()

Person.metaClass.buyHouse = lender.&borrowMoney

def p = new Person()

assert "buy house" == p.buyHouse()

动态方法名

在 Groovy 中,既然可以使用字符串作为属性名,那么反过来,也可以在运行时动态创建方法与属性名。要想创建具有动态名称的方法,只需使用将字符串引用为属性名的语言特性。

class Person {
   String name = "Fred"
}

def methodName = "Bob"

Person.metaClass."changeNameTo${methodName}" = {-> delegate.name = "Bob" }

def p = new Person()

assert "Fred" == p.name

p.changeNameToBob()

assert "Bob" == p.name

同样的概念可以应用于静态方法与属性。

Grails Web 应用框架可以算是动态方法名的一个应用范例。“动态编解码器”的概念正是通过动态方法名来实现的。

HTMLCodec 类

class HTMLCodec {
    static encode = { theTarget ->
        HtmlUtils.htmlEscape(theTarget.toString())
    }

    static decode = { theTarget ->
        HtmlUtils.htmlUnescape(theTarget.toString())
    }
}

上例实现了一个编解码器。Grails 提供了多种编解码器实现,每种实现都定义在一个类中。在运行时,会在应用类路径上出现多个编解码器类。在应用启动时,框架会将 encodeXXX 和 decodeXXX 方法添加到特定的元类中,这里的 XXX 是指编解码器类名的前面部分(如 encodeHTML)。下面采用了一些 Groovy 伪码来表示这种机制:

def codecs = classes.findAll { it.name.endsWith('Codec') }

codecs.each { codec ->
    Object.metaClass."encodeAs${codec.name-'Codec'}" = { codec.newInstance().encode(delegate) }
    Object.metaClass."decodeFrom${codec.name-'Codec'}" = { codec.newInstance().decode(delegate) }
}

def html = '<html><body>hello</body></html>'

assert '<html><body>hello</body></html>' == html.encodeAsHTML()

运行时发现

在运行时阶段执行某个方法时,还有其他什么方法或属性存在?这个问题往往是非常有用的。ExpandoMetaClass 提供了下列方法(截止目前):

  • getMetaMethod
  • hasMetaMethod
  • getMetaProperty
  • hasMetaProperty

为什么不能单纯使用反射呢?因为 Groovy 的独特性——它包含两种方法,一种是“真正”的方法,而另一种则是只在运行时才能获取并使用的方法。后者有时(但也并不总是被)称为元方法(MetaMethods)。元方法告诉我们在运行时究竟能够使用何种方法,从而使代码能够适应。

这一点特别适用于重写 invokeMethodgetProperty 和/或 setProperty 时。

GroovyObject 方法

ExpandoMetaClass 的另一个特性是能够允许重写 invokeMethodgetProperty 和 setPropertygroovy.lang.GroovyObject 类中能找到这三个方法。

下面范例展示了如何重写 invokeMethod

class Stuff {
   def invokeMe() { "foo" }
}

Stuff.metaClass.invokeMethod = { String name, args ->
   def metaMethod = Stuff.metaClass.getMetaMethod(name, args)
   def result
   if(metaMethod) result = metaMethod.invoke(delegate,args)
   else {
      result = "bar"
   }
   result
}

def stf = new Stuff()

assert "foo" == stf.invokeMe()
assert "bar" == stf.doStuff()

重写静态方法的逻辑跟之前我们见过的重写实例方法的逻辑基本相同,唯一不同之处在于对 metaClass.static 属性的访问,以及为了获取静态 MetaMethod 实例而对 getStaticMethodName 的调用。

重写静态 invokeMethod

ExpandoMetaClass 甚至可以允许利用一种特殊的 invokeMethod 格式重写静态方法。

class Stuff {
   static invokeMe() { "foo" }
}

Stuff.metaClass.'static'.invokeMethod = { String name, args ->
   def metaMethod = Stuff.metaClass.getStaticMetaMethod(name, args)
   def result
   if(metaMethod) result = metaMethod.invoke(delegate,args)
   else {
      result = "bar"
   }
   result
}

assert "foo" == Stuff.invokeMe()
assert "bar" == Stuff.doStuff()

扩展接口

可以利用 ExpandoMetaClass 为接口添加方法,但要想这样做,必须在应用启动前使用 ExpandoMetaClass.enableGlobally() 方法实施全局启用。

List.metaClass.sizeDoubled = {-> delegate.size() * 2 }

def list = []

list << 1
list << 2

assert 4 == list.sizeDoubled()  

1.8 扩展模块

1.8.1 扩展现有类

利用扩展模块,可以为现有类添加新方法,这些类中可以包括 JDK 中那样的预编译类。这些新方法与通过元类或类别定义的方法不同,它们是全局可用的。比如当你编写:

标准扩展方法

def file = new File(...)
def contents = file.getText('utf-8')

File 类中并不存在 getText 方法,但 Groovy 知道它的定义是在一个特殊类中 ResourceGroovyMethods

ResourceGroovyMethods.java

public static String getText(File file, String charset) throws IOException {
 return IOGroovyMethods.getText(newReader(file, charset));
}

你可能还注意到扩展方法是在“辅助”类(定义了多种扩展方法)中通过一个静态方法来定义的。getText 的第一个实参对应着接受者,而另一个形参则对应着扩展方法的实参。因此,我们才在 File 类(因为第一个实参是 File 类型)中定义了一个名为 getText 的方法,它只传递了一个实参(String类型的编码)。

创建扩展模块的过程非常简单:

  • 如上例般编写扩展类;
  • 编写模块描述符文件。

然后,还必须让 Groovy 能找到该扩展模块,这只需将扩展模块类和描述符放入类路径即可。这意味着有以下两种方法:

  • 直接在类路径上提供类和模块描述符。
  • 将扩展模块打包为 jar 文件,便于重用。

扩展模块可以为类添加两种方法:

  • 实例方法(类实例上调用)
  • 静态方法(仅供类自身调用)
1.8.2 实例方法

为现有类添加实例方法,需要创建一个扩展类。比如想在 Integer 上加一个 maxRetries 方法,可以采取下面的方式:

MaxRetriesExtension.groovy

class MaxRetriesExtension {                                     //1⃣️                           
    static void maxRetries(Integer self, Closure code) {        //2⃣️   
        int retries = 0
        Throwable e
        while (retries<self) {
            try {
                code.call()
                break
            } catch (Throwable err) {
                e = err
                retries++
            }
        }
        if (retries==0 && e) {
            throw e
        }
    }
}

1⃣️ 扩展类
2⃣️ 静态方法的第一个实际参数对应着消息的接受者,也就是扩展实例。

然后,在已经声明了扩展类之后,你可以这样调用它:

int i=0
5.maxRetries {
    i++
}
assert i == 1
i=0
try {
    5.maxRetries {
        throw new RuntimeException("oops")
    }
} catch (RuntimeException e) {
    assert i == 5
}
1.8.3 静态方法

也可以为类添加静态方法。这种情况下,静态方法需要在自己的文件中定义。

StaticStringExtension.groovy

class StaticStringExtension {      // 1⃣️                                     
    static String greeting(String self) {   // 2⃣️                          
        'Hello, world!'
    }
}

1⃣️ 静态扩展类 2⃣️ 静态方法的第一个实参对应着将要扩展并且还未使用的类

在这种情况下,可以直接在 String 类中调用它:

assert String.greeting() == 'Hello, world!'

1.8.4 模块描述符

为了使 Groovy 能够加载扩展方法,你必须声明扩展辅助类。必须在 META-INF/services 目录中创建一个名为 org.codehaus.groovy.runtime.ExtensionModule 的文件。

org.codehaus.groovy.runtime.ExtensionModule

moduleName=Test module for specifications
moduleVersion=1.0-test
extensionClasses=support.MaxRetriesExtension
staticExtensionClasses=support.StaticStringExtension

该模块描述符需要 4 个键:

  • moduleName:模块名称
  • moduleVersion:模块版本。注意,版本号只能用于检查是否将同一个模块加载了两种不同的版本。
  • extensionClasses:实例方法的扩展辅助类列表。可以提供几个类,但要用逗号分隔它们。
  • staticExtensionClasses:静态方法的扩展辅助类列表。可以提供几个类,也要用逗号分隔它们。

注意,模块并不一定要既能定义静态辅助类,又能定义实例辅助类。你可以在一个模块中添加几个类,也可以在单一模块中扩展不同的类,甚至还可以在单一的扩展类中使用不同的类,但强烈建议将扩展方法按功能集分入不同的类。

1.8.5 扩展模块和类路径

值得注意的是,不能在代码使用已编译扩展模块的时候,你无法使用它。这意味着,要想使用扩展模块,在将要使用它的代码被编译前,它就必须以已编译类的形式出现在类路径上。这其实就是说,与扩展类同一源单位中不能出现测试类(test class),然而,测试源通常在实际中与常规源是分开的,在构建的另一个步骤中执行,所以这根本不会造成任何不良影响。

1.8.6 类型检查的兼容性

与类别不同的是,扩展模块与类型检查是兼容的:如果在类路径上存在这些模块,类检查器就会知道扩展方法,并不会说明调用的时间。它们与静态编译也是兼容的。

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 标注接受以下列表中显示的几个参数。

属性默认值描述范例
includeNamesfalse是否在生成的 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)'
includeSuperFalse超类是否应在 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))'
includeSuperPropertiesFalse超属性是否应包含在 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)'
includeFieldsFalse除了属性之外,字段是否应包括在 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)'
ignoreNullsFalse是否应显示带有 null 值的属性/字段
@ToString(ignoreNulls=true)
class Person {
    String firstName
    String lastName
}

def p = new Person(firstName: 'Jack')
assert p.toString() == 'Person(Jack)'
includePackageFalse在 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)'
cacheFalse缓存 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()
callSuperFalse在 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()
includeFieldsFalse除了属性之外,是否应将字段包含在 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)'
cacheFalse缓存 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) // 同一实例
useCanEqualTrueequals 是否应调用 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')
}
includeFieldsFalse除了属性之外,元组构造函数生成过程中应包含的字段
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()
includePropertiesTrue元组构造函数生成过程中应包括的属性
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) {
    // 因为没有包括进属性,所以将失败  
}
includeSuperFieldsFalse元组构造函数生成过程中应包括的超级类中的字段
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()
includeSuperPropertiesTrue元组构造函数生成过程中应包含的超级类中的属性
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'
callSuperFalse在对父构造函数调用中,超级属性究竟是被调用,还是被设置为属性
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'
forceFalse默认,如果构造函数已经定义,转换将不起作用。将该属性设为 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 转换支持以下几种配置选项:

属性默认值描述范例
constructorAnnotationsFalse是否在拷贝时携带构造函数的标记
@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'
parameterAnnotationsFalse在复制构造函数时,是否携带构造函数参数中的标记
@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 方法,以及根据 firstlastborn 属性自然排序的一个实现。
  • 拥有返回比较器的三个方法:comparatorByFirstcomparatorByLast 和 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链接的 settern/an/an/a有,默认是'set'
ExternalStrategy显式构建器类n/an/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

该策略并不支持注释属性 builderClassNamebuildMethodNamebuilderMethodName 和 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() 

使用 prefixincludesexcludes 及 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

如果愿意,可以使用 builderClassNamebuildMethodNamebuilderMethodNameprefixincludes 和 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 转换行为可以通过下列参数来修改:

属性默认值描述范例
interfacesTrue由字段所实现的接口是否也能由类来实现
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 // 显式地添加接口
deprecatedfalse如果为真,也会对由
@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 而失败
methodAnnotationsFalse是否要将受托类的方法的所有注释都留给请托类的方法
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
parameterAnnotationsFalse是否将受托类方法参数的所有注释都留给请托方法
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 转换非常相似,但却带有一种不可变类:自动生成的 toStringequals 与 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
}
copyWithfalse用来确定是否生成 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)
    }
}

详情查看:

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
}
includeFieldsfalse默认只克隆属性。该表示为 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
}
includeFieldsfalse默认只具体化属性。该表示为 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 支持多种选项,能够进一步自定义转换的行为:

属性默认值描述范例
thrownjava.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'''
checkOnMethodStarttrue每个方法体开始处是否插入中断检查。详情参看:groovy.transform.ThreadInterrupt
@ThreadInterrupt(checkOnMethodStart=false)
applyToAllClassestrue同一源单位(在同一源文件中)的所有类是否该应用这个转换。详情参看:groovy.transform.ThreadInterrupt
@ThreadInterrupt(applyToAllClasses=false)
class A { ... } // 添加的中断检查
class B { ... } // 没有中断检查
applyToAllMemberstrue类的所有成员是否该应用同一转换。详情参见: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 的几个选项来进行自定义:

属性默认值描述范例
valueLong.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
thrownjava.util.concurrent.TimeoutException指定如果超时后抛出的异常类型。
@TimedInterrupt(thrown=TooLongException, applyToAllClasses = false, value=1L)
class Slow {
    def fib(n) { Thread.sleep(100); n
checkOnMethodStarttrue中断检查是否应该在每个方法体开始处插入。详情参见:groovy.transform.TimedInterrupt
@TimedInterrupt(checkOnMethodStart=false)
applyToAllClassestrue同源单位内的所有类是否应该使用同一转换。详情参见:groovy.transform.TimedInterrupt
@TimedInterrupt(applyToAllClasses=false)
class A { ... } // 添加中断检查  
class B { ... } // 无中断检查
applyToAllMemberstrue转换是否应该应用于类的所有成员。详情参见: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({ ... })
thrownjava.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'
checkOnMethodStarttrue中断检查是否应该在每个方法体开始处插入。详情参见:groovy.transform.ConditionalInterrupt
@ConditionalInterrupt(checkOnMethodStart=false)
applyToAllClassestrue同源单位内(位于同一源文件)的所有类是否应该使用同一转换。详情参见:groovy.transform.ConditionalInterrupt
@ConditionalInterrupt(applyToAllClasses=false)
class A { ... } // 添加中断检查
class B { ... } // 无中断检查
applyToAllMemberstrue转换是否应该应用于类的所有成员。详情参见: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;
}
synchronizefalse如果设为 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 转换就能轻松实现,它需要两个要素:

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

实现这一点需要完成以下两步工作:

  1. 在 META-INF/services 目录中创建 org.codehaus.groovy.transform.ASTTransformation 描述符。
  2. 创建 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)                                ?
            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⃣️ sourceUnit 参数可以提供对正在编译的源的访问,因此能够获取当前源的 AST 并从该方法中获取方法列表。
6⃣️ 迭代源文件中的每个方法。
7⃣️ 在进入方法时创建一个打印消息的语句。
8⃣️ 在退出方法时创建一个打印消息的语句。
9⃣️ 获取方法体,本例中是 BlockStatement
? 在已有代码的第一条语句之前添加进入方法消息。
⑪ 在已有代码的最后一条语句之前附加退出方法消息。
⑫ 创建一个 ExpressionStatement ,用来封装一个对应于 this.println("message") 的 MethodCallExpression

2.2.4 AST 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 {

    @Override
    void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
        ClassCodeExpressionTransformer trn = new ClassCodeExpressionTransformer() {      1⃣️        
            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⃣️ 如果参数列表中发现类型字符串的常量表达式,将其转换为大写形式。
4⃣️ 在被注释的方法上调用转换器。

AST 节点

编写 AST 转换需要对 Groovy API 内部运作机制有深入了解,尤其需要了解 AST 类。因为这些类是内部的,所以 API 有可能会在将来对其进行改动,从而造成转换失败的情况。尽管有这种隐患,但 AST 还是比较稳固的,这种情况很少发生。

抽象语法树类属于 org.codehaus.groovy.ast 包。强烈建议读者使用 Groovy 控制台来了解这些类,特别是 AST 浏览器工具。利用 AST Builder 测试套件也是一个不错的学习途径。

2.2.5 测试 AST 转换

分离源树

本节内容是关于测试 AST 转换的最佳实践的。前面的几节重点讲了要想执行一个 AST 转换,就必须进行预编译。虽然听上去非常简单,但很多人还是会栽在这上头,试图在定义 AST 转换的那个源树上使用 AST 转换。

测试 AST 转换的第一要点在于,一定要将测试源和转换源分离开。再次声明,这就是最佳实践,但你一定也要确保构建能实际地分别进行编译。在 Apache Maven 和 Gradle 中这都是默认的。

调试 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 转换的教程,不妨看看这个

转载于:https://my.oschina.net/kun123/blog/854274

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值