鍍金池/ 教程/ Java/ 運(yùn)行時(shí)及編譯時(shí)元編程(end)
Grape 依賴(lài)管理器
與 Java 的區(qū)別
語(yǔ)法風(fēng)格指南
Groovy 開(kāi)發(fā)工具包
領(lǐng)域?qū)S谜Z(yǔ)言
安全更新
Groovy 與應(yīng)用的集成
運(yùn)行時(shí)及編譯時(shí)元編程(end)
測(cè)試指南
安裝 Groovy
設(shè)計(jì)模式
Groovy 的下載

運(yùn)行時(shí)及編譯時(shí)元編程(end)

Groovy 支持兩種元編程:運(yùn)行時(shí)元編程和編譯時(shí)元編程。第一種方式允許在運(yùn)行時(shí)改變類(lèi)模式和程序行為,第二種方式則只發(fā)生在編譯時(shí)。兩種方式都有一定的優(yōu)缺點(diǎn),下面就來(lái)詳細(xì)介紹一下它們。

1. 運(yùn)行時(shí)元編程

運(yùn)行時(shí)元編程,可以將一些決策(諸如解析、注入甚至合成類(lèi)和接口的方法)推遲到運(yùn)行時(shí)來(lái)完成。為了深入了解 Groovy 的 MOP,我們需要理解 Groovy 的對(duì)象以及 Groovy 處理方法。在 Groovy 中,我們主要與三類(lèi)對(duì)象打交道:POJO、POGO,還有 Groovy 攔截器。Groovy 的元編程支持所有類(lèi)型的對(duì)象,但是它們采用的方式卻各不相同。

  • POJO —— 普通的 Java 對(duì)象,它的類(lèi)可以用 Java 或其他任何 JVM 上的語(yǔ)言來(lái)編寫(xiě)。
  • POGO —— Groovy 對(duì)象,它的類(lèi)使用 Groovy 編寫(xiě)而成,繼承自 java.lang.Object 且默認(rèn)實(shí)現(xiàn)了 groovy.lang.GroovyObject 接口。
  • Groovy 攔截器 —— 實(shí)現(xiàn)了 groovy.lang.GroovyInterceptable 接口的 Groovy 對(duì)象,并具有方法攔截功能。稍后將在 GroovyInterceptable 一節(jié)中詳細(xì)介紹。

每當(dāng)調(diào)用一個(gè)方法時(shí),Groovy 會(huì)判斷該方法是 POJO 還是 POGO。對(duì)于 POJO 對(duì)象,Groovy 會(huì)從 groovy.lang.MetaClassRegistry 讀取它的 MetaClass,并委托方法調(diào)用;對(duì)于 POGO 對(duì)象,Groovy 將要采取更多的執(zhí)行步驟,如下圖所示:

http://wiki.jikexueyuan.com/project/groovy-introduction/images/GroovyInterceptions.png" alt="Groovy攔截機(jī)制" />

圖 1 Groovy 攔截機(jī)制

1.1 GroovyObject 接口

groovy.lang.GroovyObject 是 Groovy 中的關(guān)鍵接口,地位類(lèi)似于 Java 中的 Object 類(lèi)。在 groovy.lang.GroovyObjectSupport 類(lèi)中有一個(gè) GroovyObject 的默認(rèn)實(shí)現(xiàn),負(fù)責(zé)將調(diào)用傳輸給 groovy.lang.MetaClass 對(duì)象。GroovyObject 源看起來(lái)如下所示:

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

根據(jù)運(yùn)行時(shí)元編程的Schema,當(dāng)你所調(diào)用的方法沒(méi)有在 Groovy 對(duì)象中提供的時(shí)候,調(diào)用該方法。下面這個(gè)例子中,使用了一個(gè)重寫(xiě)的 invokeMethod() 方法:

class SomeGroovyClass {

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

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

def someGroovyClass = new SomeGroovyClass()

assert someGroovyClass.test() == 'method exists'
assert someGroovyClass.someMethod() == 'called invokeMethod someMethod []'

1.1.2 getPropertysetProperty

每次對(duì)屬性的讀取都可以通過(guò)重寫(xiě)當(dāng)前對(duì)象的 getProperty() 來(lái)攔截,下面是一個(gè)簡(jiǎn)單的例子:


class SomeGroovyClass {

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

