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 对象,它实现了 groovy.lang.GroovyInterceptable 接口,并具有方法拦截功能,这将在 GroovyInterceptable 部分讨论。

对于每个方法调用,Groovy 检查该对象是 POJO 还是 POGO。对于 POJO,Groovy 从 groovy.lang.MetaClassRegistry 中获取其 MetaClass,并将方法调用委托给它。对于 POGO,Groovy 会采取更多步骤,如下图所示

GroovyInterceptions
图 1. Groovy 拦截机制

1.1. GroovyObject 接口

groovy.lang.GroovyObject 是 Groovy 中的主要接口,就像 Object 类在 Java 中一样。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 将请求转发给所有属性的 getter,除了 field3

可以通过重写 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 定义,它接受一个额外的值参数

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. 静态 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. 静态 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 的功能,称为“类别”。

类别通过所谓的“类别类”实现。类别类很特别,因为它需要满足某些预定义规则才能定义扩展方法。

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

类别类默认情况下不会启用。要使用类别类中定义的方法,需要应用 GDK 提供的范围内的 use 方法,该方法可从每个 Groovy 对象实例内部访问。

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

    def someDate = new Date()       (2)
    println someDate - 3.months
}
1 TimeCategory 为 Integer 添加方法。
2 TimeCategory 为 Date 添加方法。

