4. 运行时&编译时元编程 (TBD)

Groovy 提供两类元编程,分别为:运行时元编程与编译时元编程。第一种允许在运行时改变类模型,而第二种发生在编译时。其各有优劣之处,这一章节我们详细讲解。

4.1. 运行时元编程

在运行时元编程中,我们在运行时拦截,注入甚至合成类和接口的方法。为了深入理解 Groovy MOP , 我们需要理解 Groovy 的对象及方法的处理方式。Groovy 中有三类对象:POJO,POGO 和 Groovy 拦截器。Groovy 中对于以上对象均能使用元编程,但使用方式会有差别。

  • POJO - 正规的 Java 对象,其类可以用Java或任何其他 JVM 语言编写。
  • POGO - Groovy 对象,其类使用 Groovy 编写。继承于 java.lang.Object 并且实现 groovy.lang.GroovyObject 接口。
  • Groovy Interceptor - Groovy 对象,实现 groovy.lang.GroovyInterceptable 接口,具有方法拦截能力,我们将在 GroovyInterceptable 章节详细讲解。

For every method call Groovy checks whether the object is a POJO or a POGO. For POJOs, Groovy fetches it’s MetaClass from the groovy.lang.MetaClassRegistry and delegates method invocation to it. For POGOs, Groovy takes more steps, as illustrated in the following figure:

每个方法调用过程中, Groovy 检查当前对象是 POJO 还是 POGO。对于 POJOs , Groovy 将从 groovy.lang.MetaClassRegistry 中取出其 MetaClass 并使用代理方式进行方法调用。对于 POGOs,Groovy 将执行更多步骤,如下图

Groovy interception mechanism

4.1.1. GroovyObject interface

groovy.lang.GroovyObject 作为 Groovy 中的主要接口,就像 Java 中的 Object 类。 GroovyObject 的在 groovy.lang.GroovyObjectSupport 中默认实现, 其主要负责将调用传递给 MetaClass 对象。 参考下面代码:

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

4.1.1.1. invokeMethod

在运行时元编程模式下,当 Groovy 对象上调用的方法不存在时,将会调用 invokeMethod 方法。 下面的例子中,就是用到重载 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 []'

4.1.1.2. get/getProperty

对象上读取属性操作都将通过重载 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> Forwards the request to the getter for all properties except field3.

通过重载 setProperty 方法可以拦截属性修改操作:

class POGO {

    String property

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

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

assert pogo.property == 'overriden'

4.1.1.3. get/setMetaClass

You can a access a objects metaClass or set your own MetaClass implementation for changing the default interception mechanism. For example you can write your own implementation of the MetaClass interface and assign to it to objects and accordingly change the interception mechanism:

通过访问对象的 metaClass 或 重新设置你自己的 MetaClass 实现,可以改变默认的拦截机制。 下面例子中,你可以通过自己实现一个 MetaClass 并赋值给其对象上,这样就可以改变拦截机制:

// getMetaclass
someObject.metaClass

// setMetaClass
someObject.metaClass = new OwnMetaClassImplementation()

GroovyInterceptable 章节中有更多实例用于参考。

4.1.2. get/setAttribute

This functionality is related to the MetaClass implementation. In the default implementation you can access fields without invoking their getters and setters. The examples below demonstrate this approach:

这一功能与 MetaClass 的具体实现有关。在默认的实现中,你可以无需调用 fields 上的 getters 和 setters 方法,就可以通过 get/setAttribute 访问控制 fields 。 下面例子中将展示这种特性:

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'

4.1.3. methodMissing

Groovy 支持 methodMissing 概念. 当方法调用失败,方法的名称或参数不正确,都将调用 methodMissing 方法。

class Foo {

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

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

通常情况下,使用 methodMissing ,其可以缓存相同方法及参数调用结果,并提供给下次调用使用。

For example consider dynamic finders in GORM. These are implemented in terms of methodMissing. The code resembles something like this: 例如在 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 的开销,在第二次调用此方法,就和原生方法一样。

4.1.4. propertyMissing

当访问的属性不存在时,会调用 propertyMissingpropertyMissing 方法只有一个字符串类型的入参,其参数为调用的属性名称。

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

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

在运行时,当无法找到属性对应的 getter 方法的情况下,才能调用 propertyMissing(String) 方法。

对于 setter 方法, 第二个 propertyMissing 方法定义增加了一个 value 参数:

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

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

assert f.foo == "bar"

相比较于 methodMissing , 这是最好方式在运行时动态注册属性并能提升整体的查找性能。

methodMissingpropertyMissing 处理的方法和属性,都可以通过 ExpandoMetaClass 添加注册。

4.1.5. GroovyInterceptable

groovy.lang.GroovyInterceptable 是继承于 GroovyObject 的关键接口,实现此接口的类上的方法调用都将经过运行 时方法路由机制拦截。

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 对象中,也将会被拦截。

If we want to intercept all methods call but do not want to implement the GroovyInterceptable interface we can implement invokeMethod() on an object’s MetaClass. This approach works for both POGOs and POJOs, as shown by this example: 如果你想拦截所有方法,但是有不想实现 GroovyInterceptable 接口,可以通过在对象的 MetaClass 方法上实现 invokeMethod 方法来达到同样的效果。 这种方法对于 POGOsPOJOs 都适用,看看下面的例子:

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 的内容可以查看对应`章节 <http://www.groovy-lang.org/metaprogramming.html#_metaclasses>`_ 。

4.1.6. Categories

当我们对于无法控制的类,需要增加附加方法时,分类这种方式非常有用。 对于这种特性,Groovy 的实现是借鉴于 Objective-C ,并称其为分类。

分类通过称为 category 类来实现。category 类中需要根据预定规则来定义扩展方法。

这里有一些分类用于扩展相应类的功能,这种方式可以使其在 Groovy 中发挥更强的作用:

Category 类在默认情况下并不开启。当使用 category 中定义的方法时,需要使用 GDK 中提供的 use 方法。

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