    def getField1() {
        return 'getHa'
    }

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

def someGroovyClass = new SomeGroovyClass()

assert someGroovyClass.field1 == 'getHa'   
assert someGroovyClass.field2 == 'ho'  
assert someGroovyClass.field3 == 'field3'  
assert someGroovyClass.field4 == 'hu'   

1.1.3 getMetaClasssetMetaClass

可以訪(fǎng)問(wèn)一個(gè)對(duì)象 metaClass ,或者自定義 MetaClass 實(shí)現(xiàn)來(lái)改變默認(rèn)的攔截機(jī)制。比如,你可以自己編寫(xiě) MetaClass 接口的實(shí)現(xiàn),并將它賦予對(duì)象,從而改變攔截機(jī)制。

// getMetaclass
someObject.metaClass

// setMetaClass
someObject.metaClass = new OwnMetaClassImplementation()

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

1.2 get/setAttribute

該功能與 MetaClass 實(shí)現(xiàn)有關(guān)。在默認(rèn)的實(shí)現(xiàn)中,可以不用調(diào)用 getter 與 setter 而訪(fǎng)問(wèn)字段。下列例子就反映了這種方法。

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 的不同之處在于:只有當(dāng)方法分派失敗,找不到指定名稱(chēng)或帶有指定實(shí)參的方法時(shí),才會(huì)調(diào)用該方法。

class Foo {

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

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

通常,在使用 methodMissing 時(shí),可能會(huì)將結(jié)果緩存起來(lái),以備下次調(diào)用同樣方法時(shí)使用。

比如像下面這樣在 GORM 類(lèi)中的動(dòng)態(tài)查找器。它們是根據(jù) methodMissing 來(lái)實(shí)現(xiàn)的:

class GORM {

   def dynamicMethods = [...] // 一些利用正則表達(dá)式的動(dòng)態(tài)方法  

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

注意,假如找到一個(gè)調(diào)用的方法,就會(huì)立刻使用 ExpandoMetaClass 動(dòng)態(tài)地注冊(cè)一個(gè)新方法。這樣當(dāng)下次調(diào)用同一方法時(shí)就會(huì)更方便。使用 methodMissing,并不會(huì)產(chǎn)生像調(diào)用 invokeMethod 那么大的開(kāi)銷(xiāo),第二次調(diào)用代價(jià)也并不昂貴。

1.4 propertyMissing

Groovy 支持 propertyMissing 的概念,用來(lái)攔截失敗的屬性解析嘗試。對(duì) getter 方法而言,propertyMissing 接受一個(gè)包含屬性名的 String 參數(shù):

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

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

當(dāng) Groovy 運(yùn)行時(shí)無(wú)法找到指定屬性的 getter 方法時(shí),才會(huì)調(diào)用 propertyMissing(String) 方法。

對(duì)于 setter 方法,可以添加第二個(gè) propertyMissing 定義來(lái)接收一個(gè)附加值參數(shù)。

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"

對(duì)于 methodMissing 來(lái)說(shuō),最佳實(shí)踐應(yīng)該是在運(yùn)行時(shí)動(dòng)態(tài)注冊(cè)新屬性,從而改善總體的查找性能。

另外,處理靜態(tài)方法和屬性的 methodMissingpropertyMissing 方法可以通過(guò) ExpandoMetaClass 來(lái)添加。

1.5 GroovyInterceptable

groovy.lang.GroovyInterceptable 接口是一種標(biāo)記接口,繼承自超接口 GroovyObject,用于通知 Groovy 運(yùn)行時(shí)通過(guò)方法分派器機(jī)制時(shí)應(yīng)攔截的方法。

package groovy.lang;

public interface GroovyInterceptable extends GroovyObject {
}

當(dāng) Groovy 對(duì)象實(shí)現(xiàn)了 GroovyInterceptable 接口時(shí),它的 invokeMethod() 方法就會(huì)在任何方法調(diào)用時(shí)調(diào)用。

下面就列舉一個(gè)這種類(lèi)型的方法:

class Interception implements GroovyInterceptable {