use 方法将类别类作为第一个参数,将闭包代码块作为第二个参数。在 Closure 内部,可以访问类别方法。如上面的示例所示,即使是 JDK 类(如 java.lang.Integer 或 java.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 的自定义实现。通常,您会想要扩展现有的元类之一,例如 MetaClassImpl、DelegatingMetaClass、ExpandoMetaClass 或 ProxyMetaClass;否则,您需要实现完整的方法查找逻辑。在使用新的元类实例之前,您应该调用 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 的元类,只需将一个名为 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 将位于类路径中,因此它将成为 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 不执行继承。要启用此功能,您必须在应用程序启动之前调用 ExpandoMetaClass#enableGlobally(),例如在 main 方法或 servlet 引导程序中。

以下部分将详细介绍如何在各种场景中使用 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 带有各种编解码器实现,每个实现都定义在一个单独的类中。在运行时,应用程序类路径中将存在多个编解码器类。在应用程序启动时,框架会向某些元类添加 encodeXXX 和 decodeXXX 方法,其中 XXX 是编解码器类名的第一部分(例如 encodeHTML)。这种机制在下面的某些 Groovy 伪代码中显示出来。

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

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


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

assert '<html><body>hello</body></html>' == html.encodeAsHTML()
运行时发现

在运行时,了解在方法执行时存在的其他方法或属性通常很有用。ExpandoMetaClass 提供以下方法(截至本文撰写时)

  • getMetaMethod

  • hasMetaMethod

  • getMetaProperty

  • hasMetaProperty

为什么不能直接使用反射?因为 Groovy 与众不同,它拥有 "真实" 方法和仅在运行时可用的方法。这些方法有时(但并非总是)表示为 MetaMethods。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')

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

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

您可能会注意到,扩展方法是在辅助类(定义了各种扩展方法)中使用静态方法定义的。getText 方法的第一个参数对应于接收方,而其他参数对应于扩展方法的参数。因此,这里定义了一个名为 getText 的方法,它在 File 类上(因为第一个参数是 File 类型),它接收一个参数作为参数(编码 String)。

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

  • 编写如上所示的扩展类

  • 编写模块描述符文件

然后,您必须使扩展模块对 Groovy 可见,这与在类路径上提供扩展模块类和描述符一样简单。这意味着您可以选择

  • 直接在类路径上提供类和模块描述符

  • 或者将您的扩展模块捆绑到一个 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. 扩展模块和类路径

值得注意的是,您不能使用与使用它的代码同时编译的扩展。这意味着要使用扩展,它必须作为编译后的类在类路径上可用,然后使用它的代码才会被编译。通常,这意味着您不能在与扩展类本身相同的源单元中包含测试类。由于通常将测试源与普通源分开,并在构建的另一个步骤中执行它们,因此这不是问题。

1.10.6. 与类型检查的兼容性

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

2. 编译时元编程

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

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

2.1. 可用的 AST 转换

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

AST 转换可以分为两类

  • 全局 AST 转换是在找到它们时在编译类路径上透明地、全局地应用

  • 本地 AST 转换是通过使用标记注释源代码来应用的。与全局 AST 转换不同,本地 AST 转换可能支持参数。

Groovy 没有附带任何全局 AST 转换,但您可以找到以下列表,其中包含可在代码中使用的本地 AST 转换

2.1.1. 代码生成转换

此类别中的转换包括帮助删除样板代码的 AST 转换。这通常是您必须编写的代码,但它不包含任何有用的信息。通过自动生成此样板代码,您必须编写的代码变得简洁明了,并且由于样板代码不正确而引入错误的可能性降低了。

@groovy.transform.ToString

@ToString AST 转换会生成类的易于阅读的 toString 表示形式。例如,像下面这样注释 Person 类将自动为您生成 toString 方法

import groovy.transform.ToString

@ToString
class Person {
    String firstName
    String lastName
}

使用此定义,然后以下断言通过,这意味着已生成一个 toString 方法,它从类中获取字段值并打印出来

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

@ToString 注解接受多个参数,以下表格总结了这些参数。

属性 默认值 描述 示例

excludes

空列表

要从 toString 中排除的属性列表

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

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

includes

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

要包含在 toString 中的字段列表

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

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

includeSuper

False

是否应将超类包含在 toString 中

@ToString
class Id { long id }

@ToString(includeSuper=true)
class Person extends Id {
    String firstName
    String lastName
}

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

includeNames

false

是否在生成的 toString 中包含属性名称。

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

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

includeFields

False

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

@ToString(includeFields=true)
class Person {
    String firstName
    String lastName
    private int age
    void test() {
       age = 42
    }
}

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

includeSuperProperties

False

是否应将超属性包含在 toString 中

class Person {
    String name
}

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

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

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

includeSuperFields

False

是否应在 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

False

是否应显示具有空值的属性/字段

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

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

includePackage

True

在 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

True

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

cache

False

缓存 toString 字符串。仅当类为不可变时,才应设置为 true。

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

def p = new Person(firstName: 'Jack', lastName:'Nicholson')
def s1 = p.toString()
def s2 = p.toString()
assert s1 == s2
assert s1 == 'Person(Jack, Nicholson)'
assert s1.is(s2) // same instance

allNames

False

是否应在生成的 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 的行为

属性 默认值 描述 示例

excludes

空列表

要从 equals/hashCode 中排除的属性列表

import groovy.transform.EqualsAndHashCode

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

def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
def p2 = new Person(firstName: 'Bob', lastName: 'Nicholson')

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

includes

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

要包含在 equals/hashCode 中的字段列表

import groovy.transform.EqualsAndHashCode

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

def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
def p2 = new Person(firstName: 'Bob', lastName: 'Nicholson')

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

cache

False

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

False

是否应将 super 包含在 equals 和 hashCode 计算中

import groovy.transform.EqualsAndHashCode

@EqualsAndHashCode
class Living {
    String race
}

@EqualsAndHashCode(callSuper=true)
class Person extends Living {
    String firstName
    String lastName
}

def p1 = new Person(race:'Human', firstName: 'Jack', lastName: 'Nicholson')
def p2 = new Person(race: 'Human being', firstName: 'Jack', lastName: 'Nicholson')

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

includeFields

False

除了属性之外,是否应在 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

True

是否应使 equals 调用 canEqual 帮助程序方法。

allProperties

False

是否应将 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

False

是否应将具有内部名称的字段和/或属性包含在 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')

第一个构造函数是一个无参数构造函数,只要您没有最终属性,它就允许传统的 map 风格的构造。 Groovy 调用无参数构造函数,然后在幕后调用相关的 setter。 值得注意的是,如果第一个属性(或字段)的类型为 LinkedHashMap,或者如果存在单个 Map、AbstractMap 或 HashMap 属性(或字段),那么 map 风格的命名参数将不可用。

其他构造函数是通过按定义顺序获取属性生成的。 Groovy 将根据属性(或字段,取决于选项)的数量生成多个构造函数。

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

  • 将产生一个构造函数

  • 尝试使用初始值将产生错误

  • map 风格的命名参数将不可用

此属性通常仅在另一种 Java 框架期望一个构造函数的情况下使用,例如注入框架或 JUnit 参数化运行器。

不可变性支持

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

自定义选项

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

属性 默认值 描述 示例

excludes

空列表

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

import groovy.transform.TupleConstructor

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

def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
def p2 = new Person('Jack')
try {
    // will fail because the second property is excluded
    def p3 = new Person('Jack', 'Nicholson')
} catch (e) {
    assert e.message.contains ('Could not find matching constructor')
}

includes

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

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

import groovy.transform.TupleConstructor

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

def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
def p2 = new Person('Jack')
try {
    // will fail because the second property is not included
    def p3 = new Person('Jack', 'Nicholson')
} catch (e) {
    assert e.message.contains ('Could not find matching constructor')
}

includeProperties

True

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

import groovy.transform.TupleConstructor

@TupleConstructor(includeProperties=false)
class Person {
    String firstName
    String lastName
}

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

try {
    def p2 = new Person('Jack', 'Nicholson')
} catch(e) {
    // will fail because properties are not included
}

includeFields

False

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

import groovy.transform.TupleConstructor

@TupleConstructor(includeFields=true)
class Person {
    String firstName
    String lastName
    private String occupation
    public String toString() {
        "$firstName $lastName: $occupation"
    }
}

def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson', occupation: 'Actor')
def p2 = new Person('Jack', 'Nicholson', 'Actor')

assert p1.firstName == p2.firstName
assert p1.lastName == p2.lastName
assert p1.toString() == 'Jack Nicholson: Actor'
assert p1.toString() == p2.toString()

includeSuperProperties

True

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

import groovy.transform.TupleConstructor

class Base {
    String occupation
}

@TupleConstructor(includeSuperProperties=true)
class Person extends Base {
    String firstName
    String lastName
    public String toString() {
        "$firstName $lastName: $occupation"
    }
}

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

def p2 = new Person('Actor', 'Jack', 'Nicholson')

assert p1.firstName == p2.firstName
assert p1.lastName == p2.lastName
assert p1.toString() == 'Jack Nicholson: null'
assert p2.toString() == 'Jack Nicholson: Actor'

includeSuperFields

False

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

import groovy.transform.TupleConstructor

class Base {
    protected String occupation
    public String occupation() { this.occupation }
}

@TupleConstructor(includeSuperFields=true)
class Person extends Base {
    String firstName
    String lastName
    public String toString() {
        "$firstName $lastName: ${occupation()}"
    }
}

def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson', occupation: 'Actor')

def p2 = new Person('Actor', 'Jack', 'Nicholson')

assert p1.firstName == p2.firstName
assert p1.lastName == p2.lastName
assert p1.toString() == 'Jack Nicholson: Actor'
assert p2.toString() == p1.toString()

callSuper

False

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

import groovy.transform.TupleConstructor

class Base {
    String occupation
    Base() {}
    Base(String job) { occupation = job?.toLowerCase() }
}

@TupleConstructor(includeSuperProperties = true, callSuper=true)
class Person extends Base {
    String firstName
    String lastName
    public String toString() {
        "$firstName $lastName: $occupation"
    }
}

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

def p2 = new Person('ACTOR', 'Jack', 'Nicholson')

assert p1.firstName == p2.firstName
assert p1.lastName == p2.lastName
assert p1.toString() == 'Jack Nicholson: null'
assert p2.toString() == 'Jack Nicholson: actor'

force

False

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

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

defaults

True

指示构造函数参数启用默认值处理。 设置为 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

useSetters

False

默认情况下,转换将直接从其相应的构造函数参数设置每个属性的备份字段。 将此属性设置为 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

False

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

import groovy.transform.TupleConstructor

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

def p = new Person('Jack')

assert p.$firstName == 'Jack'

allProperties

False

是否应将 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'

pre

empty

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

import groovy.transform.TupleConstructor

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

def p = new Person('Jack')

assert p.first == 'jack'

post

empty

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

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 注解旨在通过为您生成 map 构造函数来消除样板代码。 创建一个 map 构造函数,以便设置类中的每个属性,其依据是提供的 map 中的值,该值的键与属性的名称相同。 用法如以下示例所示

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

False

是否在复制时将注解从构造函数中带走

@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.CONSTRUCTOR])
public @interface ConsAnno {}