    def someDate = new Date()         // <2>
    println someDate - 3.months
}

<1> TimeCategory adds methods to Integer <2> TimeCategory adds methods to Date

use 方法第一个参数为具体定义 category 类,第二个参数为闭包代码块。 在闭包块中可以访问 category 类中的方法。 正如上面例子中看到的, JDK 中 java.lang.Integerjava.util.Date 类也可以通过分类中定义的方法来增强。

分类也可以进行一定的封装,像下面这样:

class JPACategory{
  // Let's enhance JPA EntityManager without getting into the JSR committee
  static void persistAll(EntityManager em , Object[] entities) { //add an interface to save all
    entities?.each { em.persist(it) }
  }
}

def transactionContext = {
  EntityManager em, Closure c ->
  def tx = em.transaction
  try {
    tx.begin()
    use(JPACategory) {
      c()
    }
    tx.commit()
  } catch (e) {
    tx.rollback()
  } finally {
    //cleanup your resource here
  }
}

// user code, they always forget to close resource in exception, some even forget to commit, let's not rely on them.
EntityManager em; //probably injected
transactionContext (em) {
 em.persistAll(obj1, obj2, obj3)
 // let's do some logics here to make the example sensible
 em.persistAll(obj2, obj4, obj6)
}

当我们看到 groovy.time.TimeCategory 中所定义的方法都声明为 static 。 这种声明方式是通过 category 方式在 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 注解的方式,将经过注解的类在编译期转化为 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 的其他用法。

4.1.7. Metaclasses

待续 (TBD)

4.1.7.1. Custom metaclasses

待续 (TBD)

4.1.7.1.1. Delegating metaClass

待续

4.1.7.1.2. Magic package(Maksym Stavytskyi)

待续 (TBD)

4.1.7.2. Per instance metaclass

待续 (TBD)

4.1.7.3. ExpandoMetaClass

Groovy 中自带特殊的 MetaClass 称为 ExpandoMetaClass 。 特殊之处就在于,可以通过使用闭包方式动态的添加或改变方法,构造器,属性以及静态方法。

这些动态的处理,对于在 Test guide 章节中看到的 mokingstubbing 应用场景非常有用。

Groovy 对每个 java.lang.Class 都提供了一个 metaClass 特殊属性,其返回一个 ExpandoMetaClass 实例的引用。 在这实例上可以添加方法或修改已存在的方法内容。

接下来介绍 ExpandoMetaClass 在各种场景中的使用。

4.1.7.3.1. Methods

通过调用 metaClass 属性来获取 ExpandoMetaClass ,使用 <<= 操作符添加方法。 Note that the left shift operator is used to append a new method. 注意 << 用于添加新方法,如果这个方法已经存在,将会抛出异常。如果你想替换方法,可以使用 =

这些操作符向 metaClass 的不存在的属性传递 闭包代码块的实例。

class Book {
   String title
}

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

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

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

上面例子中就是通过通过访问 metaClass 并使用 <<= 操作符, 赋值闭包代码块的方式向类中添加新增方法。 无参数方法可以使用 {-> ...} 方式添加。

4.1.7.3.2. Properties

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 容器中将当前执行的 request 中的值存储到 request attributes 中。 Grails 中也有相同的应用。

4.1.7.3.3. Constructors

构造器添加使用特殊属性 constructor 。 同样适用 <<= 来传递闭包代码块。在代码执行时,闭包中定义的参数列表就为构造器的参数列表。

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'

需要当心的是在添加构造器时,会比较容易出现栈溢出的问题。

4.1.7.3.4. Static Methods

静态方法的加入也使用同样的技术,但需要在方法名称之前添加 static 限定符。

class Book {
   String title
}

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

def b = Book.create("The Stand")
4.1.7.3.5. Borrowing Methods

ExpandoMetaClass 中可以使用方法指针语法,向其他类中借取方法。

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()
4.1.7.3.6. Dynamic Method Names

Groovy 中可以使用 String 作为属性名称,同样可以允许你在运行时动态的创建方法或属性名称。 使用动态命名方法,直接使用了语言中引用属性名称作为字符串的特性。

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 中可以发现动态方法命名的应用。 动态编码器这一概念,也是通过使用动态方法命名来实现。

HTMLCodec Class

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

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

上面例子是一个编码器的实现。 Grails 中各种编码器都定义在各自独立的类中。 在运行环境的 classpath 中将有多个编译器类。 应用启动时将 encodeXXXdecodeXXX 方法添加到特定的 meta-classes 中,其中 XXX 就是编码器类名 的第一部分(例如: encodeHTML)。 这种处理机制将在下面的伪代码中展示:

 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()
4.1.7.3.7. Runtime Discovery

在方法执行时,了解存在的其他方法或属性是十分有用的。 当前版本中,ExpandoMetaClass 提供一下方法用于此法:

