运行时和编译时元编程

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 会采取更多步骤,如下图所示

GroovyInterceptions
图 1. Groovy 拦截机制

1.1. GroovyObject 接口

groovy.lang.GroovyObject 是 Groovy 中的主要接口,就像 Java 中的 Object 类一样。GroovyObjectgroovy.lang.GroovyObjectSupport 类中有一个默认实现,它负责将调用传递给 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

此方法主要用于与 GroovyInterceptable 接口或对象的 MetaClass 结合使用,它将拦截所有方法调用。

当调用的方法不存在于 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 []'

然而,不鼓励使用 invokeMethod 来拦截丢失的方法。在仅当方法调度失败时才拦截方法调用的情况下,请改用 methodMissing

1.1.2. get/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 将请求转发给除 field3 之外所有属性的 getter。

您可以通过重写 setProperty() 方法来拦截对属性的写入访问

class POGO {

    String property 

    void setProperty(String name, Object value) {
        this.@"$name" = 'overridden'
    }
}

def pogo = new POGO()
pogo.property = 'a'

assert pogo.property == 'overridden'

1.1.3. get/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 = [...] // an array of dynamic methods that use regex

   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 定义,它接受一个额外的 value 参数

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 一样,最佳实践是在运行时动态注册新属性以提高整体查找性能。

1.5. static methodMissing

methodMissing 方法的静态变体可以通过 ExpandoMetaClass 添加,或者可以通过 $static_methodMissing 方法在类级别实现。

class Foo {
    static def $static_methodMissing(String name, Object args) {
        return "Missing static method name is $name"
    }
}

assert Foo.bar() == 'Missing static method name is bar'

1.6. static propertyMissing

propertyMissing 方法的静态变体可以通过 ExpandoMetaClass 添加,或者可以通过 $static_propertyMissing 方法在类级别实现。

class Foo {
    static def $static_propertyMissing(String name) {
        return "Missing static property name is $name"
    }
}

assert Foo.foobar == 'Missing static property name is foobar'

1.7. GroovyInterceptable

groovy.lang.GroovyInterceptable 接口是扩展 GroovyObject 的标记接口,用于通知 Groovy 运行时所有方法都应通过 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'
    }
}
我们不能使用像 println 这样的默认 groovy 方法,因为这些方法被注入到所有 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'
    }
}
有关 MetaClass 的更多信息,请参阅 元类 部分。

1.8. 类别

在某些情况下,如果一个不受控制的类有额外的方法,那将很有用。为了实现此功能,Groovy 实现了一个从 Objective-C 借鉴的功能,称为 Categories

类别是使用所谓的 category classes 实现的。类别类是特殊的,因为它需要满足某些预定义规则才能定义扩展方法。

系统中包含了一些类别,用于向类添加功能,使其在 Groovy 环境中更易于使用

类别类默认不启用。要使用类别类中定义的方法,需要应用由 GDK 提供并可从每个 Groovy 对象实例内部获得的范围 use 方法

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

    def someDate = new Date()       (2)
    println someDate - 3.months
}
1 TimeCategoryInteger 添加方法
2 TimeCategoryDate 添加方法

use 方法将类别类作为第一个参数,将闭包代码块作为第二个参数。在 Closure 内部,可以访问类别方法。如上例所示,即使是 JDK 类(如 java.lang.Integerjava.util.Date)也可以通过用户定义的方法进行丰富。

类别不需要直接暴露给用户代码,以下也可以