class Base {
  @ConsAnno Base() {}
}

@InheritConstructors(constructorAnnotations=true)
class Child extends Base {}

assert Child.constructors[0].annotations[0].annotationType().name == 'groovy.transform.Generated'
assert Child.constructors[0].annotations[1].annotationType().name == 'ConsAnno'

parameterAnnotations

False

在复制构造函数时,是否应将注解从构造函数参数中带走

@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.PARAMETER])
public @interface ParamAnno {}

class Base {
  Base(@ParamAnno String name) {}
}

@InheritConstructors(parameterAnnotations=true)
class Child extends Base {}

assert Child.constructors[0].parameterAnnotations[0][0].annotationType().name == 'ParamAnno'
@groovy.lang.Category

@Category AST 转换简化了 Groovy 类别的创建。 从历史上看,Groovy 类别是这样编写的

class TripleCategory {
    public static Integer triple(Integer self) {
        3*self
    }
}
use (TripleCategory) {
    assert 9 == 3.triple()
}

@Category 转换允许您使用实例风格的类来编写相同的代码,而不是使用静态类风格。 这消除了对每个方法的第一个参数都是接收者的需求。 类别可以这样编写

@Category(Integer)
class TripleCategory {
    public Integer triple() { 3*this }
}
use (TripleCategory) {
    assert 9 == 3.triple()
}

请注意,混合类可以使用 this 来引用。 值得注意的是,在类别类中使用实例字段本质上是不安全的:类别不是有状态的(类似于特征)。

@groovy.transform.IndexedProperty

@IndexedProperty 注解旨在为列表/数组类型属性生成索引 getter/setter。 这在您想从 Java 使用 Groovy 类时特别有用。 虽然 Groovy 支持使用 GPath 来访问属性,但这在 Java 中不可用。 @IndexedProperty 注解将生成以下形式的索引属性

class SomeBean {
    @IndexedProperty String[] someArray = new String[2]
    @IndexedProperty List someList = []
}

def bean = new SomeBean()
bean.setSomeArray(0, 'value')
bean.setSomeList(0, 123)

assert bean.someArray[0] == 'value'
assert bean.someList == [123]
@groovy.lang.Lazy

@Lazy AST 转换实现了字段的延迟初始化。 例如,以下代码

class SomeBean {
    @Lazy LinkedList myField
}

将生成以下代码

List $myField
List getMyField() {
    if ($myField!=null) { return $myField }
    else {
        $myField = new LinkedList()
        return $myField
    }
}

用于初始化字段的默认值是声明类型的默认构造函数。 可以通过在属性赋值的右侧使用闭包来定义默认值,如以下示例所示

class SomeBean {
    @Lazy LinkedList myField = { ['a','b','c']}()
}

在这种情况下,生成的代码如下所示

List $myField
List getMyField() {
    if ($myField!=null) { return $myField }
    else {
        $myField = { ['a','b','c']}()
        return $myField
    }
}

如果字段被声明为 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 变换用于帮助编写Comparable类,并且通常可以通过众多属性轻松排序。它易于使用,如以下示例所示,我们在其中对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注释属性中提供属性名称列表,来包含或排除生成的compareTo方法中的某些属性。如果使用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

True

是否应该使用 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

False

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

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

False

是否也应该使用超级属性(优先排序)

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 变换用于帮助编写可以使用流畅的API 调用创建的类。该变换支持多种构建策略以涵盖各种情况,并且有很多配置选项可以自定义构建过程。如果您是 AST 黑客,您还可以定义自己的策略类。下表列出了与 Groovy 捆绑在一起的可用策略以及每种策略支持的配置选项。

策略

描述

builderClassName

builderMethodName

buildMethodName

prefix

includes/excludes