  • getMetaMethod
  • hasMetaMethod
  • getMetaProperty
  • hasMetaProperty

为什么不使用放射? Groovy 中的方法分为原生方法以及运行时动态方法。 这里它们通常表示为元方法(MetaMethods)。元方法可以告诉你在运行时有效的方法。

当在重载 invokeMethod , getPropertysetProperty 时这将会非常有用。

4.1.7.3.8. GroovyObject Methods

ExpandoMetaClass 的另一个特性是其允许重载 invokeMethod , getPropertysetProperty 方法,这些方法都可以在 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()

闭包代码中的第一部分用于根据给定的方法名称及参数查找对应方法。 如果方法找到,就可以对方法进行代理处理。如果没有找到,方法返回默认值。

元方法是存在于 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

The important thing to note here is that instead of a MetaMethod a MetaProperty instance is looked up. If that exists the getProperty method of the MetaProperty is called, passing the delegate.

4.1.7.3.9. Overriding Static 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 来获取静态元方法实例。

4.1.7.3.10. Extending Interfaces

使用 ExpandoMetaClass 可以在接口上添加方法。 要做到这一点,在应用启动时,需要在全局范围调用 ExpandoMetaClass.enableGlobally() 方法。

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

def list = []

list << 1
list << 2

assert 4 == list.sizeDoubled()

4.1.8. Extension modules

4.1.8.1. Extending existing classes

An extension module allows you to add new methods to existing classes, including classes which are precompiled, like classes from the JDK. Those new methods, unlike those defined through a metaclass or using a category, are available globally. For example, when you write:

扩展模块允许你在已有的类上添加新的方法,包括已经预编译的类,如 JDK 中的类。 这里新添加的方法,不同于通过 metaclass 或 category 定义的方法,其可以在全局使用。 例如:

Standard extension method

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

你需要注意的是,这个扩展方法使用 static 定义在帮助类中(其他扩展方法都在这里定义)。 getText 第一个参数对应其接受对象,其他参数对应这个扩展方法上的参数。 这样就可以在 File 类上调用 getText 方法。

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

  • 想上面编写一个扩展方法
  • 编写一个模块描述文件

然后你需要将扩展模块及描述文件配置到 classpath 中,这样在 Groovy 中就可以使用。 这意味着,你可以选择:

  • 直接将类与模块描述文件配置在 classpath
  • 或将其打包至 jar 中,方便能够重用

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

  • 实例方法(类实例上调用的方法)
  • 静态方法(类自身调用方法)

4.1.8.2. Instance methods

在类上添加实例方法,需要创建新的扩展类。 例如,你想在 Integer 上添加一个 maxRetries 方法,其方法接受一个闭包,在没有异常发生的情况下可以执行 n 次。 想实现上面的需求,你可以这样:

MaxRetriesExtension.groovy

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

<1> The extension class <2> First argument of the static method corresponds to the receiver of the message, that is to say the extended instance

声明 你的扩展类之后,你可以这样调用:

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

4.1.8.3. Static methods

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

StaticStringExtension.groovy

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

<1> The static extension class <2> First argument of the static method corresponds to the class being extended and is unused

这样你可以直接在 String 类上调用:

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

4.1.8.4. 模块描述文件 (Module descriptor)

在 Groovy 中加载扩展方法,需要声明其扩展类。 需要在 META-INF/services 目录下创建 org.codehaus.groovy.runtime.ExtensionModule 文件。

org.codehaus.groovy.runtime.ExtensionModule

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

描述文件中需要 4 个属性:

  • moduleName : 模块名称
  • moduleVersion: 模块版本号。需要注意,版本号用于检查你不会加载相同模块的不同版本。
  • extensionClasses: 扩展类列表。你可以填写多个类,使用逗号分割。
  • staticExtensionClasses: 静态方法扩展类列表。你可以填写多个类,使用逗号分割。

请注意,并不要求在一个模块上同时定义静态方法与实例方法,你可以在一个模块中添加多个类。 你也可以在一个模块中扩展多个不同的类。甚至可以在一个扩展类中,对多个类型进行扩展,但是建议能针对扩展方法进行分类。

4.1.8.5. Extension modules and classpath

It’s worth noting that you can’t use an extension which is compiled at the same time as code using it. 需要注意的是,在编译后直接通过代码调用是无法使用此扩展的。 这意味着使用扩展,需要在代码调用其之前,将其编译后的 classes 配置的到 classpath

4.1.8.6. 类型检查兼容性 (Compatibility with type checking)

与非类不同,扩展模块适用于类型检查:如果其配置在 classpath 中,类型检查器可以检查到扩展方法。也同样适用于静态编译。

4.2. 编译时元编程 (Compile-time metaprogramming)

Groovy 中的编译时元编程允许在编译时生成代码。 这种转换是在程序的修改抽象语法树(AST),在 Groovy 中我们称其为抽象语法树转换(AST transformations)。 抽象语法树转换允许你在编译过程中修改 AST 并继续执行编译过程来生成字节码。 相比较于运行时元编程,其优点在于,任何变化在 class 文件中都是可见的(字节码中可见)。 在字节码中可见是很重要的,例如,如果你变化类的一部分(实现接口,扩展抽象类,等等)或者你需要 Java 来调用你的类。 抽象语法树转换可以在 class 中添加方法。在使用运行时元编程中,新方法只能在 Groovy 被发现调用。 如果相同方法使用编译时元编程,这个方法在 Java 中同样可见。 不仅如此,编译时元编程也带来了更好的性能表现(这个过程中无需初始化阶段)。

这一章节,我们开始讲解当前版本 Groovy 中的各种编译时转换。 在随后的章节中,我们也会介绍如何 实现抽象语法树 以及这种技术的缺点。

4.2.1. Available AST transformations

抽象语法树转换分为两种类别:

  • global AST transformations are applied transparently, globally, as soon as they are found on compile classpath
  • local AST transformations are applied by annotating the source code with markers. Unlike global AST transformations, local AST transformations may support parameters.

Groovy doesn’t ship with any global AST transformation, but you can find a list of local AST transformations available for you to use in your code here:

4.2.1.1. 代码生成转换(Code generation transformations)

这种类别转换包括抽象语法树转换,能帮助去除代码样板。 这些代码都是些你必须写,但又不附带任何有用信息。 通过自动化生成代码模版,接下来需要写的代码将更加整洁,这样引入错误的可能性也会降低。

4.2.1.1.1. @groovy.transform.ToString

@ToString 抽象语法树转换,生成可读性更强 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 可以接收的参数:

(TBD)

4.2.1.1.2. @groovy.transform.EqualsAndHashCode

The @EqualsAndHashCode AST transformation aims at generating equals and hashCode methods for you. @EqualsAndHashCode 用于生成 equalshashcode 方法。 生成 hashcode 方法依照 Effective Java by Josh Bloch 中描述的做法:

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 的行为:

(TBD)
4.2.1.1.3. @groovy.transform.TupleConstructor

The @TupleConstructor annotation aims at eliminating boilerplate code by generating constructors for you. A tuple constructor is created for each property, with default values (using the Java default values). For example, the following code will generate 3 constructors: @TupleConstructor 注解通过生成构造函数消除样板代码。使用其属性创建元构造函数,如果不填写则使用默认值。 例如,下面代码中生成的 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')

The first constructor is a no-arg constructor which allows the traditional map-style construction. 第一个构造函数是一个无参数构造函数,其使用习惯的 map-style 构造。 It is worth noting that if the first property (or field) has type LinkedHashMap or if there is a single Map, AbstractMap or HashMap property (or field), then the map-style mapping is not available. 这里值得一提的是,当第一个参数是 LinkedHashMap 类型,或只有唯一一个 MapAbstractMapHashMap 参数,那么 map-style 参数设置方式将会无效。

这些构造函数生成,按照其参数定义顺序。根据其属性数量, Groovy 将生成相应的构造函数。

@TupleConstructor 接受多种配置选择:

(TBD)

4.2.1.1.4. @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 接受多种配置选择:

(TBD)

4.2.1.1.5. @groovy.transform.InheritConstructors

@InheritConstructor 用于生成超类的构造方法。在重载异常类时将特别有用:

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 接受多种配置选择:

(TBD)

4.2.1.1.6. @groovy.lang.Category

@Category 用于简化 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()
}