class JPACategory{
  // Let's enhance JPA EntityManager without getting into the JSR committee
  static void persistAll(EntityManager em , Object[] entities) { //add an interface to save all
    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 {
    //cleanup your resource here
  }
}

// user code, they always forget to close resource in exception, some even forget to commit, let's not rely on them.
EntityManager em; //probably injected
transactionContext (em) {
 em.persistAll(obj1, obj2, obj3)
 // let's do some logics here to make the example sensible
 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.9. 元类

如前所述,元类在方法解析中起着核心作用。对于来自 groovy 代码的每个方法调用,Groovy 将为给定对象找到 MetaClass 并通过 groovy.lang.MetaClass#invokeMethod(java.lang.Class,java.lang.Object,java.lang.String,java.lang.Object,boolean,boolean) 将方法解析委托给元类,这不应与 groovy.lang.GroovyObject#invokeMethod(java.lang.String,java.lang.Object) 混淆,后者是元类可能最终调用的方法。

1.9.1. 默认元类 MetaClassImpl

默认情况下,对象获取一个 MetaClassImpl 实例,该实例实现默认方法查找。此方法查找包括在对象类中查找方法(“常规”方法),但如果找不到方法,它将调用 methodMissing 并最终调用 groovy.lang.GroovyObject#invokeMethod(java.lang.String,java.lang.Object)

class Foo {}

def f = new Foo()

assert f.metaClass =~ /MetaClassImpl/

1.9.2. 自定义元类

您可以更改任何对象或类的元类,并将其替换为 MetaClass groovy.lang.MetaClass 的自定义实现。通常,您会希望扩展现有的元类之一,例如 MetaClassImplDelegatingMetaClassExpandoMetaClassProxyMetaClass;否则您将需要实现完整的方法查找逻辑。在使用新的元类实例之前,您应该调用 groovy.lang.MetaClass#initialize(),否则元类可能不会按预期运行。

委托元类

如果您只需要装饰现有的元类,DelegatingMetaClass 简化了该用例。旧的元类实现仍然可以通过 super 访问,这使得对输入进行预转换、路由到其他方法和后处理输出变得容易。

class Foo { def bar() { "bar" } }

class MyFooMetaClass extends DelegatingMetaClass {
  MyFooMetaClass(MetaClass metaClass) { super(metaClass) }
  MyFooMetaClass(Class theClass) { super(theClass) }

  Object invokeMethod(Object object, String methodName, Object[] args) {
     def result = super.invokeMethod(object,methodName.toLowerCase(), args)
     result.toUpperCase();
  }
}


def mc =  new MyFooMetaClass(Foo.metaClass)
mc.initialize()

Foo.metaClass = mc
def f = new Foo()

assert f.BAR() == "BAR" // the new metaclass routes .BAR() to .bar() and uppercases the result
魔术包

可以通过给元类一个特殊构造的(魔术)类名和包名来在启动时更改元类。为了更改 java.lang.Integer 的元类,只需在 classpath 中放置一个 groovy.runtime.metaclass.java.lang.IntegerMetaClass 类即可。例如,当使用框架时,如果您想在代码被框架执行之前进行元类更改,这将很有用。魔术包的一般形式是 groovy.runtime.metaclass.[package].[class]MetaClass。在下面的示例中,[package]java.lang[class]Integer

// file: IntegerMetaClass.groovy
package groovy.runtime.metaclass.java.lang;

class IntegerMetaClass extends DelegatingMetaClass {
  IntegerMetaClass(MetaClass metaClass) { super(metaClass) }
  IntegerMetaClass(Class theClass) { super(theClass) }
  Object invokeMethod(Object object, String name, Object[] args) {
    if (name =~ /isBiggerThan/) {
      def other = name.split(/isBiggerThan/)[1].toInteger()
      object > other
    } else {
      return super.invokeMethod(object,name, args);
    }
  }
}

通过使用 groovyc IntegerMetaClass.groovy 编译上述文件,将生成 ./groovy/runtime/metaclass/java/lang/IntegerMetaClass.class。以下示例将使用这个新元类

// File testInteger.groovy
def i = 10

assert i.isBiggerThan5()
assert !i.isBiggerThan15()

println i.isBiggerThan5()

通过使用 groovy -cp . testInteger.groovy 运行该文件,IntegerMetaClass 将在 classpath 中,因此它将成为 java.lang.Integer 的元类,拦截对 isBiggerThan*() 方法的调用。

1.9.3. 每实例元类

您可以单独更改单个对象的元类,因此可以有多个具有不同元类的相同类的对象。

class Foo { def bar() { "bar" }}

class FooMetaClass extends DelegatingMetaClass {
  FooMetaClass(MetaClass metaClass) { super(metaClass) }
  Object invokeMethod(Object object, String name, Object[] args) {
      super.invokeMethod(object,name,args).toUpperCase()
  }
}

def f1 = new Foo()
def f2 = new Foo()
f2.metaClass = new FooMetaClass(f2.metaClass)

assert f1.bar() == "bar"
assert f2.bar() == "BAR"
assert f1.metaClass =~ /MetaClassImpl/
assert f2.metaClass =~ /FooMetaClass/
assert f1.class.toString() == "class Foo"
assert f2.class.toString() == "class Foo"

1.9.4. ExpandoMetaClass

Groovy 带有一个特殊的 MetaClass,即所谓的 ExpandoMetaClass。它的特殊之处在于它允许通过使用简洁的闭包语法动态添加或更改方法、构造函数、属性甚至静态方法。

测试指南 所示,在模拟或存根场景中,应用这些修改尤其有用。

每个 java.lang.Class 都由 Groovy 提供一个特殊的 metaClass 属性,它将为您提供一个 ExpandoMetaClass 实例的引用。然后可以使用此实例添加方法或更改现有方法的行为。

默认情况下,ExpandoMetaClass 不支持继承。要启用此功能,您必须在应用程序启动前(例如在主方法或 servlet 引导中)调用 ExpandoMetaClass#enableGlobally()

以下部分详细介绍了 ExpandoMetaClass 在各种场景中的用法。

方法

一旦通过调用 metaClass 属性访问了 ExpandoMetaClass,就可以使用左移 <<= 运算符添加方法。

请注意,左移运算符用于 追加 新方法。如果类或接口声明了具有相同名称和参数类型的公共方法(包括从超类和超接口继承的方法,但不包括在运行时添加到 metaClass 的方法),则会抛出异常。如果要 替换 类或接口声明的方法,可以使用 = 运算符。

运算符应用于 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 的属性来声明 可变属性

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 附带了各种编解码器实现,每个都定义在一个类中。在运行时,应用程序 classpath 中将有多个编解码器类。在应用程序启动时,框架会向某些元类添加 encodeXXXdecodeXXX 方法,其中 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。MetaMethods 告诉您哪些方法在运行时可用,因此您的代码可以适应。

这在重写 invokeMethodgetProperty 和/或 setProperty 时特别有用。

GroovyObject 方法

ExpandoMetaClass 的另一个功能是它允许重写 invokeMethodgetPropertysetProperty 方法,所有这些方法都可以在 groovy.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()

Closure 代码的第一步是查找给定名称和参数的 MetaMethod。如果找到方法,则一切正常并将其委托。否则,返回一个虚拟值。

MetaMethod 是已知存在于 MetaClass 上的方法,无论是在运行时还是编译时添加。

相同的逻辑可用于重写 setPropertygetProperty

class Person {
   String name = "Fred"
}

Person.metaClass.getProperty = { String name ->
   def metaProperty = Person.metaClass.getMetaProperty(name)
   def result
   if(metaProperty) result = metaProperty.getProperty(delegate)
   else {
      result = "Flintstone"
   }
   result
}

def p = new Person()

assert "Fred" == p.name
assert "Flintstone" == p.other

这里需要注意的重要一点是,不是查找 MetaMethod,而是查找 MetaProperty 实例。如果存在,则调用 MetaPropertygetProperty 方法,并传递委托。

重写静态 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()

用于重写静态方法的逻辑与我们之前看到的重写实例方法的逻辑相同。唯一的区别是对 metaClass.static 属性的访问以及调用 getStaticMethodName 以检索静态 MetaMethod 实例。

扩展接口

可以使用 ExpandoMetaClass 将方法添加到接口。但是,要做到这一点,它 必须 在应用程序启动前使用 ExpandoMetaClass.enableGlobally() 方法全局启用。

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

def list = []

list << 1
list << 2

assert 4 == list.sizeDoubled()

1.10. 扩展模块

1.10.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 类上定义一个名为 getText 的方法(因为第一个参数是 File 类型),它接受一个参数作为参数(编码 String)。

创建扩展模块的过程很简单

  • 如上所示编写一个扩展类

  • 编写一个模块描述符文件

然后您必须使扩展模块对 Groovy 可见,这就像在 classpath 中提供扩展模块类和描述符一样简单。这意味着您有选择

  • 直接在 classpath 中提供类和模块描述符

  • 或将您的扩展模块打包到 jar 中以实现可重用性

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

  • 实例方法(在类的实例上调用)

  • 静态方法(在类本身上调用)

1.10.2. 实例方法

要向现有类添加实例方法,您需要创建一个扩展类。例如,假设您想在 Integer 上添加一个 maxRetries 方法,该方法接受一个闭包并最多执行 n 次,直到没有异常抛出。为此,您只需编写以下内容

MaxRetriesExtension.groovy
class MaxRetriesExtension {                                     (1)
    static void maxRetries(Integer self, Closure code) {        (2)
        assert self >= 0
        int retries = self
        Throwable e = null
        while (retries > 0) {
            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 {
        i++
        throw new RuntimeException("oops")
    }
} catch (RuntimeException e) {
    assert i == 5
}

1.10.3. 静态方法

也可以向类添加静态方法。在这种情况下,静态方法需要定义在其 自己的 文件中。静态和实例扩展方法 不能 出现在同一个类中。

StaticStringExtension.groovy
class StaticStringExtension {                                       (1)
    static String greeting(String self) {                           (2)
        'Hello, world!'
    }
}
1 静态扩展类
2 静态方法的第一个参数对应于被扩展的类,并且 未使用

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

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

1.10.4. 模块描述符

为了让 Groovy 能够加载您的扩展方法,您必须声明您的扩展帮助类。您必须在 META-INF/groovy 目录中创建一个名为 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.10.5. 扩展模块和 classpath

值得注意的是,您不能同时编译和使用扩展。这意味着要使用扩展,它 必须 在使用它的代码编译之前,作为编译后的类,在 classpath 中可用。通常,这意味着您不能将 测试 类与扩展类本身放在同一个源单元中。由于通常测试源与普通源分离并在构建的另一个步骤中执行,因此这不是问题。

1.10.6. 与类型检查的兼容性

与类别不同,扩展模块与类型检查兼容:如果它们在 classpath 中找到,则类型检查器会知道扩展方法,并且在您调用它们时不会抱怨。它也与静态编译兼容。

2. 编译时元编程

Groovy 中的编译时元编程允许在编译时生成代码。这些转换正在改变程序的抽象语法树(AST),这就是为什么在 Groovy 中我们称之为 AST 转换。AST 转换允许您挂接到编译过程,修改 AST 并继续编译过程以生成常规字节码。与运行时元编程相比,这具有将更改在类文件本身(即在字节码中)中可见的优点。在字节码中可见很重要,例如,如果您希望转换成为类契约的一部分(实现接口,扩展抽象类等)或者如果您需要您的类可以从 Java(或其他 JVM 语言)调用。例如,AST 转换可以向类添加方法。如果您使用运行时元编程,新方法将仅从 Groovy 可见。如果您使用编译时元编程进行相同的操作,该方法也将从 Java 可见。最后但并非最不重要的一点是,编译时元编程的性能可能会更好(因为不需要初始化阶段)。

在本节中,我们将首先解释 Groovy 发行版中捆绑的各种编译时转换。在后续部分中,我们将描述如何 实现您自己的 AST 转换 以及这种技术的缺点。

2.1. 可用的 AST 转换

Groovy 附带了各种 AST 转换,涵盖了不同的需求:减少样板(代码生成),实现设计模式(委托等),日志记录,声明式并发,克隆,更安全的脚本,调整编译,实现 Swing 模式,测试以及最终管理依赖项。如果这些 AST 转换都不能满足您的需求,您仍然可以实现自己的转换,如 开发您自己的 AST 转换 部分所示。

AST 转换可以分为两类

  • 全局 AST 转换是透明地、全局地应用的,只要在编译 classpath 中找到它们。

  • 局部 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 注解接受几个参数,总结在下表中

属性 默认值 描述 示例

排除项

空列表

要从 toString 中排除的属性列表

@ToString(excludes=['firstName'])
class Person {
    String firstName
    String lastName
}

def p = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p.toString() == 'Person(Nicholson)'

包含项

未定义标记列表(表示所有字段)

要包含在 toString 中的字段列表

@ToString(includes=['lastName'])
class Person {
    String firstName
    String lastName
}

def p = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p.toString() == 'Person(Nicholson)'

includeSuper

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

includeNames

是否在生成的 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)'

includeFields

除了属性之外,字段是否应包含在 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)'

includeSuperProperties

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

includeSuperFields

可见超字段是否应包含在 toString 中

class Person {
    protected String name
}

@ToString(includeSuperFields = true, includeNames = true)
@MapConstructor(includeSuperFields = true)
class BandMember extends Person {
    String bandName
}

def bono = new BandMember(name:'Bono', bandName: 'U2').toString()

assert bono.toString() == 'BandMember(bandName:U2, name:Bono)'

ignoreNulls

是否显示 null 值的属性/字段

@ToString(ignoreNulls=true)
class Person {
    String firstName
    String lastName
}

def p = new Person(firstName: 'Jack')
assert p.toString() == 'Person(Jack)'

includePackage

在 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)'

allProperties

在 toString 中包含所有 JavaBean 属性

@ToString(includeNames=true)
class Person {
    String firstName
    String getLastName() { 'Nicholson' }
}

def p = new Person(firstName: 'Jack')
assert p.toString() == 'acme.Person(firstName:Jack, lastName:Nicholson)'

缓存

缓存 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

allNames

具有内部名称的字段和/或属性是否应包含在生成的 toString 中

@ToString(allNames=true)
class Person {
    String $firstName
}

def p = new Person($firstName: "Jack")
assert p.toString() == 'acme.Person(Jack)'
@groovy.transform.EqualsAndHashCode

@EqualsAndHashCode AST 转换旨在为您生成 equalshashCode 方法。生成的哈希码遵循 Josh BlochEffective 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 的行为

属性 默认值 描述 示例

排除项

空列表

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

包含项

未定义标记列表(表示所有字段)

要包含在 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()

缓存

缓存 hashCode 计算。仅当类不可变时才应设置为 true。

import groovy.transform.EqualsAndHashCode
import groovy.transform.Immutable

@Immutable
class SlowHashCode {
    static final SLEEP_PERIOD = 500

    int hashCode() {
        sleep SLEEP_PERIOD
        127
    }
}

@EqualsAndHashCode(cache=true)
@Immutable
class Person {
    SlowHashCode slowHashCode = new SlowHashCode()
}

def p = new Person()
p.hashCode()

def start = System.currentTimeMillis()
p.hashCode()
assert System.currentTimeMillis() - start < SlowHashCode.SLEEP_PERIOD

callSuper

是否在 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 being', firstName: 'Jack', lastName: 'Nicholson')

assert p1!=p2
assert p1.hashCode() != p2.hashCode()

includeFields

除了属性之外,字段是否应包含在 equals/hashCode 中

import groovy.transform.EqualsAndHashCode

@EqualsAndHashCode(includeFields=true)
class Person {
    private String firstName