includeSuperProperties

allNames

SimpleStrategy

链接的设置器

n/a

n/a

n/a

是,默认“set”

n/a

是,默认false

ExternalStrategy

显式构建器类,构建的类保持不变

n/a

n/a

是,默认“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
}

然后,只需像这里显示的那样以链接的方式调用设置器

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

对于每个属性,将创建一个生成的设置器,它看起来像这样

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)也是如此。

如果您有一个希望在构建过程中调用的设置器,可以使用注释属性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'

请注意,您提供的(通常为空)构建器类将填充有适当的设置器和构建方法。生成的构建方法将看起来类似于

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

您要为其创建构建器的类可以是任何遵循正常 JavaBean 约定的 Java 或 Groovy 类,例如无参数构造函数和属性的设置器。以下是如何使用 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'

@Builder的注释属性builderMethodNamebuilderClassName不适用于此策略。

您可以将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注释(在类、方法或构造函数位置),那么您有责任确保生成的辅助类和工厂方法具有唯一的名称(即最多只有一个可以使用默认名称值)。以下是一个突出显示方法和构造函数用法(以及说明为唯一名称所需的重命名)的示例。

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

如果您有一个希望在构建过程中调用的设置器,可以使用注释属性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进行注释,实现了接口,并且具有注释属性以指示应为任何提供的 method 抛出带有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')
    }

}

第四个示例说明了用户提供的代码的情况。我们的类使用@AutoImplement进行注释,实现了接口,具有显式覆盖的hasNext方法,以及包含为任何提供的 method 提供的代码的注释属性。以下是类的定义

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

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

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 变换的行为可以使用以下参数更改

属性 默认值 描述 示例

接口

True

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

interface Greeter { void sayHello() }
class MyGreeter implements Greeter { void sayHello() { println 'Hello!'} }

class DelegatingGreeter { // no explicit interface
    @Delegate MyGreeter greeter = new MyGreeter()
}
def greeter = new DelegatingGreeter()
assert greeter instanceof Greeter // interface was added transparently

已弃用

false

如果为 true,则还委托使用 @Deprecated 注释的方法

class WithDeprecation {
    @Deprecated
    void foo() {}
}
class WithoutDeprecation {
    @Deprecated
    void bar() {}
}
class Delegating {
    @Delegate(deprecated=true) WithDeprecation with = new WithDeprecation()
    @Delegate WithoutDeprecation without = new WithoutDeprecation()
}
def d = new Delegating()
d.foo() // passes thanks to deprecated=true
d.bar() // fails because of @Deprecated

methodAnnotations

False

是否将委托方法的注解传递到您的委托方法。

class WithAnnotations {
    @Transactional
    void method() {
    }
}
class DelegatingWithoutAnnotations {
    @Delegate WithAnnotations delegate
}
class DelegatingWithAnnotations {
    @Delegate(methodAnnotations = true) WithAnnotations delegate
}
def d1 = new DelegatingWithoutAnnotations()
def d2 = new DelegatingWithAnnotations()
assert d1.class.getDeclaredMethod('method').annotations.length==1
assert d2.class.getDeclaredMethod('method').annotations.length==2

parameterAnnotations

False

是否将委托方法参数的注解传递到您的委托方法。

class WithAnnotations {
    void method(@NotNull String str) {
    }
}
class DelegatingWithoutAnnotations {
    @Delegate WithAnnotations delegate
}
class DelegatingWithAnnotations {
    @Delegate(parameterAnnotations = true) WithAnnotations delegate
}
def d1 = new DelegatingWithoutAnnotations()
def d2 = new DelegatingWithAnnotations()
assert d1.class.getDeclaredMethod('method',String).parameterAnnotations[0].length==0
assert d2.class.getDeclaredMethod('method',String).parameterAnnotations[0].length==1

excludes

空数组

要从委托中排除的方法列表。有关更细粒度的控制,另请参阅 excludeTypes

class Worker {
    void task1() {}
    void task2() {}
}
class Delegating {
    @Delegate(excludes=['task2']) Worker worker = new Worker()
}
def d = new Delegating()
d.task1() // passes
d.task2() // fails because method is excluded

includes

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

要包含在委托中的方法列表。有关更细粒度的控制,另请参阅 includeTypes

class Worker {
    void task1() {}
    void task2() {}
}
class Delegating {
    @Delegate(includes=['task1']) Worker worker = new Worker()
}
def d = new Delegating()
d.task1() // passes
d.task2() // fails because method is not included

excludeTypes

空数组

包含要从委托中排除的方法签名的接口列表

interface AppendStringSelector {
    StringBuilder append(String str)
}
class UpperStringBuilder {
    @Delegate(excludeTypes=AppendStringSelector)
    StringBuilder sb1 = new StringBuilder()

    @Delegate(includeTypes=AppendStringSelector)
    StringBuilder sb2 = new StringBuilder()

    String toString() { sb1.toString() + sb2.toString().toUpperCase() }
}
def usb = new UpperStringBuilder()
usb.append(3.5d)
usb.append('hello')
usb.append(true)
assert usb.toString() == '3.5trueHELLO'

includeTypes

未定义的标记数组(表示默认情况下没有列表)

包含要包含在委托中的方法签名的接口列表