Note that the mixed in class can be referenced using this instead. It’s also worth noting that using instance fields in a category class is inherently unsafe: categories are not stateful (like traits).

4.2.1.1.7. @groovy.transform.IndexedProperty

@IndexedProperty 注解用于生成 list/array 类型中属性索引的 getters/setters. 如果你希望在 Java 中使用 Groovy 类,这就会特别有用。 Groovy 中支持 GPath 访问属性,但是在 Java 是不支持的。@IndexedProperty 可以生成下面属性索引方法:

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

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

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

@Lazy 用于延迟初始化属性。例如,接下来的代码:

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

If the field is declared volatile then initialization will be synchronized using the double-checked locking pattern. 如果属性声明为 volatile ,则在初始化时使用 double-checked locking 模式进行同步处理。

使用 sofe=true 参数设置,这里的属性将为使用软引用,提供了一个简单的实现方式。 在这种情况下,如果 GC 开始回收引用,初始化将在下次访问 field 时开始执行。

4.2.1.1.9. @groovy.lang.Newify

@Newify 用于使用可替换的语法来构建对象:

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

设置 flag 为 false 可以禁止 Ruby 语法。

4.2.1.1.10. @groovy.transform.Sortable

The @Sortable AST transformation is used to help write classes that are Comparable and easily sorted by numerous properties. It is easy to use as shown in the following example where we annotate the Person class:

@Sortable 用于编写可比较类,通过多个属性进行排序。接下来的代码中在 Person 类上注解使用:

import groovy.transform.Sortable

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

生成的类中有如下属性:

  • 实现 Comparable 接口
  • 其中 compareTo 方法按照 first , last , born 属性顺序实现
  • 有三个比较方法:comparatorByFirst, comparatorByLastcomparatorByBorn

生成 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 可以用于任何需要比较的应用中,类似下面场景:

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
}

这里 Person 中将包含 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']
4.2.1.1.11. @groovy.transform.builder.Builder

@Builder 用于帮助创建流式 API. 这种转换支持多种构建策略,针对不同的案例;并可以根据一系列配置选项,自定义构建过程。 甚至可以定义你自己的构建策略。 下表中列举了, Groovy 中支持的策略及其策略中支持的配置选项:

Strategy Description builderClassName builderMethodName buildMethodName prefix includes/excludes
SimpleStrategy chained setters n/a n/a n/a yes default ‘set’ yes
ExternalStrategy explicit builder class, class being built untouched n/a n/a yes, default ‘build’ yes, default ‘’ yes
DefaultStrategy creates a nested helper class yes, default <TypeName>Builder yes, default ‘builder’ yes, default ‘build’ yes, default ‘’ yes
InitializerStrategy creates a nested helper class providing type-safe fluent creation yes, default <TypeName>Initializer yes, default ‘createInitializer’ yes, default ‘create’ but usually only used internally yes, default ‘’ yes
4.2.1.1.11.1. SimpleStrategy

下面例子中使用 SimpleStrategy :

import groovy.transform.builder.*

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

然后,可以通过链式调用 setters :

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

对于每一个属性都会生成类似如下的 setter 方法:

public Person setFirst(java.lang.String first) {
    this.first = first
    return this
}

你可以向下面这样指定一个前缀:

import groovy.transform.builder.*

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

这里会看到,调用链式 setters 会是这种样式:

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

你可以将 SimpleStrategy 与 @Canonical 结合使用,主要可以用于 includesexcludes 特定的属性。 Groovy 内建的构建机制,如果可以满足你的需求,也可以不急于使用 @Builder 方式:

def p2 = new Person(first: 'Keira', last: 'Knightley', born: 1985)
def p3 = new Person().with {
    first = 'Geoffrey'
    last = 'Rush'
    born = 1951
}
4.2.1.1.11.2. ExternalStrategy

创建一个 builder 类,使用 @Builder 注解,指定 ExternalStrategy 及 forClass . 假设你需要构建下面这个类:

class Person {
    String first
    String last
    int born
}