    def definedMethod() { }

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

下面這段代碼測(cè)試顯示,無(wú)論方法是否存在,調(diào)用方法都將返回同樣的值。

class InterceptableTest extends GroovyTestCase {

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

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

我們不能使用默認(rèn)的 Groovy 方法(比如 println),因?yàn)檫@些方法已經(jīng)被注入到了 Groovy 所有的對(duì)象中,自然會(huì)被攔截。

如果想要攔截所有的方法調(diào)用,但又不想實(shí)現(xiàn) GroovyInterceptable 這個(gè)接口,那么我們可以在一個(gè)對(duì)象的 MetaClass 上實(shí)現(xiàn) invokeMethod()。該方法同時(shí)適于 POGO 與 POJO 對(duì)象,如下所示:

class InterceptionThroughMetaClassTest extends GroovyTestCase {

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

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

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

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

參看MetaClasses 一節(jié)內(nèi)容了解 MetaClass 的更多內(nèi)容。

1.6 類(lèi)別(Categories)

如果一個(gè)不受控制的類(lèi)有額外的方法,在某些情況下反而是有用的。為了實(shí)現(xiàn)這種功能,Groovy 從 Objective-C 那里借用并實(shí)現(xiàn)了一個(gè)概念,叫做:類(lèi)別Categories)。

類(lèi)別功能是利用類(lèi)別類(lèi)category classes)來(lái)實(shí)現(xiàn)的。類(lèi)別類(lèi)的特殊之處在于,需要遵循特定的預(yù)定義規(guī)則才能定義擴(kuò)展方法。

系統(tǒng)已經(jīng)包括了一些類(lèi)別,可以為類(lèi)添加相應(yīng)功能,從而使它們?cè)?Groovy 環(huán)境中更為實(shí)用。

類(lèi)別類(lèi)默認(rèn)是不能啟用的。要想使用定義在類(lèi)別類(lèi)中的方法,必須要使用 GDK 所提供的 use 范圍方法,并且可用于每一個(gè) Groovy 對(duì)象實(shí)例內(nèi)部。

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

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

1?? TimeCategoryInteger 添加了方法
2?? TimeCategoryDate 添加了方法

use 方法將類(lèi)別類(lèi)作為第一個(gè)形式參數(shù),將一個(gè)閉包代碼段作為第二個(gè)形式參數(shù)。在 Category 中,可以訪(fǎng)問(wèn)類(lèi)別的任何方法。如上述代碼所示,甚至 JDK 的 java.lang.Integerjava.util.Date 類(lèi)都可以通過(guò)用戶(hù)定義方法來(lái)豐富與增強(qiáng)。

類(lèi)別不需要直接暴露給用戶(hù)代碼,如下所示:

class JPACategory{
  // 下面讓我們無(wú)需通過(guò) JSR 委員會(huì)的支持來(lái)增強(qiáng)JPA EntityManager   
  static void persistAll(EntityManager em , Object[] entities) { //添加一個(gè)接口保存所有   
    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 {
    //清除所有資源   
  }
}

// 用戶(hù)代碼。他們經(jīng)常會(huì)在出現(xiàn)異常時(shí)忘記關(guān)閉資源,有些甚至?xí)浱峤?,所以不能指望他們?EntityManager em; //probably injected
transactionContext (em) {
 em.persistAll(obj1, obj2, obj3)
 // 在這里制定一些邏輯代碼,使范例更合理。  
 em.persistAll(obj2, obj4, obj6)
}

通過(guò)查看 groovy.time.TimeCategory 類(lèi),我們就會(huì)發(fā)現(xiàn),擴(kuò)展方法都聲明為 static 方法。實(shí)際上,要想使類(lèi)別類(lèi)的方法能成功地添加到 use 代碼段內(nèi)的類(lèi)中,這是類(lèi)別類(lèi)必須滿(mǎn)足的條件之一。

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