interface AppendBooleanSelector {
    StringBuilder append(boolean b)
}
interface AppendFloatSelector {
    StringBuilder append(float b)
}
class NumberBooleanBuilder {
    @Delegate(includeTypes=AppendBooleanSelector, interfaces=false)
    StringBuilder nums = new StringBuilder()
    @Delegate(includeTypes=[AppendFloatSelector], interfaces=false)
    StringBuilder bools = new StringBuilder()
    String result() { "${nums.toString()} ~ ${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

False

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

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,最小化可变性@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 生成的不可变类会自动变为最终的。此外,会检查每个属性的类型,并在类上进行各种检查,例如,目前不允许公开实例字段。如果需要,它还会生成一个 copyWith 构造函数。

支持以下注解属性

属性 默认值 描述 示例

copyWith

false

一个布尔值,表示是否生成 copyWith( Map ) 方法。

import groovy.transform.Immutable

@Immutable( copyWith=true )
class User {
    String  name
    Integer age
}

def bob   = new User( 'bob', 43 )
def alice = bob.copyWith( name:'alice' )
assert alice.name == 'alice'
assert alice.age  == 43
@groovy.transform.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

目前,该注解仅适用于自递归方法调用,即对同一方法的单次递归调用。如果您有一个涉及简单相互递归的场景,请考虑使用闭包和 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

已弃用。请考虑使用 traits 代替。

2.1.3. 日志改进

Groovy 提供了一系列 AST 变换,有助于集成最广泛使用的日志框架。每个常用框架都有一个变换和相关注解。这些变换提供了一种简化的声明性方法来使用日志框架。在每种情况下,变换都会

  • 向注释的类添加一个静态 final 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)的字段,但你可以通过指定 value 属性让它使用任何你想要的字段,如下面的示例所示

import groovy.transform.Synchronized

import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

class Counter {
    int cpt
    private final Object myLock = new Object()

    @Synchronized('myLock')
    int incrementAndGet() {
        cpt++
    }
    int get() {
        cpt
    }
}
@groovy.transform.WithReadLock@groovy.transform.WithWriteLock

@WithReadLock AST 转换与 @WithWriteLock 转换一起工作,使用 JDK 提供的 ReentrantReadWriteLock 功能提供读/写同步。此注释可以添加到方法或静态方法中。它将透明地创建一个 $reentrantLock final 字段(或对于静态方法为 $REENTRANTLOCK),并将添加适当的同步代码。例如,以下代码

import groovy.transform.WithReadLock
import groovy.transform.WithWriteLock

class Counters {
    public final Map<String,Integer> map = [:].withDefault { 0 }

    @WithReadLock
    int get(String id) {
        map.get(id)
    }

    @WithWriteLock
    void add(String id, int num) {
        Thread.sleep(200) // emulate long computation
        map.put(id, map.get(id)+num)
    }
}

等同于此

import groovy.transform.WithReadLock as WithReadLock
import groovy.transform.WithWriteLock as WithWriteLock

public class Counters {

    private final Map<String, Integer> map
    private final java.util.concurrent.locks.ReentrantReadWriteLock $reentrantlock

    public int get(java.lang.String id) {
        $reentrantlock.readLock().lock()
        try {
            map.get(id)
        }
        finally {
            $reentrantlock.readLock().unlock()
        }
    }

    public void add(java.lang.String id, int num) {
        $reentrantlock.writeLock().lock()
        try {
            java.lang.Thread.sleep(200)
            map.put(id, map.get(id) + num )
        }
        finally {
            $reentrantlock.writeLock().unlock()
        }
    }
}

@WithReadLock@WithWriteLock 都支持指定替代锁对象。在这种情况下,用户必须声明引用字段,如下面的替代方法所示

import groovy.transform.WithReadLock
import groovy.transform.WithWriteLock

import java.util.concurrent.locks.ReentrantReadWriteLock

class Counters {
    public final Map<String,Integer> map = [:].withDefault { 0 }
    private final ReentrantReadWriteLock customLock = new ReentrantReadWriteLock()

    @WithReadLock('customLock')
    int get(String id) {
        map.get(id)
    }

    @WithWriteLock('customLock')
    void add(String id, int num) {
        Thread.sleep(200) // emulate long computation
        map.put(id, map.get(id)+num)
    }
}

有关详细信息,

2.1.5. 更轻松的克隆和外部化

Groovy 提供了两个注释,旨在分别简化 CloneableExternalizable 接口的实现,分别命名为 @AutoClone@AutoExternalize

@groovy.transform.AutoClone

@AutoClone 注释旨在使用各种策略实现 @java.lang.Cloneable 接口,这要归功于 style 参数