明确指定创建及使用的 builder 类:

import groovy.transform.builder.*

@Builder(builderStrategy=ExternalStrategy, forClass=Person)
class PersonBuilder { }

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

这里需要注意,编写的 builder 类(通常此类都是空的)将会自动生成合适的 build 方法及 setters 方法。 生成的 build 方法如下:

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

对于 Java 或 Groovy 中的普通 JavaBean (无参构造函数及属性 setter 方法) 都可以为其创建 builder. 下面的例子就是对于 Java 类来创建 builder :

import groovy.transform.builder.*

@Builder(builderStrategy=ExternalStrategy, forClass=javax.swing.DefaultButtonModel)
class ButtonModelBuilder {}

def model = new ButtonModelBuilder().enabled(true).pressed(true).armed(true).rollover(true).selected(true).build()
assert model.isArmed()
assert model.isPressed()
assert model.isEnabled()
assert model.isSelected()
assert model.isRollover()

这里可以使用 prefixincludesexcludes 以及 buildMethodName 注解属性来自定化 builder。 下面例子中将说明这些自定义的使用方式:

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 中的 buildMethodNamebuilderClassName 并不适用这种模式。

你可以将 ExternalStrategy 与 @Canonical 结合使用,主要可以用于 includesexcludes 特定的属性。

4.2.1.1.11.3. DefaultStrategy

下面代码中将展示如何使用 DefaultStrategy:

import groovy.transform.builder.Builder

@Builder
class Person {
    String firstName
    String lastName
    int age
}

def person = Person.builder().firstName("Robert").lastName("Lewandowski").age(21).build()
assert person.firstName == "Robert"
assert person.lastName == "Lewandowski"
assert person.age == 21

如果你想,可以通过使用 builderClassName , buildMethodName, builderMethodName, prefix, includesexcludes 注解属性自定构建过程的各个方面,下面的例子中将使用其中的一部分:

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 属性不适用于这种模式。

4.2.1.1.11.4. InitializerStrategy

InitializerStrategy 的使用方式:

import groovy.transform.builder.*
import groovy.transform.*

@ToString
@Builder(builderStrategy=InitializerStrategy)
class Person {
    String firstName
    String lastName
    int age
}

Your class will be locked down to have a single public constructor taking a “fully set” initializer. It will also have a factory method to create the initializer. These are used as follows:

source
@CompileStatic
    def firstLastAge() {
    assert new Person(Person.createInitializer().firstName("John").lastName("Smith").age(21)).toString() == 'Person(John, Smith, 21)'
}
firstLastAge()

使用初始化方式,如果设置属性不完整将出现编译错误。如果你不需要严格控制,你可以不使用 @Compilation

You can use the InitializerStrategy in conjunction with @Canonical and @Immutable. 你可以将 InitializerStrategy 结合 @Canonical@Immutable 使用。 这样可以通过使用 includesexcludes 明确特定的属性的使用。

下面代码使用 @Builder 结合 @Immutable:

import groovy.transform.builder.*
import groovy.transform.*

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

@CompileStatic
def createFirstLastBorn() {
  def p = new Person(Person.createInitializer().first('Johnny').last('Depp').born(1963))
  assert "$p.first $p.last $p.born" == 'Johnny Depp 1963'
}

createFirstLastBorn()

这种模式下还支持注解静态方法及构造器。 在这种情况下,静态方法或构造方法的参数将成为构建对象的属性,静态方法的返回类型为目标构建类型。 如果在一个类中使用了多个 @Builder , 你需要确保生成的辅助类或工厂方法的名字不会重复

forClass 属性不适用于这种模式。

4.2.1.2. Class design annotations

使用这类注解声明方式,简化设计模式(代理,单例等等)的实现方式。

4.2.1.2.1. @groovy.lang.Delegate

@Delegate 用于实现代理模式:

class Event {
    @Delegate Date when
    String title
}

The when field is annotated with @Delegate, meaning that the Event class will delegate calls to Date methods to the when field. In this case, the generated code looks like this: when 使用 @Delegate 注解,表示 Event 将代理 Date 的方法至 when 实例上 ,在这种情况下,将生成如下代码:

class Event {
    Date when
    String title
    boolean before(Date other) {
        when.before(other)
    }
    // ...
}

@Delegate 配置参数: (TBD)

4.2.1.2.2. @groovy.transform.Immutable

@Immutable 用于创建不可变类,其成员均为不可变。在这里你只需要向下面这样注解对应的类即可:

import groovy.transform.Immutable

@Immutable
class Point {
    int x
    int y
}