    Person(String firstName) {
        this.firstName = firstName
    }
}

def p1 = new Person('Jack')
def p2 = new Person('Jack')
def p3 = new Person('Bob')

assert p1 == p2
assert p1 != p3

useCanEqual

equals 是否应调用 canEqual 辅助方法。

allProperties

JavaBean 属性是否应包含在 equals 和 hashCode 计算中

@EqualsAndHashCode(allProperties=true, excludes='first, last')
class Person {
    String first, last
    String getInitials() { first[0] + last[0] }
}

def p1 = new Person(first: 'Jack', last: 'Smith')
def p2 = new Person(first: 'Jack', last: 'Spratt')
def p3 = new Person(first: 'Bob', last: 'Smith')

assert p1 == p2
assert p1.hashCode() == p2.hashCode()
assert p1 != p3
assert p1.hashCode() != p3.hashCode()

allNames

具有内部名称的字段和/或属性是否应包含在 equals 和 hashCode 计算中

import groovy.transform.EqualsAndHashCode

@EqualsAndHashCode(allNames=true)
class Person {
    String $firstName
}

def p1 = new Person($firstName: 'Jack')
def p2 = new Person($firstName: 'Bob')

assert p1 != p2
assert p1.hashCode() != p2.hashCode()
@groovy.transform.TupleConstructor

@TupleConstructor 注解旨在通过为您生成构造函数来消除样板代码。创建一个元组构造函数,其中每个属性(以及可能每个字段)都有一个参数。每个参数都有一个默认值(如果存在则使用属性的初始值,否则根据属性类型使用 Java 的默认值)。

实现细节

通常,您不需要了解生成的构造函数的实现细节;您只需以正常方式使用它们。但是,如果您想添加多个构造函数,了解 Java 集成选项或满足某些依赖注入框架的要求,那么一些细节就很有用了。

如前所述,生成的构造函数应用了默认值。在后续编译阶段,Groovy 编译器的标准默认值处理行为将应用于。最终结果是在您的类的字节码中放置了多个构造函数。这提供了明确的语义,并且对于 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')

第一个构造函数是无参数构造函数,只要您没有 final 属性,它就允许传统的映射式构造。Groovy 在内部调用无参数构造函数,然后调用相关的 setter。值得注意的是,如果第一个属性(或字段)的类型是 LinkedHashMap,或者只有一个 Map、AbstractMap 或 HashMap 属性(或字段),则映射式命名参数将不可用。

其他构造函数是按照它们定义的顺序获取属性生成的。Groovy 将生成与属性(或字段,取决于选项)数量相同的构造函数。

defaults 属性(参见可用配置选项表)设置为 false,将禁用正常的默认值行为,这意味着

  • 将只生成一个构造函数

  • 尝试使用初始值将导致错误

  • 映射样式命名参数将不可用

此属性通常仅用于其他 Java 框架期望只有一个构造函数的情况,例如注入框架或 JUnit 参数化运行器。

不变性支持

如果 @PropertyOptions 注解也出现在带有 @TupleConstructor 注解的类上,那么生成的构造函数可能包含自定义属性处理逻辑。@PropertyOptions 注解上的 propertyHandler 属性可以设置为 ImmutablePropertyHandler,这将导致添加不可变类所需的逻辑(防御性复制、克隆等)。这通常在使用 @Immutable 元注解时会自动发生。某些注解属性可能不支持所有属性处理器。

自定义选项

@TupleConstructor AST 转换接受几个注解属性

属性 默认值 描述 示例

排除项

空列表

要从元组构造函数生成中排除的属性列表

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

包含项

未定义列表(表示所有字段)

要包含在元组构造函数生成中的字段列表

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

includeProperties

属性是否应包含在元组构造函数生成中

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
}

includeFields

除了属性之外,字段是否应包含在元组构造函数生成中

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

includeSuperProperties

超类中的属性是否应包含在元组构造函数生成中

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'

includeSuperFields

超类中的字段是否应包含在元组构造函数生成中

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

callSuper

父构造函数调用中是否应调用超属性,而不是设置为属性

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'

强制

默认情况下,如果已定义构造函数,则转换将不执行任何操作。将此属性设置为 true,将生成构造函数,并且您有责任确保未定义重复的构造函数。

import groovy.transform.*

@ToString @TupleConstructor(force=true)
final class Person {
    String name
    // explicit constructor would normally disable tuple constructor
    Person(String first, String last) { this("$first $last") }
}

assert new Person('john smith').toString() == 'Person(john smith)'
assert new Person('john', 'smith').toString() == 'Person(john smith)'

默认值

指示构造函数参数启用默认值处理。设置为 false 可获得一个构造函数,但禁用初始值支持和命名参数。

@ToString
@TupleConstructor(defaults=false)
class Musician {
  String name
  String instrument
  int born
}

assert new Musician('Jimi', 'Guitar', 1942).toString() == 'Musician(Jimi, Guitar, 1942)'
assert Musician.constructors.size() == 1

使用 Setters

默认情况下,转换将直接从其相应的构造函数参数设置每个属性的后备字段。将此属性设置为 true,构造函数将调用 setter(如果存在)。通常认为在构造函数中调用可重写的 setter 是不好的风格。您有责任避免这种不良风格。

import groovy.transform.*

@ToString @TupleConstructor(useSetters=true)
final class Foo {
    String bar
    void setBar(String bar) {
        this.bar = bar?.toUpperCase() // null-safe
    }
}

assert new Foo('cat').toString() == 'Foo(CAT)'
assert new Foo(bar: 'cat').toString() == 'Foo(CAT)'

allNames

具有内部名称的字段和/或属性是否应包含在构造函数中

import groovy.transform.TupleConstructor

@TupleConstructor(allNames=true)
class Person {
    String $firstName
}

def p = new Person('Jack')

assert p.$firstName == 'Jack'

allProperties

JavaBean 属性是否应包含在构造函数中

@TupleConstructor(allProperties=true)
class Person {
    String first
    private String last
    void setLast(String last) {
        this.last = last
    }
    String getName() { "$first $last" }
}

assert new Person('john', 'smith').name == 'john smith'

包含要插入到生成的构造函数开头的语句的闭包

import groovy.transform.TupleConstructor

@TupleConstructor(pre={ first = first?.toLowerCase() })
class Person {
    String first
}

def p = new Person('Jack')

assert p.first == 'jack'

包含要插入到生成的构造函数末尾的语句的闭包

import groovy.transform.TupleConstructor
import static groovy.test.GroovyAssert.shouldFail

@TupleConstructor(post={ assert first })
class Person {
    String first
}

def jack = new Person('Jack')
shouldFail {
  def unknown = new Person()
}

defaults 注解属性设置为 false,将 force 注解属性设置为 true,允许通过为不同情况使用不同的自定义选项(前提是每种情况具有不同的类型签名)创建多个元组构造函数,如以下示例所示

class Named {
  String name
}

@ToString(includeSuperProperties=true, ignoreNulls=true, includeNames=true, includeFields=true)
@TupleConstructor(force=true, defaults=false)
@TupleConstructor(force=true, defaults=false, includeFields=true)
@TupleConstructor(force=true, defaults=false, includeSuperProperties=true)
class Book extends Named {
  Integer published
  private Boolean fiction
  Book() {}
}

assert new Book("Regina", 2015).toString() == 'Book(published:2015, name:Regina)'
assert new Book(2015, false).toString() == 'Book(published:2015, fiction:false)'
assert new Book(2015).toString() == 'Book(published:2015)'
assert new Book().toString() == 'Book()'
assert Book.constructors.size() == 4

同样,这是另一个使用不同 includes 选项的示例

@ToString(includeSuperProperties=true, ignoreNulls=true, includeNames=true, includeFields=true)
@TupleConstructor(force=true, defaults=false, includes='name,year')
@TupleConstructor(force=true, defaults=false, includes='year,fiction')
@TupleConstructor(force=true, defaults=false, includes='name,fiction')
class Book {
    String name
    Integer year
    Boolean fiction
}

assert new Book("Regina", 2015).toString() == 'Book(name:Regina, year:2015)'
assert new Book(2015, false).toString() == 'Book(year:2015, fiction:false)'
assert new Book("Regina", false).toString() == 'Book(name:Regina, fiction:false)'
assert Book.constructors.size() == 3
@groovy.transform.MapConstructor

@MapConstructor 注解旨在通过为您生成一个映射构造函数来消除样板代码。映射构造函数是这样创建的:类中的每个属性都根据所提供映射中具有属性名称键的值进行设置。用法如本例所示

import groovy.transform.*

@ToString
@MapConstructor
class Person {
    String firstName
    String lastName
}

def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1.toString() == 'Person(Jack, Nicholson)'

生成的构造函数大致如下所示

public Person(Map args) {
    if (args.containsKey('firstName')) {
        this.firstName = args.get('firstName')
    }
    if (args.containsKey('lastName')) {
        this.lastName = args.get('lastName')
    }
}
@groovy.transform.Canonical

@Canonical 元注解结合了 @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 元注解生成类似的不可变类。@Canonical 元注解支持它聚合的注解中的配置选项。有关更多详细信息,请参阅这些注解。

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(excludes=['lastName'])

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

assert p1==p2 // Effect of @EqualsAndHashCode(excludes=['lastName'])
assert p1.hashCode()==p2.hashCode() // Effect of @EqualsAndHashCode(excludes=['lastName'])