  • 默认的 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 是不可变的,Objectclone() 方法会复制 String 引用。这同样适用于基本字段和 java.lang.Number 的大多数具体子类。

除了克隆样式之外,@AutoClone 还支持多种选项

属性 默认值 描述 示例

excludes

空列表

需要从克隆中排除的属性或字段名称列表。也可以使用包含逗号分隔的字段/属性名称的字符串。有关详细信息,请参阅 groovy.transform.AutoClone#excludes

import groovy.transform.AutoClone
import groovy.transform.AutoCloneStyle

@AutoClone(style=AutoCloneStyle.SIMPLE,excludes='authors')
class Book {
    String isbn
    String title
    List authors
    Date publicationDate
}

includeFields

false

默认情况下,只克隆属性。将此标志设置为 true 还将克隆字段。

import groovy.transform.AutoClone
import groovy.transform.AutoCloneStyle

@AutoClone(style=AutoCloneStyle.SIMPLE,includeFields=true)
class Book {
    String isbn
    String title
    List authors
    protected Date publicationDate
}
@groovy.transform.AutoExternalize

@AutoExternalize AST 转换将帮助创建 java.io.Externalizable 类。它将自动将接口添加到类中并生成 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 注释支持两个参数,可以让你稍微自定义它的行为

属性 默认值 描述 示例

excludes

空列表

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

import groovy.transform.AutoExternalize

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

includeFields

false

默认情况下,只外部化属性。将此标志设置为 true 还将克隆字段。

import groovy.transform.AutoExternalize

@AutoExternalize(includeFields=true)
class Book {
    String isbn
    String title
    protected float price
}

2.1.6. 更安全的脚本编写

Groovy 语言使在运行时执行用户脚本变得很容易(例如使用 groovy.lang.GroovyShell),但如何确保脚本不会占用所有 CPU(无限循环)或并发脚本不会慢慢消耗线程池中所有可用的线程?Groovy 提供了一些注释,这些注释旨在实现更安全的脚本编写,生成代码,例如允许你自动中断执行。

@groovy.transform.ThreadInterrupt

JVM 世界中的一种复杂情况是线程无法停止。Thread#stop 方法存在但已弃用(而且不可靠),因此你唯一的希望在于 Thread#interrupt。调用后者会设置线程上的 interrupt 标志,但它 **不会** 停止线程的执行。这存在问题,因为在线程中执行的代码有责任检查中断标志并正确退出。当你作为开发人员知道你正在执行的代码旨在在独立线程中运行时,这很有道理,但通常情况下你并不知道。对于用户脚本来说,情况更糟,他们甚至可能不知道什么是线程(想想 DSL)。

@ThreadInterrupt 通过在代码的关键位置添加线程中断检查来简化此操作

  • 循环 (for, while)

  • 方法的第一个指令

  • 闭包体的第一个指令

假设以下用户脚本

while (true) {
    i++
}

这是一个明显的无限循环。如果此代码在其自己的线程中执行,中断将无济于事:如果你在线程上 join,那么调用代码将能够继续,但线程仍然处于活动状态,在后台运行,你无法停止它,从而慢慢导致线程饥饿。

一种解决此问题的方法是按照以下方式设置 shell

def config = new CompilerConfiguration()
config.addCompilationCustomizers(
        new ASTTransformationCustomizer(ThreadInterrupt)
)
def binding = new Binding(i:0)
def shell = new GroovyShell(binding,config)

然后将 shell 配置为自动对所有脚本应用 @ThreadInterrupt AST 转换。这允许你以这种方式执行用户脚本

def t = Thread.start {
    shell.evaluate(userCode)
}
t.join(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方法的单个参数将是 final。尝试在构造函数或方法主体中修改这些参数将被编译器标记。

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

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

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

这里,add方法将具有 final 参数,但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 转换,而是一个与特征一起使用的标记接口。有关更多详细信息,请参阅特征文档

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

转换将根据列表的泛型类型生成相应的添加/删除方法。此外,它还将根据类中声明的公共方法创建fireXXX方法

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

public class Component {

    @ListenerList
    private List<ActionListener> listeners

    public void addActionListener(ActionListener listener) {
        if ( listener == null) {
            return
        }
        if ( listeners == null) {
            listeners = []
        }
        listeners.add(listener)
    }

    public void removeActionListener(ActionListener listener) {
        if ( listener == null) {
            return
        }
        if ( listeners == null) {
            listeners = []
        }
        listeners.remove(listener)
    }

    public ActionListener[] getActionListeners() {
        Object __result = []
        if ( listeners != null) {
            __result.addAll(listeners)
        }
        return (( __result ) as ActionListener[])
    }

    public void fireActionPerformed(ActionEvent param0) {
        if ( listeners != null) {
            ArrayList<ActionListener> __list = new ArrayList<ActionListener>(listeners)
            for (def listener : __list ) {
                listener.actionPerformed(param0)
            }
        }
    }
}

@Bindable支持多个选项,这些选项将让您进一步自定义转换的行为

属性 默认值 描述 示例

name

泛型类型名称

默认情况下,将附加到 add/remove/…方法的​​后缀是列表泛型类型的简单类名。

class Component {
    @ListenerList(name='item')
    List<ActionListener> listeners;
}

synchronize

false

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

class Component {
    @ListenerList(synchronize = true)
    List<ActionListener> listeners;
}
@groovy.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 node 指的是由 @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 由于 lookup 总是返回一个列表,因此始终需要选择要处理的元素

例如,假设你想测试 for 循环变量的声明类型。那么你可以像这样操作

import groovy.transform.ASTTest
import groovy.transform.PackageScope
import org.codehaus.groovy.ast.ClassHelper
import org.codehaus.groovy.ast.expr.DeclarationExpression
import org.codehaus.groovy.ast.stmt.ForStatement

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 那么我们想确保toStringCANONICALIZATION 中被添加
3 否则,如果toString 存在并且上下文中的变量added 为空
4 那么这意味着当前编译阶段是添加toString 的阶段

2.1.10. Grape 处理

@groovy.lang.Grapes

Grape 是嵌入到 Groovy 中的依赖管理引擎,它依赖于几个注释,这些注释在本指南部分中进行了详细描述。

2.2. 开发 AST 变换

有两种类型的变换:全局变换和局部变换。

  • 全局变换 由编译器对正在编译的代码应用,无论变换在哪里应用。实现全局变换的编译类位于添加到编译器类路径中的 JAR 文件中,并且包含服务定位器文件META-INF/services/org.codehaus.groovy.transform.ASTTransformation,其中包含一行,其中包含变换类的名称。变换类必须具有无参数构造函数并实现org.codehaus.groovy.transform.ASTTransformation 接口。它将在每个源代码的编译中运行,因此请确保不要创建以扩展和耗时的方式扫描所有 AST 的变换,以保持编译器的快速运行。

  • 局部变换 是通过注释要变换的代码元素来局部应用的变换。为此,我们重复使用注释符号,这些注释应该实现org.codehaus.groovy.transform.ASTTransformation。编译器将发现它们并将变换应用于这些代码元素。

2.2.1. 编译阶段指南

Groovy AST 变换必须在九个定义的编译阶段中的一个阶段执行(org.codehaus.groovy.control.CompilePhase)。

全局变换可以在任何阶段应用,但局部变换只能在语义分析阶段或之后应用。简而言之,编译阶段如下

  • 初始化:打开源文件并配置环境

  • 解析:使用语法生成表示源代码的令牌树

  • 转换:从令牌树创建抽象语法树 (AST)。

  • 语义分析:执行语法无法检查的一致性和有效性检查,并解析类。

  • 规范化:完成构建 AST

  • 指令选择:选择指令集,例如 Java 6 或 Java 7 字节码级别

  • 类生成:在内存中创建类的字节码

  • 输出:将二进制输出写入文件系统

  • 最终化:执行任何最后的清理

一般来说,在后续阶段可以获得更多类型信息。如果你的变换涉及读取 AST,那么信息更丰富的后续阶段可能是一个不错的选择。如果你的变换涉及写入 AST,那么树更稀疏的早期阶段可能更方便。

2.2.2. 局部变换

局部 AST 变换与它们应用的上下文相关。在大多数情况下,上下文由一个注释定义,该注释将定义变换的范围。例如,注释一个字段意味着变换应用于该字段,而注释类意味着变换应用于整个类。

作为一个简单的例子,假设你想要编写一个@WithLogging 变换,它将在方法调用开始和结束时添加控制台消息。因此,以下“Hello World”示例实际上会打印“Hello World”,以及一个开始和停止消息

简陋的方面导向编程
@WithLogging
def greet() {
    println "Hello World"
}

greet()

局部 AST 变换是一种简单的方法。它需要两件事

  • @WithLogging 注释的定义

  • org.codehaus.groovy.transform.ASTTransformation 的实现,它将日志表达式添加到方法中

ASTTransformation 是一个回调,它让你可以访问org.codehaus.groovy.control.SourceUnit,通过它你可以获得对org.codehaus.groovy.ast.ModuleNode(AST)的引用。

AST(抽象语法树)是一个树结构,它主要由org.codehaus.groovy.ast.expr.Expression(表达式)或org.codehaus.groovy.ast.expr.Statement(语句)组成。学习 AST 的一个简单方法是在调试器中探索它。一旦你有了 AST,你就可以分析它以了解有关代码的信息或重写它以添加新功能。

局部变换注释很简单。以下是@WithLogging 注释

import org.codehaus.groovy.transform.GroovyASTTransformationClass

import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target

@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.METHOD])
@GroovyASTTransformationClass(["gep.WithLoggingASTTransformation"])
public @interface WithLogging {
}