    // ...

另外一個(gè)必備條件是靜態(tài)方法的第一個(gè)實(shí)參必須定義方法一旦被啟用時(shí),該方法所連接的類(lèi)型;而另一個(gè)實(shí)參則是常見(jiàn)的方法用于形參的實(shí)參。

由于形參和靜態(tài)方法的規(guī)范,類(lèi)別方法定義可能會(huì)比普通方法定義稍微不太直觀。因此,作為替代方案,Groovy 引入了 @Category 標(biāo)記,利用這一標(biāo)記,可在編譯時(shí)將標(biāo)注的類(lèi)轉(zhuǎn)化為類(lèi)別類(lèi)。

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 標(biāo)記的優(yōu)點(diǎn)在于,在使用實(shí)例方法時(shí),可以不需要把目標(biāo)類(lèi)別當(dāng)做第一個(gè)形參。目標(biāo)類(lèi)別類(lèi)作為實(shí)參提供給標(biāo)記使用。

關(guān)于 @Category 的另外介紹,可參看 編譯時(shí)元編程

1.7 MetaClasses

(待定)

1.7.1 自定義 metaclass 類(lèi)

(待定)

授權(quán) metaclass

(待定)

魔法包(Magic package)

(待定)

1.7.2 每個(gè)實(shí)例的 metaclass

(待定)

1.7.3 ExpandoMetaClass

Groovy 提供了一種叫做 ExpandoMetaClass 的特殊 MetaClass。其特殊之處在于,它允許可以使用靈活的閉包語(yǔ)法來(lái)動(dòng)態(tài)添加或改變方法、構(gòu)造函數(shù)、屬性,甚至靜態(tài)方法。

對(duì)于測(cè)試向?qū)?/a>中所展示的模擬或存根情況,使用這些修改會(huì)特別有用。

每一個(gè) Groovy 所提供的 java.lang.Class 都帶有一個(gè)特殊的 metaClass 屬性,它將提供一個(gè) ExpandoMetaClass 實(shí)例的引用。該實(shí)例可用于添加方法或改變已有方法的行為。

默認(rèn)情況下,ExpandoMetaClass 不支持繼承。為了啟用繼承,必須在應(yīng)用程序開(kāi)始運(yùn)作前(比如在 main 方法或 servlet bootstrap 中)就調(diào)用 ExpandoMetaClass#enableGlobally()

下面這些內(nèi)容詳細(xì)介紹了 ExpandoMetaClass 在不同情況下的應(yīng)用。

方法

一旦通過(guò)調(diào)用 metaClass 屬性訪(fǎng)問(wèn)了 ExpandoMetaClass,就可以通過(guò)左移(<<)或等于號(hào)(=)操作符來(lái)添加方法。

注意,左移操作符是用于追加append)一個(gè)新的方法。如果方法已經(jīng)存在,則會(huì)拋出一個(gè)異常。如果需要替代replace)一個(gè)方法,則需要使用 = 操作符。

下例展示了操作符是如何應(yīng)用于 metaClass 的一個(gè)不存在的屬性上,從而傳入 Closure 代碼塊的一個(gè)實(shí)例的。

class Book {
   String title
}

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

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

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

上例顯示,通過(guò)訪(fǎng)問(wèn) metaClass 屬性,可將一個(gè)新方法添加到一個(gè)類(lèi)上,并可使用 <<= 操作符來(lái)指定一個(gè) Closure 代碼塊。Closure 形參被解析為方法形參。形參方法可以通過(guò) {→ …?} 格式來(lái)添加。

屬性

ExpandoMetaClass 支持兩種方式來(lái)添加或重寫(xiě)屬性。

首先,只需通過(guò)為 metaClass 賦予一個(gè)值,就可以聲明一個(gè)可變屬性mutable property):

class Book {
   String title
}

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

assert "Stephen King" == b.author

另一個(gè)方式是,通過(guò)使用添加實(shí)例方法的標(biāo)準(zhǔn)機(jī)制來(lái)添加 getter 和(或)setter 方法:

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

def b = new Book()

assert "Stephen King" == b.author

在上述源代碼實(shí)例中,屬性由閉包所指定,并且是一個(gè)只讀屬性。添加一個(gè)相等的 setter 方法也是可行的,但屬性值需要存儲(chǔ)起來(lái)以備后續(xù)使用。這種做法可以參照下面的例子:

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

但這并不是唯一的辦法。比如在一個(gè) servlet 容器中,將當(dāng)前執(zhí)行請(qǐng)求中的值當(dāng)作請(qǐng)求屬性保存起來(lái)(就像 Grails 中的某些情況一樣)。

構(gòu)造函數(shù)

構(gòu)造函數(shù)可以通過(guò)特殊的 constructor 屬性來(lái)添加。<<= 操作符都可以用于指定 Closure 代碼段。當(dāng)代碼在運(yùn)行時(shí)執(zhí)行時(shí),Closure 實(shí)參會(huì)成為構(gòu)造函數(shù)的實(shí)參。

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'

但在添加構(gòu)造函數(shù)時(shí)要格外注意,因?yàn)檫@極易造成棧溢出。

靜態(tài)方法

添加靜態(tài)方法的方法與添加實(shí)例方法基本一樣,只不過(guò)要在方法名前加上 static 修飾符。

class Book {
   String title
}

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

def b = Book.create("The Stand")
借用方法

利用 ExpandoMetaClass,可以使用 Groovy 方法點(diǎn)標(biāo)記法從其他類(lèi)中借用方法。

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()
動(dòng)態(tài)方法名