@Canonical 元注解可以与显式使用一个或多个其组件注解结合使用,如下所示

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(excludes=['lastName'])

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

assert p1==p2 // Effect of @EqualsAndHashCode(excludes=['lastName'])
assert p1.hashCode()==p2.hashCode() // Effect of @EqualsAndHashCode(excludes=['lastName'])

@Canonical 的任何适用注解属性都会传递给显式注解,但显式注解中已有的属性具有优先级。

@groovy.transform.InheritConstructors

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

复制构造函数时是否保留注解

@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 == 'groovy.transform.Generated'
assert Child.constructors[0].annotations[1].annotationType().name == 'ConsAnno'

parameterAnnotations

复制构造函数时是否保留构造函数参数的注解

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

如果字段声明为 volatile,则初始化将使用 双重检查锁定 模式进行同步。

使用 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 转换用于帮助编写可比较且易于按多个属性排序的类。它易于使用,如以下示例所示,我们注释 Person

import groovy.transform.Sortable

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

生成的类具有以下属性

  • 它实现了 Comparable 接口

  • 它包含一个 compareTo 方法,其实现基于 firstlastborn 属性的自然排序

  • 它有三个返回比较器的方法:comparatorByFirstcomparatorByLastcomparatorByBorn

生成的 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']

通常,所有属性都按照它们定义的优先级顺序在生成的 compareTo 方法中使用。您可以通过在 includesexcludes 注解属性中提供属性名称列表来包含或排除某些属性。如果使用 includes,给定属性名称的顺序将决定比较时属性的优先级。为了说明这一点,请考虑以下 Person 类定义

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

它将有两个比较器方法 comparatorByFirstcomparatorByBorn,生成的 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']

@Sortable AST 转换的行为可以通过以下附加参数进一步更改

属性 默认值 描述 示例

allProperties

是否使用 JavaBean 属性(在本地属性之后排序)

import groovy.transform.*

@Canonical(includeFields = true)
@Sortable(allProperties = true, includes = 'nameSize')
class Player {
  String name
  int getNameSize() { name.size() }
}

def finalists = [
  new Player('Serena'),
  new Player('Venus'),
  new Player('CoCo'),
  new Player('Mirjana')
]

assert finalists.sort()*.name == ['CoCo', 'Venus', 'Serena', 'Mirjana']

allNames

是否使用具有“内部”名称的属性

import groovy.transform.*

@Canonical(allNames = true)
@Sortable(allNames = false)
class Player {
  String $country
  String name
}

def finalists = [
  new Player('USA', 'Serena'),
  new Player('USA', 'Venus'),
  new Player('USA', 'CoCo'),
  new Player('Croatian', 'Mirjana')
]

assert finalists.sort()*.name == ['Mirjana', 'CoCo', 'Serena', 'Venus']

includeSuperProperties

是否也使用超属性(首先排序)

class Person {
  String name
}

@Canonical(includeSuperProperties = true)
@Sortable(includeSuperProperties = true)
class Citizen extends Person {
  String country
}

def people = [
  new Citizen('Bob', 'Italy'),
  new Citizen('Cathy', 'Hungary'),
  new Citizen('Cathy', 'Egypt'),
  new Citizen('Bob', 'Germany'),
  new Citizen('Alan', 'France')
]

assert people.sort()*.name == ['Alan', 'Bob', 'Bob', 'Cathy', 'Cathy']
assert people.sort()*.country == ['France', 'Germany', 'Italy', 'Egypt', 'Hungary']
@groovy.transform.builder.Builder

@Builder AST 转换用于帮助编写可以使用 fluent api 调用创建的类。该转换支持多种构建策略以覆盖各种情况,并且有许多配置选项可以自定义构建过程。如果您是 AST 黑客,您还可以定义自己的策略类。下表列出了 Groovy 捆绑的可用策略以及每种策略支持的配置选项。

策略

描述

builderClassName

builderMethodName

buildMethodName

前缀

包括/排除

includeSuperProperties

allNames

SimpleStrategy

链式设置器

不适用

不适用

不适用

是,默认“set”

不适用

是,默认 false

ExternalStrategy

显式构建器类,正在构建的类未触及

不适用

不适用

是,默认“build”

是,默认“”

是,默认 false

是,默认 false

DefaultStrategy

创建嵌套辅助类

是,默认 <TypeName>Builder

是,默认“builder”

是,默认“build”

是,默认“”

是,默认 false

是,默认 false

InitializerStrategy

创建嵌套辅助类,提供类型安全的流畅创建

是,默认 <TypeName>Initializer

是,默认“createInitializer”

是,默认“create”,但通常仅在内部使用

是,默认“”

是,默认 false

是,默认 false

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'

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

调用链式设置器将如下所示

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

您可以将 SimpleStrategy@TupleConstructor 结合使用。如果您的 @Builder 注解没有显式的 includesexcludes 注解属性,但您的 @TupleConstructor 注解有,则 @TupleConstructor 中的属性将被重用于 @Builder。这同样适用于任何组合 @TupleConstructor 的注解别名,例如 @Canonical

如果您有要在构造过程中调用的 setter,则可以使用注解属性 useSetters。有关详细信息,请参阅 JavaDoc。

此策略不支持注解属性 builderClassNamebuildMethodNamebuilderMethodNameforClassincludeSuperProperties

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
}

您正在为其创建构建器的类可以是遵循正常 JavaBean 约定的任何 Java 或 Groovy 类,例如,一个无参构造函数和属性的 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()

生成的构建器可以使用 prefixincludesexcludesbuildMethodName 注解属性进行自定义。这是一个说明各种自定义的示例

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'

@BuilderbuilderMethodNamebuilderClassName 注解属性不适用于此策略。

您可以将 ExternalStrategy@TupleConstructor 结合使用。如果您的 @Builder 注解没有显式的 includesexcludes 注解属性,但您要为其创建构建器的类的 @TupleConstructor 注解有,则 @TupleConstructor 中的属性将被重用于 @Builder。这同样适用于任何组合 @TupleConstructor 的注解别名,例如 @Canonical

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

如果您愿意,可以使用 builderClassNamebuildMethodNamebuilderMethodNameprefixincludesexcludes 注解属性来自定义构建过程的各个方面,其中一些在示例中用到

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 注解(在类、方法或构造函数位置),则由您来确保生成的辅助类和工厂方法具有唯一的名称(即,不能有多个使用默认名称值)。有关方法和构造函数用法(以及说明唯一名称所需的重命名)的示例,但使用 DefaultStrategy 策略,请参阅该策略的文档。

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 注解没有显式的 includesexcludes 注解属性,但您的 @Canonical 注解有,则 @Canonical 中的属性将被重用于 @Builder。这是一个使用 @Builder@Immutable 的示例

import groovy.transform.builder.*
import groovy.transform.*
import static groovy.transform.options.Visibility.PRIVATE

@Builder(builderStrategy=InitializerStrategy)
@Immutable
@VisibilityOptions(PRIVATE)
class Person {
    String first
    String last
    int born
}

def publicCons = Person.constructors
assert publicCons.size() == 1

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

如果您有要在构造过程中调用的 setter,则可以使用注解属性 useSetters。有关详细信息,请参阅 JavaDoc。

此策略还支持注释静态方法和构造函数。在这种情况下,静态方法或构造函数参数将成为用于构建目的的属性,并且在静态方法的情况下,方法的返回类型将成为正在构建的目标类。如果您在类中使用了多个 @Builder 注解(在类、方法或构造函数位置),则由您来确保生成的辅助类和工厂方法具有唯一的名称(即,不能有多个使用默认名称值)。有关方法和构造函数用法(但使用 DefaultStrategy 策略)的示例,请参阅该策略的文档。

此策略不支持注解属性 forClass

@groovy.transform.AutoImplement

@AutoImplement AST 转换提供任何找到的抽象方法(来自超类或接口)的虚拟实现。虚拟实现对于所有找到的抽象方法都是相同的,可以是

  • 本质上为空(对于 void 方法和带返回类型的方法,返回该类型的默认值)

  • 抛出指定异常的语句(带可选消息)

  • 一些用户提供的代码

第一个示例说明了默认情况。我们的类使用 @AutoImplement 注解,具有一个超类和一个接口,如这里所示

import groovy.transform.AutoImplement

@AutoImplement
class MyNames extends AbstractList<String> implements Closeable { }

提供了 Closeable 接口的 void close() 方法,并将其留空。还提供了超类中三个抽象方法的实现。getaddAllsize 方法分别具有 Stringbooleanint 的返回类型,默认值分别为 nullfalse0。我们可以使用我们的类(并检查其中一个方法的预期返回类型),使用以下代码

assert new MyNames().size() == 0

同样值得研究等效的生成代码

class MyNames implements Closeable extends AbstractList<String> {

    String get(int param0) {
        return null
    }

    boolean addAll(Collection<? extends String> param0) {
        return false
    }

    void close() throws Exception {
    }

    int size() {
        return 0
    }

}

第二个示例说明了最简单的异常情况。我们的类使用 @AutoImplement 注解,有一个超类,并且一个注解属性表示如果调用我们的任何“虚拟”方法,则应抛出 IOException。这是类定义

@AutoImplement(exception=IOException)
class MyWriter extends Writer { }

我们可以使用该类(并检查其中一个方法是否抛出预期异常),使用以下代码

import static groovy.test.GroovyAssert.shouldFail

shouldFail(IOException) {
  new MyWriter().flush()
}