Immutable classes generated with @Immutable are automatically made final. 使用 @Immutable 注解的类会自动构建为 final . 对于不可变类,需要保证其成员属性的不可变性,其必须为不可变类型(原始类型或其包装类),已知不可变类型或声明为 @Immutable 类。 @Immutable 与 @Canonical 相似之处在于,前者也会自动生成 toString, equalshashCode 方法,但是如果修改其属性将会抛出 ReadOnlyPropertyException 异常。

@Immutable 依赖于一系列的不可变类型(例如 java.net.URIjava.lang.String),如果需要使用的类型不在其中,@Immutable 将无法使用。 但是通过下面的配置方式,可以指定类型为不可变: (TBD)

4.2.1.2.3. @groovy.transform.Memoized

@Memoized 用于简化实现方法调用结果缓存,让我看看下面的代码:

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

这里模拟耗时计算,在不使用 @Memoized 情况下,方法的每次调用都将耗费几秒的时间:

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

添加 @Memoized 将改变方法的执行过程,并添加缓存:

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

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

以下两个可选参数用于配置缓存的大小:

  • protectedCacheSize: GC 清理后,保护的结果数量大小
  • maxCacheSize: 内存中保存的结果数量上限

默认情况下,缓存结果数量不受限制也不受保护。

4.2.1.2.4. @groovy.lang.Singleton

@Singleton 用于实现单例模式。这种情况下,数据域使用双重检查:

@Singleton
class GreetingService {
    String greeting(String name) { "Hello, $name!" }
}
assert GreetingService.instance.greeting('Bob') == 'Hello, Bob!'

默认情况下,单例在类初始化时创建,并通过 instance 属性调用。 这里也可以通过配置改变,调用单例的名称:

@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 , 可以允许自定义构造函数。

4.2.1.2.5. @groovy.transform.Mixin

弃用,使用 traits 替代。

4.2.1.3. Logging improvements

Groovy 支持集成一些广泛使用的日志框架。添加注释并不会阻止你在 classpath 中添加合适的日志框架。

所有的转换工作都基本相同:

  • add static final log field corresponding to the logger
  • wrap all calls to log.level() into the appropriate log.isLevelEnabled guard, depending on the underlying framework

Those transformations support two parameters:

  • value (default log) corresponds to the name of the logger field
  • category (defaults to the class name) is the name of the logger category
4.2.1.3.1. @groovy.util.logging.Log

@Log 注解依赖于 JDK logging 框架:

@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!'
    }
}
4.2.1.3.2. @groovy.util.logging.Commons

Groovy supports the Apache Commons Logging framework using to the @Commons annotation. Writing: @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!'
    }
}
4.2.1.3.3. @groovy.util.logging.Log4j

@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!'
    }
}
4.2.1.3.4. @groovy.util.logging.Log4j2