在 Groovy 中,既然可以使用字符串作為屬性名,那么反過(guò)來(lái),也可以在運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建方法與屬性名。要想創(chuàng)建具有動(dòng)態(tài)名稱(chēng)的方法,只需使用將字符串引用為屬性名的語(yǔ)言特性。

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

同樣的概念可以應(yīng)用于靜態(tài)方法與屬性。

Grails Web 應(yīng)用框架可以算是動(dòng)態(tài)方法名的一個(gè)應(yīng)用范例?!皠?dòng)態(tài)編解碼器”的概念正是通過(guò)動(dòng)態(tài)方法名來(lái)實(shí)現(xiàn)的。

HTMLCodec 類(lèi)

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

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

上例實(shí)現(xiàn)了一個(gè)編解碼器。Grails 提供了多種編解碼器實(shí)現(xiàn),每種實(shí)現(xiàn)都定義在一個(gè)類(lèi)中。在運(yùn)行時(shí),會(huì)在應(yīng)用類(lèi)路徑上出現(xiàn)多個(gè)編解碼器類(lèi)。在應(yīng)用啟動(dòng)時(shí),框架會(huì)將 encodeXXXdecodeXXX 方法添加到特定的元類(lèi)中,這里的 XXX 是指編解碼器類(lèi)名的前面部分(如 encodeHTML)。下面采用了一些 Groovy 偽碼來(lái)表示這種機(jī)制:

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()
運(yùn)行時(shí)發(fā)現(xiàn)

在運(yùn)行時(shí)階段執(zhí)行某個(gè)方法時(shí),還有其他什么方法或?qū)傩源嬖??這個(gè)問(wèn)題往往是非常有用的。ExpandoMetaClass 提供了下列方法(截止目前):

  • getMetaMethod
  • hasMetaMethod
  • getMetaProperty
  • hasMetaProperty

為什么不能單純使用反射呢?因?yàn)?Groovy 的獨(dú)特性——它包含兩種方法,一種是“真正”的方法,而另一種則是只在運(yùn)行時(shí)才能獲取并使用的方法。后者有時(shí)(但也并不總是被)稱(chēng)為元方法(MetaMethods)。元方法告訴我們?cè)谶\(yùn)行時(shí)究竟能夠使用何種方法,從而使代碼能夠適應(yīng)。

這一點(diǎn)特別適用于重寫(xiě) invokeMethod、getProperty 和/或 setProperty 時(shí)。

GroovyObject 方法

ExpandoMetaClass 的另一個(gè)特性是能夠允許重寫(xiě) invokeMethod、getPropertysetPropertygroovy.lang.GroovyObject 類(lèi)中能找到這三個(gè)方法。

下面范例展示了如何重寫(xiě) 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()

重寫(xiě)靜態(tài)方法的邏輯跟之前我們見(jiàn)過(guò)的重寫(xiě)實(shí)例方法的邏輯基本相同,唯一不同之處在于對(duì) metaClass.static 屬性的訪(fǎng)問(wèn),以及為了獲取靜態(tài) MetaMethod 實(shí)例而對(duì) getStaticMethodName 的調(diào)用。

重寫(xiě)靜態(tài) invokeMethod

ExpandoMetaClass 甚至可以允許利用一種特殊的 invokeMethod 格式重寫(xiě)靜態(tài)方法。

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()
擴(kuò)展接口

可以利用 ExpandoMetaClass 為接口添加方法,但要想這樣做,必須在應(yīng)用啟動(dòng)前使用 ExpandoMetaClass.enableGlobally() 方法實(shí)施全局啟用。

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

def list = []

list << 1
list << 2

assert 4 == list.sizeDoubled()  

1.8 擴(kuò)展模塊

1.8.1 擴(kuò)展現(xiàn)有類(lèi)

利用擴(kuò)展模塊,可以為現(xiàn)有類(lèi)添加新方法,這些類(lèi)中可以包括 JDK 中那樣的預(yù)編譯類(lèi)。這些新方法與通過(guò)元類(lèi)或類(lèi)別定義的方法不同,它們是全局可用的。比如當(dāng)你編寫(xiě):

標(biāo)準(zhǔn)擴(kuò)展方法

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

File 類(lèi)中并不存在 getText 方法,但 Groovy 知道它的定義是在一個(gè)特殊類(lèi)中 ResourceGroovyMethods