同样值得研究等效的生成代码,其中提供了三个 void 方法,所有这些方法都抛出提供的异常

class MyWriter extends Writer {

    void flush() throws IOException {
        throw new IOException()
    }

    void write(char[] param0, int param1, int param2) throws IOException {
        throw new IOException()
    }

    void close() throws Exception {
        throw new IOException()
    }

}

第三个示例说明了带有提供消息的异常情况。我们的类使用 @AutoImplement 注解,实现了一个接口,并且具有注解属性,指示任何提供的方法都应抛出带有 Not supported by MyIterator 作为消息的 UnsupportedOperationException。这是类定义

@AutoImplement(exception=UnsupportedOperationException, message='Not supported by MyIterator')
class MyIterator implements Iterator<String> { }

我们可以使用该类(并检查其中一个方法是否抛出预期异常并具有预期形式的消息),使用以下代码

def ex = shouldFail(UnsupportedOperationException) {
     new MyIterator().hasNext()
}
assert ex.message == 'Not supported by MyIterator'

同样值得研究等效的生成代码,其中提供了三个 void 方法,所有这些方法都抛出提供的异常

class MyIterator implements Iterator<String> {

    boolean hasNext() {
        throw new UnsupportedOperationException('Not supported by MyIterator')
    }

    String next() {
        throw new UnsupportedOperationException('Not supported by MyIterator')
    }

}

同样值得研究等效的生成代码,其中提供了 next 方法

@AutoImplement(code = { throw new UnsupportedOperationException('Should never be called but was called on ' + new Date()) })
class EmptyIterator implements Iterator<String> {
    boolean hasNext() { false }
}

next 方法已提供。

def ex = shouldFail(UnsupportedOperationException) {
     new EmptyIterator().next()
}
assert ex.message.startsWith('Should never be called but was called on ')

同样值得研究等效的生成代码,其中提供了 next 方法。

class EmptyIterator implements java.util.Iterator<String> {

    boolean hasNext() {
        false
    }

    String next() {
        throw new UnsupportedOperationException('Should never be called but was called on ' + new Date())
    }

}
@groovy.transform.NullCheck

@NullCheck AST 转换将空值检查守卫语句添加到构造函数和方法中,这会导致这些方法在提供空参数时尽早失败。它可以被视为一种防御性编程形式。注解可以添加到单个方法或构造函数,或添加到类中,在这种情况下它将应用于所有方法/构造函数。

@NullCheck
String longerOf(String first, String second) {
    first.size() >= second.size() ? first : second
}

assert longerOf('cat', 'canary') == 'canary'
def ex = shouldFail(IllegalArgumentException) {
    longerOf('cat', null)
}
assert ex.message == 'second cannot be null'

2.1.2. 类设计注解

此类别注解旨在通过使用声明式风格简化众所周知的设计模式(委托、单例等)的实现。

@groovy.transform.BaseScript

@BaseScript 用于脚本中,表示脚本应扩展自定义脚本基类而不是 groovy.lang.Script。有关更多详细信息,请参阅 领域特定语言 的文档。

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

除了注释属性(或字段)之外,您还可以注释方法。在这种情况下,该方法可以被认为是委托的 getter 或工厂方法。例如,这里有一个类(相当不寻常地)具有一个委托池,它们以循环方式访问

class Test {
    private int robinCount = 0
    private List<List> items = [[0], [1], [2]]

    @Delegate
    List getRoundRobinList() {
        items[robinCount++ % items.size()]
    }

    void checkItems(List<List> testValue) {
        assert items == testValue
    }
}

这是该类的用法示例

def t = new Test()
t << 'fee'
t << 'fi'
t << 'fo'
t << 'fum'
t.checkItems([[0, 'fee', 'fum'], [1, 'fi'], [2, 'fo']])

以这种循环方式使用标准列表会违反列表的许多预期属性,因此不要指望上述类除了这个简单的示例之外还能做任何有用的事情。

@Delegate AST 转换的行为可以通过以下参数更改

属性 默认值 描述 示例

接口

字段实现的接口是否也应由类实现

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

已弃用

如果为 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

方法注解

是否将委托方法上的注解复制到委托方法。

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==1
assert d2.class.getDeclaredMethod('method').annotations.length==2

parameterAnnotations

是否将委托方法参数上的注解复制到委托方法参数。

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

排除项

空数组

要从委托中排除的方法列表。有关更细粒度的控制,另请参见 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

包含项

未定义标记数组(表示所有方法)

要包含在委托中的方法列表。有关更细粒度的控制,另请参见 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()} ~ ${bools.toString()}" }
}
def b = new NumberBooleanBuilder()
b.append(true)
b.append(3.14f)
b.append(false)
b.append(0.0f)
assert b.result() == "truefalse ~ 3.140.0"
b.append(3.5d) // would fail because we didn't include append(double)

allNames

委托模式是否也应用于具有内部名称的方法

class Worker {
    void task$() {}
}
class Delegating {
    @Delegate(allNames=true) Worker worker = new Worker()
}
def d = new Delegating()
d.task$() //passes
@groovy.transform.Immutable

@Immutable 元注解结合了以下注解

@Immutable 元注解简化了不可变类的创建。不可变类很有用,因为它们通常更容易推理,并且本质上是线程安全的。有关如何在 Java 中实现不可变类的所有详细信息,请参阅 Effective Java, Minimize Mutability@Immutable 元注解会自动为您完成 Effective Java 中描述的大部分工作。要使用元注解,您只需像以下示例中那样注释该类

import groovy.transform.Immutable

@Immutable
class Point {
    int x
    int y
}

不可变类的要求之一是无法修改类中的任何状态信息。实现此要求的一种方法是,对每个属性都使用不可变类,或者,对于构造函数和属性 Getter 中的任何可变属性,执行特殊编码(例如防御性复制输入和防御性复制输出)。在 @ImmutableBase@MapConstructor@TupleConstructor 之间,属性要么被识别为不可变,要么自动处理许多已知情况的特殊编码。我们提供了各种机制来扩展所允许的处理属性类型。有关详细信息,请参阅 @ImmutableOptions@KnownImmutable

@Immutable 应用于类后,其结果与应用 @Canonical 元注解的结果非常相似,但生成的类将包含额外的逻辑来处理不可变性。例如,尝试修改属性时会抛出 ReadOnlyPropertyException,因为属性的后端字段将自动变为最终字段,从而观察到此行为。

@Immutable 元注解支持其聚合注解中的配置选项。有关更多详细信息,请参阅这些注解。

@groovy.transform.ImmutableBase

使用 @ImmutableBase 生成的不可变类将自动变为 final。此外,将检查每个属性的类型,并对类执行各种检查,例如,目前不允许使用公共实例字段。如果需要,它还会生成一个 copyWith 构造函数。

支持以下注解属性

属性 默认值 描述 示例

copyWith

一个布尔值,表示是否生成 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.PropertyOptions

此注解允许您指定在类构造期间由转换使用的自定义属性处理器。主 Groovy 编译器会忽略它,但其他转换(如 @TupleConstructor@MapConstructor@ImmutableBase)会引用它。它经常在 @Immutable 元注解的幕后使用。

@groovy.transform.VisibilityOptions

此注解允许您为另一个转换生成的构造指定自定义可见性。主 Groovy 编译器会忽略它,但其他转换(如 @TupleConstructor@MapConstructor@NamedVariant)会引用它。

@groovy.transform.ImmutableOptions

Groovy 的不可变性支持依赖于一个预定义的已知不可变类列表(如 java.net.URIjava.lang.String),如果您使用不在该列表中的类型,它将失败。由于 @ImmutableOptions 注解的以下注解属性,您可以将已知不可变类型添加到列表中。

属性 默认值 描述 示例

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
}

如果您认为某个类型是不可变的,但它不是自动处理的类型之一,那么您需要正确编码该类以确保不可变性。

@groovy.transform.KnownImmutable

@KnownImmutable 注解实际上并不会触发任何 AST 转换。它只是一个标记注解。您可以使用此注解(包括 Java 类)注释您的类,它们将被识别为不可变类中成员的可接受类型。这使您无需显式使用 @ImmutableOptions 中的 knownImmutablesknownImmutableClasses 注解属性。

@groovy.transform.Memoized

@Memoized AST 转换通过简单地使用 @Memoized 注解方法来简化缓存的实现,从而允许缓存方法调用的结果。让我们想象以下方法:

long longComputation(int seed) {
    // slow computation
    Thread.sleep(100*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(100*seed)
    System.nanoTime()
}

def x = longComputation(1) // returns after 100 milliseconds
def y = longComputation(1) // returns immediately
def z = longComputation(2) // returns after 200 milliseconds
assert x==y
assert x!=z

缓存的大小可以使用两个可选参数进行配置:

  • protectedCacheSize:保证在垃圾回收后不会清除的结果数量。

  • maxCacheSize:可以保留在内存中的最大结果数量。

默认情况下,缓存大小不受限制,并且没有缓存结果受垃圾回收保护。设置 protectedCacheSize>0 将创建一个无限缓存,其中包含一些受保护的结果。设置 maxCacheSize>0 将创建一个有限缓存,但不受垃圾保护。同时设置两者将创建一个有限的受保护缓存。

@groovy.transform.TailRecursive

@TailRecursive 注解可用于自动将方法末尾的递归调用转换为等效的迭代版本。这避免了由于过多递归调用导致的堆栈溢出。下面是计算阶乘时的使用示例:

import groovy.transform.CompileStatic
import groovy.transform.TailRecursive

@CompileStatic
class Factorial {

    @TailRecursive
    static BigInteger factorial( BigInteger i, BigInteger product = 1) {
        if( i == 1) {
            return product
        }
        return factorial(i-1, product*i)
    }
}

assert Factorial.factorial(1) == 1
assert Factorial.factorial(3) == 6
assert Factorial.factorial(5) == 120
assert Factorial.factorial(50000).toString().size() == 213237 // Big number and no Stack Overflow

目前,此注解仅适用于自递归方法调用,即对完全相同方法的单个递归调用。如果遇到涉及简单相互递归的场景,请考虑使用 Closures 和 trampoline()。另请注意,目前仅处理非 void 方法(void 调用将导致编译错误)。

目前,某些形式的方法重载可能会欺骗编译器,并且某些非尾递归调用被错误地视为尾递归。
@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.lang.Mixin

已弃用。请考虑改用 trait。

2.1.3. 日志改进

Groovy 提供了一系列 AST 转换,有助于集成最广泛使用的日志框架。每个常用框架都有一个转换和关联的注解。这些转换提供了使用日志框架的流线型声明式方法。在每种情况下,转换都将:

  • 向注解类添加与日志记录器对应的静态最终 log 字段

  • 根据底层框架,将所有对 log.level() 的调用包装到适当的 log.isLevelEnabled 守卫中

这些转换支持两个参数:

  • 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 使用 @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!'
    }
}

您仍然需要将适当的 commons-logging jar 添加到您的类路径中。

@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!'
    }
}

您仍然需要将适当的 log4j jar 添加到您的类路径中。此注解还可以与兼容的 reload4j log4j 即插即用替代品一起使用,只需使用该项目的 jar,而不是 log4j jar。

@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!'
    }
}

您仍然需要将适当的 log4j2 jar 添加到您的类路径中。

@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!'
    }
}

您仍然需要将适当的 slf4j jar 添加到您的类路径中。

@groovy.util.logging.PlatformLog

Groovy 使用 @PlatformLog 注解支持 Java Platform Logging API and Service 框架。编写:

@groovy.util.logging.PlatformLog
class Greeter {
    void greet() {
        log.info 'Called greeter'
        println 'Hello, world!'
    }
}

等同于编写:

import java.lang.System.Logger
import java.lang.System.LoggerFinder
import static java.lang.System.Logger.Level.INFO

class Greeter {
    private static final transient Logger log =
        LoggerFinder.loggerFinder.getLogger(Greeter.class.name, Greeter.class.module)
    void greet() {
        log.log INFO, 'Called greeter'
        println 'Hello, world!'
    }
}

您需要使用 JDK 9+ 才能使用此功能。

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 最终字段(或静态方法的 $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. 更简单的克隆和外部化

Groovy 提供了两个注解,旨在分别促进 CloneableExternalizable 接口的实现,它们分别名为 @AutoClone@AutoExternalize

@groovy.transform.AutoClone

@AutoClone 注解旨在通过 style 参数使用各种策略实现 @java.lang.Cloneable 接口。

  • 默认的 AutoCloneStyle.CLONE 策略首先调用 super.clone(),然后对每个可克隆属性调用 clone()

  • AutoCloneStyle.SIMPLE 策略使用常规构造函数调用,并将属性从源复制到克隆。

  • AutoCloneStyle.COPY_CONSTRUCTOR 策略创建并使用复制构造函数。

  • AutoCloneStyle.SERIALIZATION 策略使用序列化(或外部化)来克隆对象。

这些策略各有利弊,在 groovy.transform.AutoClonegroovy.transform.AutoCloneStyle 的 Javadoc 中进行了讨论。

例如,以下示例:

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

请注意,String 属性未明确处理,因为 String 是不可变的,并且 Object 中的 clone() 方法将复制 String 引用。这也适用于原始字段和 java.lang.Number 的大多数具体子类。

除了克隆样式,@AutoClone 还支持多个选项:

属性 默认值 描述 示例

排除项

空列表

需要从克隆中排除的属性或字段名称列表。也允许使用由逗号分隔的字段/属性名称组成的字符串。有关详细信息,请参阅 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

默认情况下,只克隆属性。将此标志设置为 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 类。它将自动向类添加接口并生成 writeExternalreadExternal 方法。例如,此代码:

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 注解支持两个参数,可以稍微自定义其行为:

属性 默认值 描述 示例

排除项

空列表

需要从外部化中排除的属性或字段名称列表。也允许使用由逗号分隔的字段/属性名称组成的字符串。有关详细信息,请参阅 groovy.transform.AutoExternalize#excludes

import groovy.transform.AutoExternalize

@AutoExternalize(excludes='price')
class Book {
    String isbn
    String title
    float price
}

includeFields

默认情况下,只外部化属性。将此标志设置为 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(1000) // give at most 1000ms for the script to complete
if (t.alive) {
    t.interrupt()
}

转换自动修改用户代码,如下所示:

while (true) {
    if (Thread.currentThread().interrupted) {
        throw new InterruptedException('The current thread has been interrupted.')
    }
    i++
}

循环中引入的检查保证,如果当前线程上设置了 interrupt 标志,将抛出异常,中断线程的执行。

@ThreadInterrupt 支持多个选项,可让您进一步自定义转换的行为:

属性 默认值 描述 示例

thrown

java.lang.InterruptedException

指定如果线程中断则抛出的异常类型。

class BadException extends Exception {
    BadException(String message) { super(message) }
}

def config = new CompilerConfiguration()
config.addCompilationCustomizers(
        new ASTTransformationCustomizer(thrown:BadException, ThreadInterrupt)
)
def binding = new Binding(i:0)
def shell = new GroovyShell(this.class.classLoader,binding,config)

def userCode = """
try {
    while (true) {
        i++
    }
} catch (BadException e) {
    i = -1
}
"""

def t = Thread.start {
    shell.evaluate(userCode)
}
t.join(1000) // 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.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,则允许执行。如果返回 true,则将抛出异常。

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

如果您想创建包私有字段而不是属性(私有字段+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.Final

@Final 本质上是 final 修饰符的别名。意图是您几乎永远不会直接使用 @Final 注解(只需使用 final)。但是,在创建应将 final 修饰符应用于所注解节点的元注解时,您可以混入 @Final,例如:

@AnnotationCollector([Singleton,Final]) @interface MySingleton {}

@MySingleton
class GreetingService {
    String greeting(String name) { "Hello, $name!" }
}
assert GreetingService.instance.greeting('Bob') == 'Hello, Bob!'
assert Modifier.isFinal(GreetingService.modifiers)
@groovy.transform.AutoFinal

@AutoFinal 注解指示编译器在注解节点内的许多位置自动插入 final 修饰符。如果应用于方法(或构造函数),则该方法(或构造函数)的参数将被标记为 final。如果应用于类定义,则该类中所有声明的方法和构造函数也将受到相同处理。

将方法或构造函数的参数重新赋值给其主体通常被认为是糟糕的做法。通过向所有参数声明添加 final 修饰符,您可以完全避免这种做法。一些程序员认为,到处添加 final 会增加样板代码的数量,并使方法签名有些嘈杂。替代方法可能是使用代码审查流程或应用 codenarc 规则,以便在观察到这种做法时发出警告,但这些替代方法可能会导致质量检查期间的反馈延迟,而不是在 IDE 中或编译期间。@AutoFinal 注解旨在最大限度地提高编译器/IDE 反馈,同时保留简洁的代码,并最大程度地减少样板噪音。

以下示例说明了在类级别应用注解:

import groovy.transform.AutoFinal

@AutoFinal
class Person {
    private String first, last

    Person(String first, String last) {
        this.first = first
        this.last = last
    }

    String fullName(String separator) {
        "$first$separator$last"
    }

    String greeting(String salutation) {
        "$salutation, $first"
    }
}

在此示例中,构造函数的两个参数以及 fullnamegreeting 方法的单个参数都将是最终的。在构造函数或方法体中尝试修改这些参数将被编译器标记。

以下示例说明了在方法级别应用注解:

class Calc {
    @AutoFinal
    int add(int a, int b) { a + b }

    int mult(int a, int b) { a * b }
}

在这里,add 方法将具有最终参数,但 mult 方法将保持不变。

@groovy.transform.AnnotationCollector

@AnnotationCollector 允许创建元注解,这在专用章节中进行了描述。

@groovy.transform.TypeChecked

@TypeChecked 激活您 Groovy 代码的编译时类型检查。有关详细信息,请参阅类型检查章节

@groovy.transform.CompileStatic

@CompileStatic 激活您 Groovy 代码的静态编译。有关详细信息,请参阅类型检查章节

@groovy.transform.CompileDynamic

@CompileDynamic 禁用您 Groovy 代码部分的静态编译。有关详细信息,请参阅类型检查章节

@groovy.lang.DelegatesTo

@DelegatesTo 从技术上讲,不是 AST 转换。它旨在文档化代码,并在您使用类型检查静态编译时帮助编译器。该注解在本指南的DSL 部分有详细描述。

@groovy.transform.SelfType

@SelfType 不是 AST 转换,而是与 trait 一起使用的标记接口。有关详细信息,请参阅 trait 文档

2.1.8. Swing 模式

@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 从您的类中删除了大量样板代码,极大地提高了可读性。如果注解放在单个属性上,则只有该属性被绑定:

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

该转换将根据列表的泛型类型生成适当的 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

如果设置为 true,则生成的方法将同步。

class Component {
    @ListenerList(synchronize = true)
    List<ActionListener> listeners;
}
@groovy.beans.Vetoable

@Vetoable 注解的工作方式类似于 @Bindable,但根据 JavaBeans 规范生成受约束的属性,而不是绑定属性。该注解可以放置在类上,这意味着所有属性都将转换为受约束的属性,或者放置在单个属性上。例如,用 @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.test.NotYetImplemented

@NotYetImplemented 用于反转 JUnit 3/4 测试用例的结果。如果某个功能尚未实现但测试已存在,则它特别有用。在这种情况下,预期测试会失败。将其标记为 @NotYetImplemented 将反转测试结果,如下例所示:

import groovy.test.GroovyTestCase
import groovy.test.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
        }
    }
}