@Log4j2 用于支持 ` Apache Log4j 2.x <http://logging.apache.org/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!'
    }
}
4.2.1.3.5. @groovy.util.logging.Slf4j

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

4.2.1.4. Declarative concurrency

Groovy 中提供了一系列注解,用于简化常见的并发模式。

4.2.1.4.1. @groovy.transform.Synchronized

@Synchronized 与关键字 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 的成员变量,也可以指定你期望的变量用于加锁:

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
    }
}
4.2.1.4.2. @groovy.transform.WithReadLock and @groovy.transform.WithWriteLock

@WithReadLock 与 @WithWriteLock 共同提供读写锁。其可以用于方法或静态方法,使用使用将创建 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 and @WithWriteLock 都支持替换锁对象。这种情况下,锁对象的引用必须用户自己声明:

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

import java.util.concurrent.locks.ReentrantReadWriteLock

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

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

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

更多细节可以参考 groovy.transform.WithReadLock groovy.transform.WithWriteLock

4.2.1.5. Easier cloning and externalizing

Groovy 中提供 @AutoClone@AutoExternalize 分别用于实现 ClonableExternalizable 接口。

4.2.1.5.1. @groovy.transform.AutoClone

@AutoClone 用于实现 @java.lang.Cloneable 接口:

  • the default AutoCloneStyle.CLONE strategy calls super.clone() first then clone() on each cloneable property
  • the AutoCloneStyle.SIMPLE strategy uses a regular constructor call and copies properties from the source to the clone
  • the AutoCloneStyle.COPY_CONSTRUCTOR strategy creates and uses a copy constructor
  • the AutoCloneStyle.SERIALIZATION strategy uses serialization (or externalization) to clone the object

Each of those strategies have pros and cons which are discussed in the Javadoc for groovy.transform.AutoClone and groovy.transform.AutoCloneStyle . 以上这些模式各的优缺点将在 groovy.transform.AutoClonegroovy.transform.AutoCloneStyle 中讨论。

看看下面的例子:

@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 为不可变类型,clone 方法将会拷贝 String 的引用。 对于原始类型或 java.lang.Number 子类型都是相同的处理方式。

除了拷贝的样式, AutoClone 还支持多种配置选项:

4.2.1.5.2. @groovy.transform.AutoExternalize

@AutoExternalize 用于辅助创建 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 支持两个参数,用于自定义配置: (TBD)

4.2.1.6. Safer scripting

Groovy 中在运行时可以非常容易的使用用户的脚本(如使用 groovy.lang.GroovyShell)。 你如何确保脚本不会十分消耗 CPU 或不会慢慢将线程池中的线程消耗殆尽? Groovy 中提供了一些注解用于保障脚本的安全,例如生成代码允许自动中断执行过程。

4.2.1.6.1. @groovy.transform.ThreadInterrupt

JVM 中一个复杂的情况就是线程不能被终止。 其中 Thread#stop 方法虽然存在,但是已经废弃(不可用),你只能依赖方法 Thread#interrupt. 调用后者将会在线程上设置一个中断标志,但是并不会终止线程执行。 线程中执行的代码有责任检查中断标志并正确退出。 This makes sense when you, as a developer, know that the code you are executing is meant to be run in an independent thread, but in general, you don’t know it. It’s even worse with user scripts, who might not even know what a thread is (think of DSLs).

@ThreadInterrupt 简化了线程代码中的中断检查:

  • loops (for, while)
  • first instruction of a method
  • first instruction of a closure body

Let’s imagine the following user script:

while (true) {
    i++
}

This is an obvious infinite loop. 这是一个明显的无限循环。 如果这段代码在它自身的线程中执行,将无法中断:如果你 join 这个线程,调用代码将继续执行,线程也将持续存活在后台运行,无法终止,并慢慢引起线程饥饿。 一种解决方法:

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

这样配置将 @ThreadInterrupt 用于所有脚本中。可以运行你这样执行脚本:

def t = Thread.start {
    shell.evaluate(userCode)
}
t.join(500) // give at most 500ms 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++
}

这种检查方式引入内容循环保护,如果中断标志设置到当前线程上,将抛出异常,中断线程执行。

@ThreadInterrupt 支持多种配置选项用于自定义其转换方式: (TBD)

4.2.1.6.2. @groovy.transform.TimedInterrupt

@TimedInterrupt 用于处理与 @groovy.transform.ThreadInterrupt 略微不同的问题。 替代检查线程中断标志的方式,其用于检查当前线程执行的时间,如果时间过长,将抛出异常。

这个注解中并没有生成监控线程。 它的工作原理与 @ThreadInterrupt 类似,也是在代码中合适的位置设置检查点。 这意味着,如果在线程中有 IO 阻塞,也不会引起中断。

看看下面代码:

def fib(int n) { n<2?n:fib(n-1)+fib(n-2) }
result = fib(600)

这里是还未优化的 Fibonacci 数列计算实现。 如果 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(value=1, unit=TimeUnit.SECONDS)
class MyClass {
    def fib(int n) {
        n<2?n:fib(n-1)+fib(n-2)
    }
}

@TimedInterrupt 支持多种配置选项用于自定义其转换方式:

(TBD)

@TimedInterrupt 当前还无法兼容静态方法。

4.2.1.6.3. @groovy.transform.ConditionalInterrupt

这最后一个注解可以自定义模式中断脚本。 尤其是在,你希望管理资源(限制调用 API 的次数)可以选择这个注解。 下面例子中,用户代码使用无线循环,@ConditionalInterrupt 允许检查配额管理器并动态中断脚本:

@ConditionalInterrupt({Quotas.disallow('user')})
class UserCode {
    void doSomething() {
        int i=0
        while (true) {
            println "Consuming resources ${++i}"
        }
    }
}

The quota checking is very basic here, but it can be any code:

class Quotas {
    static def quotas = [:].withDefault { 10 }
    static boolean disallow(String userName) {
        println "Checking quota for $userName"
        (quotas[userName]--)<0
    }
}

We can make sure @ConditionalInterrupt works properly using this test code:

assert Quotas.quotas['user'] == 10
def t = Thread.start {
    new UserCode().doSomething()
}
t.join(5000)
assert !t.alive
assert Quotas.quotas['user'] < 0

Of course, in practice, it is unlikely that @ConditionalInterrupt will be itself added by hand on user code. It can be injected in a similar manner as the example shown in the ThreadInterrupt section, using the 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 支持多种配置选项用于自定义其转换方式:

(TBD)

4.2.1.6.4. Compiler directives