ResourceGroovyMethods.java

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

你可能還注意到擴(kuò)展方法是在“輔助”類(lèi)(定義了多種擴(kuò)展方法)中通過(guò)一個(gè)靜態(tài)方法來(lái)定義的。getText 的第一個(gè)實(shí)參對(duì)應(yīng)著接受者,而另一個(gè)形參則對(duì)應(yīng)著擴(kuò)展方法的實(shí)參。因此,我們才在 File 類(lèi)(因?yàn)榈谝粋€(gè)實(shí)參是 File 類(lèi)型)中定義了一個(gè)名為 getText 的方法,它只傳遞了一個(gè)實(shí)參(String類(lèi)型的編碼)。

創(chuàng)建擴(kuò)展模塊的過(guò)程非常簡(jiǎn)單:

  • 如上例般編寫(xiě)擴(kuò)展類(lèi);
  • 編寫(xiě)模塊描述符文件。

然后,還必須讓 Groovy 能找到該擴(kuò)展模塊,這只需將擴(kuò)展模塊類(lèi)和描述符放入類(lèi)路徑即可。這意味著有以下兩種方法:

  • 直接在類(lèi)路徑上提供類(lèi)和模塊描述符。
  • 將擴(kuò)展模塊打包為 jar 文件,便于重用。

擴(kuò)展模塊可以為類(lèi)添加兩種方法:

  • 實(shí)例方法(類(lèi)實(shí)例上調(diào)用)
  • 靜態(tài)方法(僅供類(lèi)自身調(diào)用)

1.8.2 實(shí)例方法

為現(xiàn)有類(lèi)添加實(shí)例方法,需要?jiǎng)?chuàng)建一個(gè)擴(kuò)展類(lèi)。比如想在 Integer 上加一個(gè) maxRetries 方法,可以采取下面的方式:

MaxRetriesExtension.groovy

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

1?? 擴(kuò)展類(lèi)
2?? 靜態(tài)方法的第一個(gè)實(shí)際參數(shù)對(duì)應(yīng)著消息的接受者,也就是擴(kuò)展實(shí)例。

然后,在已經(jīng)聲明了擴(kuò)展類(lèi)之后,你可以這樣調(diào)用它:

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

1.8.3 靜態(tài)方法

也可以為類(lèi)添加靜態(tài)方法。這種情況下,靜態(tài)方法需要在自己的文件中定義。

StaticStringExtension.groovy

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

1?? 靜態(tài)擴(kuò)展類(lèi) 2?? 靜態(tài)方法的第一個(gè)實(shí)參對(duì)應(yīng)著將要擴(kuò)展并且還未使用的類(lèi)

在這種情況下,可以直接在 String 類(lèi)中調(diào)用它:

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

1.8.4 模塊描述符

為了使 Groovy 能夠加載擴(kuò)展方法,你必須聲明擴(kuò)展輔助類(lèi)。必須在 META-INF/services 目錄中創(chuàng)建一個(gè)名為 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 個(gè)鍵:

  • moduleName:模塊名稱(chēng)
  • moduleVersion:模塊版本。注意,版本號(hào)只能用于檢查是否將同一個(gè)模塊加載了兩種不同的版本。
  • extensionClasses:實(shí)例方法的擴(kuò)展輔助類(lèi)列表??梢蕴峁讉€(gè)類(lèi),但要用逗號(hào)分隔它們。
  • staticExtensionClasses:靜態(tài)方法的擴(kuò)展輔助類(lèi)列表??梢蕴峁讉€(gè)類(lèi),也要用逗號(hào)分隔它們。

注意,模塊并不一定要既能定義靜態(tài)輔助類(lèi),又能定義實(shí)例輔助類(lèi)。你可以在一個(gè)模塊中添加幾個(gè)類(lèi),也可以在單一模塊中擴(kuò)展不同的類(lèi),甚至還可以在單一的擴(kuò)展類(lèi)中使用不同的類(lèi),但強(qiáng)烈建議將擴(kuò)展方法按功能集分入不同的類(lèi)。

1.8.5 擴(kuò)展模塊和類(lèi)路徑