使用此技术的另一个优点是,您可以在知道如何修复错误之前为错误编写测试用例。如果将来某个时候,代码中的修改通过副作用修复了错误,您将收到通知,因为预期失败的测试通过了。

@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

@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

@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 返回标签为“anchor”的 AST 节点列表。
2 由于查找始终返回列表,因此始终需要选择要处理的元素。

例如,假设您要测试 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

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 处理

@groovy.lang.Grapes

Grape 是 Groovy 内置的依赖管理引擎,它依赖于本指南此章节中详细描述的几个注解。

2.2. 开发 AST 转换

转换分为两种:全局转换和局部转换。

  • 全局转换由编译器应用于正在编译的代码,无论转换适用于何处。实现全局转换的编译类位于 JAR 中,该 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 转换必须在九个定义的编译阶段之一中执行(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。现在,在运行示例脚本时,在 IDE 中将命中 LoggingASTTransformation 中设置的任何断点。

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

    }

    private static Statement createPrintlnAst(String message) {                 (11)
        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 在现有代码的第一条语句之前添加进入方法的消息
10 在现有代码的最后一条语句之后追加退出方法的消息
11 创建了一个 ExpressionStatement,它包装了一个 MethodCallExpression,对应于 this.println("message")

需要注意的是,为了本示例的简洁性,我们没有进行必要的检查,例如检查被注解的节点是否确实是 MethodNode,或者方法体是否是 BlockStatement 的实例。这个练习留给读者。

请注意 createPrintlnAst(String) 方法中新 println 语句的创建。为代码创建 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 转换与局部转换类似,但有一个主要区别:它们不需要注解,这意味着它们是全局应用的,也就是说,应用于每个正在编译的类。因此,将它们的使用限制在最后一种情况非常重要,因为它们会对编译器性能产生重大影响。

局部 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)                             (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 编写 AST 转换,强烈建议使用 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.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. 宏

简介

直到 2.5.0 版本,在开发 AST 转换时,开发人员需要深入了解编译器如何构建 AST(抽象语法树),以便了解如何在编译时添加新的表达式或语句。

虽然使用 org.codehaus.groovy.ast.tool.GeneralUtils 静态方法可以减轻直接编写这些 AST 节点的负担,但这仍然是一种低级别的方式。我们需要一些东西来抽象我们直接编写 AST 的方式,这正是 Groovy 宏的用途。它们允许您在编译期间直接添加代码,而无需将您想到的代码翻译成 org.codehaus.groovy.ast.* 节点相关的类。

语句和表达式

让我们看一个例子,创建一个局部 AST 转换:@AddMessageMethod。当应用于给定类时,它将向该类添加一个名为 getMessage 的新方法。该方法将返回“42”。注解非常简单:

@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.TYPE])
@GroovyASTTransformationClass(["metaprogramming.AddMethodASTTransformation"])
@interface AddMethod { }

不使用宏的 AST 转换会是什么样子?大概是这样:

@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class AddMethodASTTransformation extends AbstractASTTransformation {
    @Override
    void visit(ASTNode[] nodes, SourceUnit source) {
        ClassNode classNode = (ClassNode) nodes[1]

        ReturnStatement code =
                new ReturnStatement(                              (1)
                        new ConstantExpression("42"))             (2)

        MethodNode methodNode =
                new MethodNode(
                        "getMessage",
                        ACC_PUBLIC,
                        ClassHelper.make(String),
                        [] as Parameter[],
                        [] as ClassNode[],
                        code)                                     (3)

        classNode.addMethod(methodNode)                           (4)
    }
}
1 创建 return 语句
2 创建常量表达式“42”
3 将代码添加到新方法
4 将新方法添加到注解类

如果您不熟悉 AST API,那肯定不像您想象的代码。现在看看使用宏后,前面的代码如何简化。

@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class AddMethodWithMacrosASTTransformation extends AbstractASTTransformation {
    @Override
    void visit(ASTNode[] nodes, SourceUnit source) {
        ClassNode classNode = (ClassNode) nodes[1]

        ReturnStatement simplestCode = macro { return "42" }   (1)

        MethodNode methodNode =
                new MethodNode(
                        "getMessage",
                        ACC_PUBLIC,
                        ClassHelper.make(String),
                        [] as Parameter[],
                        [] as ClassNode[],
                        simplestCode)                          (2)

        classNode.addMethod(methodNode)                        (3)
    }
}
1 简单多了。您想添加一个返回“42”的 return 语句,这正是您在 macro 实用方法中可以读取到的。您的普通代码将为您翻译成 org.codehaus.groovy.ast.stmt.ReturnStatement
2 将 return 语句添加到新方法。
3 将新代码添加到注解类。

虽然此示例中使用了 macro 方法来创建**语句**,但 macro 方法也可以用于创建**表达式**,具体取决于您使用的 macro 签名:

  • macro(Closure):使用闭包内的代码创建给定语句。

  • macro(Boolean,Closure):如果为 **true**,则将闭包内的表达式包装在语句中;如果为 **false**,则返回表达式。

  • macro(CompilePhase, Closure):在特定编译阶段使用闭包内的代码创建给定语句。

  • macro(CompilePhase, Boolean, Closure):在特定编译阶段创建语句或表达式(true == 语句,false == 表达式)。

所有这些签名都可以在 org.codehaus.groovy.macro.runtime.MacroGroovyMethods 中找到。

有时我们可能只对创建给定表达式感兴趣,而不是整个语句,为此我们应该使用带有布尔参数的任何 macro 调用。

@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class AddGetTwoASTTransformation extends AbstractASTTransformation {

    BinaryExpression onePlusOne() {
        return macro(false) { 1 + 1 }                                      (1)
    }

    @Override
    void visit(ASTNode[] nodes, SourceUnit source) {
        ClassNode classNode = nodes[1]
        BinaryExpression expression = onePlusOne()                         (2)
        ReturnStatement returnStatement = GeneralUtils.returnS(expression) (3)

        MethodNode methodNode =
                new MethodNode("getTwo",
                        ACC_PUBLIC,
                        ClassHelper.Integer_TYPE,
                        [] as Parameter[],
                        [] as ClassNode[],
                        returnStatement                                    (4)
                )

        classNode.addMethod(methodNode)                                    (5)
    }
}
1 我们告诉宏不要将表达式包装在语句中,我们只对表达式感兴趣。
2 分配表达式
3 使用 GeneralUtils 中的方法创建 ReturnStatement 并返回表达式。
4 将代码添加到新方法
5 向类中添加方法
变量替换

宏很棒,但如果我们的宏无法接收参数或解析周围的变量,我们就无法创建任何有用或可重用的东西。

在以下示例中,我们正在创建一个 AST 转换 @MD5,当应用于给定字符串字段时,它将添加一个返回该字段 MD5 值的方法。

@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.FIELD])
@GroovyASTTransformationClass(["metaprogramming.MD5ASTTransformation"])
@interface MD5 { }

和转换

@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
class MD5ASTTransformation extends AbstractASTTransformation {

    @Override
    void visit(ASTNode[] nodes, SourceUnit source) {
        FieldNode fieldNode = nodes[1]
        ClassNode classNode = fieldNode.declaringClass
        String capitalizedName = fieldNode.name.capitalize()
        MethodNode methodNode = new MethodNode(
                "get${capitalizedName}MD5",
                ACC_PUBLIC,
                ClassHelper.STRING_TYPE,
                [] as Parameter[],
                [] as ClassNode[],
                buildMD5MethodCode(fieldNode))

        classNode.addMethod(methodNode)
    }

    BlockStatement buildMD5MethodCode(FieldNode fieldNode) {
        VariableExpression fieldVar = GeneralUtils.varX(fieldNode.name) (1)

        return macro(CompilePhase.SEMANTIC_ANALYSIS, true) {            (2)
            return java.security.MessageDigest
                    .getInstance('MD5')
                    .digest($v { fieldVar }.getBytes())                 (3)
                    .encodeHex()
                    .toString()
        }
    }
}
1 我们需要一个变量表达式的引用。
2 如果使用标准包之外的类,我们应该添加任何必要的导入或使用限定名。当使用给定静态方法的限定名时,您需要确保它在正确的编译阶段解析。在这个特殊情况下,我们指示宏在 SEMANTIC_ANALYSIS 阶段解析它,这是第一个包含类型信息的编译阶段。
3 为了替换宏内的任何 expression,我们需要使用 $v 方法。$v 接收一个闭包作为参数,并且该闭包只允许替换表达式,这意味着继承 org.codehaus.groovy.ast.expr.Expression 的类。
MacroClass

如前所述,macro 方法只能生成 statementsexpressions。但是,如果我们想生成其他类型的节点,例如方法、字段等等,该怎么办?

org.codehaus.groovy.macro.transform.MacroClass 可以用来在我们的转换中创建**类**(ClassNode 实例),就像我们之前使用 macro 方法创建语句和表达式一样。