注释保留可以是SOURCE,因为你不需要在之后使用该注释。这里的元素类型是METHOD@WithLogging 因为注释适用于方法。

但最重要的部分是@GroovyASTTransformationClass 注释。它将@WithLogging 注释链接到你要编写的ASTTransformation 类。gep.WithLoggingASTTransformation 是我们将要编写的ASTTransformation 的完全限定类名。这一行将注释连接到变换。

有了它,Groovy 编译器将在源代码单元中找到@WithLogging 时调用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 参数是一个包含两个 AST 节点的数组,其中第一个是注释节点(@WithLogging),第二个是被注释的节点(方法节点)
6 创建一个语句,当我们进入方法时打印一条消息
7 创建一个语句,当我们退出方法时打印一条消息
8 获取方法体,在本例中是一个BlockStatement
9 在现有代码的第一个语句之前添加进入方法的消息
10 在现有代码的最后一个语句之后附加退出方法的消息
11 创建一个ExpressionStatement,它封装了一个对应于this.println("message")MethodCallExpression

重要的是要注意,为了简明起见,在这个例子中,我们没有进行必要的检查,例如检查被注释的节点是否真的是一个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

转换的代码看起来类似于局部情况,但我们不需要使用ASTNode[]参数,而是需要使用SourceUnit

gep/WithLoggingASTTransformation.groovy
@CompileStatic                                                                  (1)
@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS)                  (2)
class WithLoggingASTTransformation implements ASTTransformation {               (3)

    @Override
    void visit(ASTNode[] nodes, SourceUnit sourceUnit) {                        (4)
        def methods = sourceUnit.AST.methods                                    (5)
        methods.each { method ->                                                (6)
            def startMessage = createPrintlnAst("Starting $method.name")        (7)
            def endMessage = createPrintlnAst("Ending $method.name")            (8)

            def existingStatements = ((BlockStatement)method.code).statements   (9)
            existingStatements.add(0, startMessage)                             (10)
            existingStatements.add(endMessage)                                  (11)
        }
    }