值得注意的是,不能在代碼使用已編譯擴(kuò)展模塊的時(shí)候,你無(wú)法使用它。這意味著,要想使用擴(kuò)展模塊,在將要使用它的代碼被編譯前,它就必須以已編譯類(lèi)的形式出現(xiàn)在類(lèi)路徑上。這其實(shí)就是說(shuō),與擴(kuò)展類(lèi)同一源單位中不能出現(xiàn)測(cè)試類(lèi)(test class),然而,測(cè)試源通常在實(shí)際中與常規(guī)源是分開(kāi)的,在構(gòu)建的另一個(gè)步驟中執(zhí)行,所以這根本不會(huì)造成任何不良影響。

1.8.6 類(lèi)型檢查的兼容性

與類(lèi)別不同的是,擴(kuò)展模塊與類(lèi)型檢查是兼容的:如果在類(lèi)路徑上存在這些模塊,類(lèi)檢查器就會(huì)知道擴(kuò)展方法,并不會(huì)說(shuō)明調(diào)用的時(shí)間。它們與靜態(tài)編譯也是兼容的。

2 編譯時(shí)元編程

在 Groovy 中,編譯時(shí)元編程能夠容許編譯時(shí)生成代碼。這種轉(zhuǎn)換會(huì)影響程序的抽象語(yǔ)法樹(shù)(AST,Abstract Syntax Tree),這也就是我們?cè)?Groovy 中把它成為 AST 轉(zhuǎn)換的原因。AST 轉(zhuǎn)換能使我們實(shí)時(shí)了解編譯過(guò)程,繼而修改 AST,從而繼續(xù)編譯過(guò)程,生成常規(guī)的字節(jié)碼。與運(yùn)行時(shí)元編程相比,在類(lèi)文件自身中(也就是說(shuō),在字節(jié)碼內(nèi))就可以看到變化。這一點(diǎn)是非常重要的,比如說(shuō)當(dāng)你想讓轉(zhuǎn)換成為類(lèi)抽象一部分時(shí)(實(shí)現(xiàn)接口,繼承抽象類(lèi),等等),或者甚至當(dāng)需要讓類(lèi)可從 Java (或其他的 JVM 語(yǔ)言)中調(diào)用時(shí)。例如,AST 轉(zhuǎn)換可以為一個(gè)類(lèi)添加一些方法。如果用運(yùn)行時(shí)元編程來(lái)實(shí)現(xiàn)的話(huà),新方法只能可見(jiàn)于 Groovy;而用編譯時(shí)元編程來(lái)實(shí)現(xiàn),新方法也可以在 Java 中顯現(xiàn)出來(lái)。最后一點(diǎn)也同樣重要,編譯時(shí)元編程的性能要好過(guò)運(yùn)行時(shí)元編程(因?yàn)椴辉傩枰跏蓟^(guò)程)。

本節(jié)中,我們將要探討與 Groovy 分發(fā)版所綁定的各種編譯時(shí)轉(zhuǎn)換,而在隨后的一節(jié)中,再來(lái)介紹如何實(shí)現(xiàn)自定義的 AST 轉(zhuǎn)換,以及這一技術(shù)的優(yōu)點(diǎn)。

2.1 可用的 AST 轉(zhuǎn)換

Groovy 有很多可用的 AST 轉(zhuǎn)換,它們可滿(mǎn)足不同的需求:減少樣板文件(代碼生成),實(shí)現(xiàn)設(shè)計(jì)模式(委托等模式),記錄日志,聲明并發(fā),克隆,更安全地記錄腳本,編譯微調(diào),實(shí)現(xiàn) Swing 模式,測(cè)試并最終管理各種依賴(lài)。如果發(fā)現(xiàn)沒(méi)有任何一個(gè)轉(zhuǎn)換能夠滿(mǎn)足特定需求,還可以自定義轉(zhuǎn)換,詳情請(qǐng)看:開(kāi)發(fā)自定義 AST 轉(zhuǎn)換。

AST 轉(zhuǎn)換可分為兩大類(lèi):

  • 全局 AST 轉(zhuǎn)換。它們的應(yīng)用是透明的,具有全局性,只要能在類(lèi)路徑上找到它們,就可以使用它們。
  • 本地 AST 轉(zhuǎn)換。利用標(biāo)記來(lái)注解源代碼。與全局 AST 轉(zhuǎn)換不同,本地 AST 轉(zhuǎn)換可能支持形式參數(shù)。

Groovy 并不帶有任何的全局 AST 轉(zhuǎn)換,但你可以在這里找到一些可用的本地 AST 轉(zhuǎn)換:

2.1.1 代碼生成轉(zhuǎn)換