下一个例子是一个局部转换 @Statistics。当应用于给定类时,它将添加两个方法 getMethodCount()getFieldCount(),分别返回类中方法的数量和字段的数量。这是标记注解。

@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.TYPE])
@GroovyASTTransformationClass(["metaprogramming.StatisticsASTTransformation"])
@interface Statistics {}

和 AST 转换

@CompileStatic
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class StatisticsASTTransformation extends AbstractASTTransformation {

    @Override
    void visit(ASTNode[] nodes, SourceUnit source) {
        ClassNode classNode = (ClassNode) nodes[1]
        ClassNode templateClass = buildTemplateClass(classNode)  (1)

        templateClass.methods.each { MethodNode node ->          (2)
            classNode.addMethod(node)
        }
    }

    @CompileDynamic
    ClassNode buildTemplateClass(ClassNode reference) {          (3)
        def methodCount = constX(reference.methods.size())       (4)
        def fieldCount = constX(reference.fields.size())         (5)

        return new MacroClass() {
            class Statistics {
                java.lang.Integer getMethodCount() {             (6)
                    return $v { methodCount }
                }

                java.lang.Integer getFieldCount() {              (7)
                    return $v { fieldCount }
                }
            }
        }
    }
}
1 创建模板类
2 将模板类方法添加到注解类
3 传递引用类
4 提取引用类方法计数表达式。
5 提取引用类字段计数表达式。
6 使用引用的方法计数表达式构建 getMethodCount() 方法
7 使用引用的字段计数表达式构建 getFieldCount() 方法

基本上,我们创建了 Statistics 类作为模板,以避免编写低级 AST API,然后我们将模板类中创建的方法复制到它们的最终目的地。

MacroClass 实现中的类型应在内部解析,这就是为什么我们必须编写 java.lang.Integer 而不是简单地编写 Integer 的原因。
请注意,我们正在使用 @CompileDynamic。那是因为我们使用 MacroClass 的方式就像我们实际实现它一样。因此,如果您使用 @CompileStatic,它会抱怨,因为抽象类的实现不能是另一个不同的类。
@Macro 方法

您已经看到,通过使用 macro 可以节省大量工作,但您可能会想,这个方法从何而来。您没有声明它或静态导入它。您可以将其视为一个特殊的全局方法(或者,如果您愿意,可以将其视为每个 Object 上的方法)。这与 println 扩展方法的定义方式非常相似。但与 println 不同的是,macro 扩展是在编译过程的早期完成的,而 println 成为在编译过程后期选择执行的方法。将 macro 声明为早期扩展的可用方法之一是通过使用 @Macro 注解 macro 方法定义,并使用与扩展模块类似机制使该方法可用。此类方法称为方法,好消息是您可以定义自己的宏方法。

要定义您自己的宏方法,请以与扩展模块类似的方式创建一个类,并添加一个方法,例如:

public class ExampleMacroMethods {

    @Macro
    public static Expression safe(MacroContext macroContext, MethodCallExpression callExpression) {
        return ternaryX(
                notNullX(callExpression.getObjectExpression()),
                callExpression,
                constX(null)
        );
    }
    ...
}

现在,您将使用 META-INF/groovy 目录中的 org.codehaus.groovy.runtime.ExtensionModule 文件将其注册为扩展模块。

现在,假设类和元信息文件都在您的类路径上,您可以使用以下方式使用宏方法:

def nullObject = null
assert null == safe(safe(nullObject.hashcode()).toString())

2.2.6. 测试 AST 转换

分离源树

本节介绍测试AST转换的最佳实践。前面几节强调了为了执行AST转换,它必须预编译。这听起来很明显,但很多人都被它困扰,试图在定义AST转换的同一个源目录中使用它。

因此,测试AST转换的第一个技巧是将测试源与转换的源分开。同样,这仅仅是最佳实践,但您必须确保您的构建也确实将它们单独编译。对于Apache MavenGradle来说,这都是默认情况。

调试AST转换

能够在AST转换中设置断点,以便您可以在IDE中调试代码,这非常方便。但是,您可能会惊讶地发现IDE没有停在断点处。原因其实很简单:如果您的IDE使用Groovy编译器编译AST转换的单元测试,那么编译是从IDE触发的,但是编译文件的进程没有调试选项。只有当测试用例执行时,虚拟机的调试选项才会被设置。简而言之:为时已晚,类已经编译,并且您的转换也已经应用。

一个非常简单的解决方法是使用提供assertScript方法的GroovyTestCase类。这意味着,您应该这样写,而不是在测试用例中这样写

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类将以调试活动状态编译,并且断点将被命中。

ASTMatcher

有时您可能希望对AST节点进行断言;可能是为了过滤节点,或者为了确保给定的转换已构建预期的AST节点。

过滤节点

例如,如果您只想将给定的转换应用于特定的AST节点集,您可以使用**ASTMatcher**来过滤这些节点。以下示例展示了如何将给定表达式转换为另一个表达式。使用**ASTMatcher**,它查找特定表达式1 + 1并将其转换为3。这就是我们称之为@Joking示例的原因。

首先我们创建只能应用于方法的@Joking注解

@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.METHOD])
@GroovyASTTransformationClass(["metaprogramming.JokingASTTransformation"])
@interface Joking { }

然后是转换,它只将org.codehaus.groovy.ast.ClassCodeExpressionTransformer的一个实例应用于方法代码块中的所有表达式。

@CompileStatic
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class JokingASTTransformation extends AbstractASTTransformation {
    @Override
    void visit(ASTNode[] nodes, SourceUnit source) {
        MethodNode methodNode = (MethodNode) nodes[1]

        methodNode
            .getCode()
            .visit(new ConvertOnePlusOneToThree(source))  (1)
    }
}
1 获取方法的代码语句并应用表达式转换器

这时就使用**ASTMatcher**将转换仅应用于与表达式1 + 1匹配的表达式。

class ConvertOnePlusOneToThree extends ClassCodeExpressionTransformer {
    SourceUnit sourceUnit

    ConvertOnePlusOneToThree(SourceUnit sourceUnit) {
        this.sourceUnit = sourceUnit
    }

    @Override
    Expression transform(Expression exp) {
        Expression ref = macro { 1 + 1 }     (1)

        if (ASTMatcher.matches(ref, exp)) {  (2)
            return macro { 3 }               (3)
        }

        return super.transform(exp)
    }
}
1 构建用作参考模式的表达式
2 检查当前评估的表达式是否与参考表达式匹配
3 如果匹配,则将当前表达式替换为使用macro构建的表达式

然后您可以按如下方式测试实现

package metaprogramming

class Something {
    @Joking
    Integer getResult() {
        return 1 + 1
    }
}

assert new Something().result == 3

单元测试 AST 转换

通常我们测试 AST 转换只是检查转换的最终使用是否符合我们的预期。但是如果能有一种简单的方法来检查,例如,转换添加的节点是否从一开始就符合我们的预期,那就太好了。

以下转换将一个新方法giveMeTwo添加到带注解的类中。

@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class TwiceASTTransformation extends AbstractASTTransformation {

    static final String VAR_X = 'x'

    @Override
    void visit(ASTNode[] nodes, SourceUnit source) {
        ClassNode classNode = (ClassNode) nodes[1]
        MethodNode giveMeTwo = getTemplateClass(sumExpression)
            .getDeclaredMethods('giveMeTwo')
            .first()

        classNode.addMethod(giveMeTwo)                  (1)
    }

    BinaryExpression getSumExpression() {               (2)
        return macro {
            $v{ varX(VAR_X) } +
            $v{ varX(VAR_X) }
        }
    }

    ClassNode getTemplateClass(Expression expression) { (3)
        return new MacroClass() {
            class Template {
                java.lang.Integer giveMeTwo(java.lang.Integer x) {
                    return $v { expression }
                }
            }
        }
    }
}
1 将方法添加到带注解的类中
2 构建一个二进制表达式。二进制表达式在+标记的两侧使用相同的变量表达式(请检查org.codehaus.groovy.ast.tool.GeneralUtils中的varX方法)。
3 构建一个新的ClassNode,其中包含一个名为giveMeTwo的方法,该方法返回作为参数传入的表达式的结果。

现在,不再创建对给定示例代码执行转换的测试。我希望检查二进制表达式的构建是否正确完成。

void testTestingSumExpression() {
    use(ASTMatcher) {                 (1)
        TwiceASTTransformation sample = new TwiceASTTransformation()
        Expression referenceNode = macro {
            a + a                     (2)
        }.withConstraints {           (3)
            placeholder 'a'           (4)
        }

        assert sample
            .sumExpression
            .matches(referenceNode)   (5)
    }
}
1 将 ASTMatcher 作为类别使用
2 构建一个模板节点
3 对该模板节点应用一些约束
4 告诉编译器a是一个占位符。
5 断言参考节点和当前节点相等

当然,您总是可以/应该检查实际执行情况

void testASTBehavior() {
    assertScript '''
    package metaprogramming

    @Twice
    class AAA {

    }

    assert new AAA().giveMeTwo(1) == 2
    '''
}
ASTTest

最后但同样重要的是,测试AST转换还包括测试**编译期间**AST的状态。Groovy为此提供了一个名为@ASTTest的工具:它是一个注解,可以让你在抽象语法树上添加断言。更多详情请查阅ASTTest文档

2.2.7. 外部参考

如果您对编写AST转换的分步教程感兴趣,可以关注这个工作坊