    private static Statement createPrintlnAst(String message) {                 (12)
        new ExpressionStatement(
            new MethodCallExpression(
                new VariableExpression("this"),
                new ConstantExpression("println"),
                new ArgumentListExpression(
                    new ConstantExpression(message)
                )
            )
        )
    }
}
1 即使不是强制的,如果你用 Groovy 编写 AST 变换,强烈建议使用CompileStatic,因为它将提高编译器的性能。
2 使用org.codehaus.groovy.transform.GroovyASTTransformation 进行注释,以告知变换需要在哪个编译阶段运行。这里是在语义分析阶段。
3 实现ASTTransformation 接口
4 它只有一个visit 方法
5 sourceUnit参数可以访问正在编译的源代码,因此我们可以获取当前源代码的 AST,并从该文件中检索方法列表
6 我们对源文件中的每个方法进行迭代
7 创建一个语句,当我们进入方法时打印一条消息
8 创建一个语句,当我们退出方法时打印一条消息
9 获取方法体,在本例中是一个BlockStatement
10 在现有代码的第一个语句之前添加进入方法的消息
11 在现有代码的最后一个语句之后附加退出方法的消息
12 创建一个ExpressionStatement,它封装了一个对应于this.println("message")MethodCallExpression

2.2.4. AST API 指南

AbstractASTTransformation

虽然您已经看到可以直接实现ASTTransformation接口,但在几乎所有情况下,您都不会这样做,而是会扩展org.codehaus.groovy.transform.AbstractASTTransformation类。此类提供了一些实用方法,可以使 AST 转换更容易编写。Groovy 中包含的几乎所有 AST 转换都扩展了此类。

ClassCodeExpressionTransformer

能够将一个表达式转换为另一个表达式是一个常见的用例。Groovy 提供了一个类,可以轻松地做到这一点:org.codehaus.groovy.ast.ClassCodeExpressionTransformer

为了说明这一点,让我们创建一个@Shout转换,它将方法调用参数中的所有String常量转换为其大写版本。例如

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

greet()

应该打印

HELLO WORLD

然后,转换的代码可以使用ClassCodeExpressionTransformer来简化此过程

@CompileStatic
@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS)
class ShoutASTTransformation implements ASTTransformation {

    @Override
    void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
        ClassCodeExpressionTransformer trn = new ClassCodeExpressionTransformer() {         (1)
            private boolean inArgList = false
            @Override
            protected SourceUnit getSourceUnit() {
                sourceUnit                                                                  (2)
            }

            @Override
            Expression transform(final Expression exp) {
                if (exp instanceof ArgumentListExpression) {
                    inArgList = true
                } else if (inArgList &&
                    exp instanceof ConstantExpression && exp.value instanceof String) {
                    return new ConstantExpression(exp.value.toUpperCase())                  (3)
                }
                def trn = super.transform(exp)
                inArgList = false
                trn
            }
        }
        trn.visitMethod((MethodNode)nodes[1])                                               (4)
    }
}
1 在内部,转换将创建一个ClassCodeExpressionTransformer
2 转换器需要返回源代码单元
3 如果在参数列表中检测到类型为字符串的常量表达式,则将其转换为其大写版本
4 在被注解的方法上调用转换器
AST 节点
编写 AST 转换需要深入了解 Groovy 内部 API。特别是,它需要了解 AST 类。由于这些类是内部的,因此 API 在将来可能会发生变化,这意味着您的转换可能会中断。尽管有这样的警告,但 AST 一直非常稳定,这种情况很少发生。

抽象语法树的类属于org.codehaus.groovy.ast包。建议读者使用 Groovy 控制台,特别是 AST 浏览器工具,以了解这些类。学习的另一个资源是AST Builder测试套件。

2.2.5. 宏

简介

在 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 创建一个返回语句
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”的返回语句,这正是您在macro实用方法中看到的内容。您的普通代码将被自动转换为org.codehaus.groovy.ast.stmt.ReturnStatement
2 将返回语句添加到新方法中
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,当应用于给定的 String 字段时,它将添加一个返回该字段的 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 如果使用标准包之外的类,我们应该添加任何需要的导入或使用限定名称。当使用给定静态方法的限定名称时,您需要确保它在正确的编译阶段被解析。在这种特殊情况下,我们指示宏在语义分析阶段解析它,这是第一个具有类型信息的编译阶段。
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 不同的是,println 会在编译过程的后期被选择执行,而 macro 扩展是在编译过程的早期完成的。通过使用 @Macro 注解来注解一个 macro 方法的定义,并使用类似的机制来扩展模块,从而将 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 文件将其注册为扩展模块。

现在,假设该类和元信息文件位于您的类路径上,您可以以下列方式使用 macro 方法

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 触发的,但编译文件的进程没有调试选项。只有在执行测试用例时,才会在虚拟机上设置调试选项。简而言之:为时已晚,该类已经编译,您的变换已经应用。

一个非常简单的解决方法是使用 GroovyTestCase 类,它提供了一个 assertScript 方法。这意味着,您不应该在测试用例中编写以下内容

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

void testMyTransform() {
    def c = new Subject()
    c.methodToBeTested()
}

您应该编写以下内容

void testMyTransformWithBreakpoint() {
    assertScript '''
        import metaprogramming.MyTransformToDebug

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

区别在于,当您使用 assertScript 时,assertScript 块中的代码是在**执行单元测试时**编译的。也就是说,这次,Subject 类将使用调试激活进行编译,断点将被命中。

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 构建一个名为 giveMeTwo 的新 **ClassNode**,它返回作为参数传递的表达式的结果。

现在,我不想创建一个测试来在给定的示例代码上执行变换。我想检查二元表达式的构造是否正确完成

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 变换的分步教程,您可以查看 此研讨会