這一類(lèi)轉(zhuǎn)換包含能夠去除樣板文件代碼的 AST 轉(zhuǎn)換。樣板文件代碼通常是一種必須編寫(xiě)然而又沒(méi)有任何有用信息的代碼。通過(guò)自動(dòng)生成這種樣板文件代碼,剩下必須要寫(xiě)的代碼就變得清晰而簡(jiǎn)潔起來(lái),從而就減少了因?yàn)闃影逦募a不正確而引入的錯(cuò)誤。

@groovy.transform.ToString

@ToString AST 轉(zhuǎn)換能夠生成人類(lèi)可讀的類(lèi)的 toString 形式。比如,像下面這樣注解 Person 類(lèi)會(huì)自動(dòng)為你生成 toString 方法。

import groovy.transform.ToString

@ToString
class Person {
    String firstName
    String lastName
}

根據(jù)這種定義,下列斷言就得以通過(guò),意味著已經(jīng)生成了一個(gè) toString 方法,它會(huì)從類(lèi)中獲取字段值,并將它們打印出來(lái)。

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

@ToString 標(biāo)注接受以下列表中顯示的幾個(gè)參數(shù)。

屬性 默認(rèn)值 描述 范例
includeNames false 是否在生成的 toString 中包含屬性名
@ToString(includeNames=true)
class Person {
    String firstName
    String lastName
}
def p = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p.toString() == 'Person(firstName:Jack, lastName:Nicholson)'
excludes 空列表 從 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 超類(lèi)是否應(yīng)在 toString 中
@ToString
class Id { long id }

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

def p = new Person(id:1, firstName: 'Jack', lastName: 'Nicholson')
assert p.toString() == 'Person(Jack, Nicholson, Id(1))'
includeSuperProperties False 超屬性是否應(yīng)包含在 toString 中
class Person {
    String name
}

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

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

assert bono.toString() == 'BandMember(bandName:U2, name:Bono)'
includeFields False 除了屬性之外,字段是否應(yīng)包括在 toString 中
@ToString(includeFields=true)
class Person {
    String firstName
    String lastName
    private int age
    void test() {
       age = 42
    }
}

def p = new Person(firstName: 'Jack', lastName: 'Nicholson')
p.test()
assert p.toString() == 'Person(Jack, Nicholson, 42)'
ignoreNulls False 是否應(yīng)顯示帶有 null 值的屬性/字段
@ToString(ignoreNulls=true)
class Person {
    String firstName
    String lastName
}

def p = new Person(firstName: 'Jack')
assert p.toString() == 'Person(Jack)'
includePackage False 在 toString 中使用完全限定的類(lèi)名,而非簡(jiǎn)單類(lèi)名
@ToString(includePackage=true)
class Person {
    String firstName
    String lastName
}

def p = new Person(firstName: 'Jack', lastName:'Nicholson')
assert p.toString() == 'acme.Person(Jack, Nicholson)'
cache False 緩存 toString 字符串。如果類(lèi)不可變,是否應(yīng)只設(shè)為 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) // 同一實(shí)例
@groovy.transform.EqualsAndHashCode

@EqualsAndHashCode AST 轉(zhuǎn)換主要目的是為了生成 equalshashCode 方法。生成的散列碼遵循 Josh Bloch 所著的 Effective Java 中所介紹的最佳實(shí)踐:

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

下面是一些用來(lái)調(diào)整 @EqualsAndHashCode 行為的選項(xiàng):

屬性 默認(rèn)值 描述 范例
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()
callSuper False 在 equals 或 hashcode 計(jì)算中是否包含 super
import groovy.transform.EqualsAndHashCode

@EqualsAndHashCode
class Living {
    String race
}

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

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

assert p1!=p2
assert p1.hashCode() != p2.hashCode()
includeFields False 除了屬性之外,是否應(yīng)將字段包含在 equals / hashCode 之中
@ToString(includeFields=true)
class Person {
    String firstName
    String lastName
    private int age
    void test() {
       age = 42
    }
}

def p = new Person(firstName: 'Jack', lastName: 'Nicholson')
p.test()
assert p.toString() == 'Person(Jack, Nicholson, 42)'
cache False 緩存 hashCode 計(jì)算。如果類(lèi)不可改變,是否只應(yīng)將其設(shè)為 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) // 同一實(shí)例  
useCanEqual True equals 是否應(yīng)調(diào)用 canEqual 輔助方法 參看 http://www.artima.com/lejava/articles/equality.html
@groovy.transform.TupleConstructor

@TupleCo