1. 程式人生 > >執行時和編譯時超程式設計—執行時超程式設計

執行時和編譯時超程式設計—執行時超程式設計

執行時和編譯時超程式設計 第一部分

Groovy語言支援兩種風格的超程式設計:執行時超程式設計和編譯時超程式設計。第一種超程式設計支援在程式執行時修改類模型和程式行為,而第二種發生在編譯時。兩種超程式設計有各自的優缺點,在這一章節我們將詳細討論。

注:譯者也是第一次接觸Groovy,由於時間和水平有限(姑且讓譯者使用這個理由吧,對待知識本應該一絲不苟)部分專有名詞可能翻譯不準確甚至有誤(讀者閱讀的過程中最好能參考原文),懇請讀者不吝留言指出,謝謝!

1.執行時超程式設計

通過執行時超程式設計,我們可以推遲執行時的分支決策(譯者注:該處原文為we can postpone to runtime the decision,對於decision,譯者也找不到一個合適的表達,請讀者根據下圖和上下文理解,如果讀者有更好的翻譯請留言指出,謝謝)來攔截,注入甚至合成類或介面的方法。對於Groovy MOP(譯者注:對於初學者,這裡突然冒出這個新名詞,譯者也頭大,通過查詢,MOP是Mete Object Protocol的縮寫,讀者可

參考該文來了解)的更深理解,我們需要理解Groovy的物件和方法處理。在Groovy裡,我們主要使用三種類型的物件:POJO,POGO和Groovy攔截器。Groovy支援超程式設計多種方式來對這些型別物件進行超程式設計。

  • POJO – 一個普通的Java物件,它的類可以使用Java或其他支援JVM的語言來編寫。
  • POGO – 一個Groovy物件,類用Groovy實現。預設繼承了java.lang.Object並且實現了groovy.lang.GroovyObject介面。

對於每次方法呼叫,Groovy都會檢查物件是一個POJO還是一個POGO。對於POJOs,Groovy從groovy.lang.MetaClassRegistry類中攜帶元資訊並且委託方法來呼叫。對於POGOs,Groovy有更復雜的不知,我們在下圖演示:

1.1 GroovyObject介面

Groovy.lang.GroovyObject的地位和Java中的Object類一樣,是一個主介面。GroovyObject有一個預設的實現類groovy.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

根據執行時超程式設計的規定,當你呼叫的方法不是Groovy物件時將會呼叫這個方法。這兒有一個簡單的示例演示過載invokeMethod()方法:

class SomeGroovyClass {

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

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

def someGroovyClass = new SomeGroovyClass()

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

1.1.2 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) {
        [email protected]"$name" = 'overriden'
    }
}

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

assert pogo.property == 'overriden'

1.1.3 get/setMetaClass

你可以訪問一個物件的metaClass或者通過改變預設的攔截機制來設定實現你自己的MetaClass。比如說你通過寫你自己的MetaClass實現介面來將一套攔截機制分配到一個物件上:

// getMetaclass
someObject.metaClass

// setMetaClass
someObject.metaClass = new OwnMetaClassImplementation()

1.2 get/setAttribute

這個功能和MetaClass實現類相關。在該類預設的實現裡,你可以無需呼叫他們的getter和setters方法來訪問屬性。下面是一個示例:

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執行時,propertyMissing(String)方法只有在沒有任何getter方法可以被給定的property所找到才會被呼叫。
對於setter方法,可以新增第二個propertyMissing定義來新增一個額外的值引數

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

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

assert f.foo == "bar"

methodMissing方法的最適用地方在動態註冊新的屬性時能極大提供查詢屬性所花費的效能。
methodMissing和propertyMissing方法可以通過ExpandoMetaClass來新增靜態方法和屬性。

1.5 GroovyInterceptable

Groovy.lang.GroovyInterceptable介面是一個繼承了GroovyObject的標記介面,在Groovy執行時,用於標記所有方法可以通過Groovy的方法分發機制被攔截。

package groovy.lang;

public interface GroovyInterceptable extends GroovyObject {
}

當一個Groovy物件實現了GroovyInterceptable介面,它的invokeMethod()將在任何方法呼叫時被呼叫。下面是這個型別的一個簡單示例:

class Interception implements GroovyInterceptable {

    def definedMethod() { }

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

下一塊程式碼是一個測試類,不管呼叫存在的方法還是不存在的方法都將返回相同的結果。

class InterceptableTest extends GroovyTestCase {

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

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

我們不能使用預設的Groovy方法比如println,因為這些方法是被注入到Groovy物件中區,因此它們也會被攔截。
如果我們想攔截所有所有方法但又不想實現GroovyInterceptable介面,我們可以在一個物件的MetaClass類上實現invokeMethod()。對於POGOs和POJOs,這種方式都是可以的。下面是一個示例:

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類的詳細資訊可以在MetaClass章節找到。

1.6 Categories

有這樣一種場景,如果能讓一個類的某些方法不受控制將會是很有用的。為了實現這種可能性,Groovy從Object-C借用實現了一個特性,叫做Categories。
Categories特性實現了所謂的category類,一個category類是需要滿足某些特定的預定義的規則來定義一些拓展方法。
下面有幾個categories是在Groovy環境中系統提供的一些額外功能:

Category類預設是不能使用的,要使用這些定義在一個category類的方法需要使用 use 方法,這個方法是GDK提供的一個內置於Groovy物件中的例項:

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

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

(1) TimeCategory新增一個方法到Integer
(2) TimeCategory新增一個方法到Date
use 方法把category類作為第一個引數,一個閉包程式碼塊作為第二個引數。在Closure裡可以訪問catetory。從上面的例子可以看到,即便是JDK的類,比如java.lang.Integer或java.util.Date也是可以被包含到使用者定義的方法裡的。
一個category不需要直接暴露給使用者程式碼,下面的示例說明了這一點:

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

    // ...

另外一個要求是靜態方法的第一個引數必須定義型別,只要方法被啟用。另外一個引數可以作為一個普通的引數當成方法的變數。
因為引數和靜態方法的轉變,category方法的定義可能比一般的方法定義不那麼直觀。不過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的詳細說明。

1.7 MetaClasses

(TBD)

1.7.1 Custom metaclasses

(TBD)
Delegating metaclass
(TBD)
Magic package(Maksym Stavyskyi)
(TBD)

1.7.2 Per instance metaclass

(TBD)

1.7.3 ExpandoMetaclass

Groovy有一個特殊的MetaClass類叫做ExpandoMetaClass。它的特別之處在於支援動態新增或修改方法,建構函式,屬性,甚至通過使用一個閉包語法來新增或修改靜態方法。
這些特性測試場景將會非常使用,具體在測試指南將會說明。
在Groovy裡,每一個java.lang.Class類都有一個特殊的metaClass屬性,可以通過它拿到一個ExpandoMetaCalss例項。這個例項可以被用於新增方法或修改一個已經存在的方法的行為。
預設ExpandoMetaCalss是不能被繼承的,如果你需要這樣做必須在你的應用啟動前或servlet啟動類前呼叫ExpandoMetaClass#enableGlobally()
下面的小節將詳細說明如何在各種場景使用ExpandoMetaCalss。

Methods
一旦ExpandoMetaClass通過metaClass屬性被呼叫,就可以使用<<或 = 操作符來新增方法。
注意 << 是用來新增新方法,如果一個方法已經存在使用它會丟擲異常。如果你想替換一個方法可以使用 = 操作符。
對於一個不存在的metaClass屬性通過傳入一個閉包程式碼塊例項來實現

class Book {
   String title
}

Book.metaClass.titleInUpperCase &amp;amp;amp;amp;lt;&amp;amp;amp;amp;lt; {-&amp;amp;amp;amp;gt; title.toUpperCase() }

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

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

上面的示例演示瞭如何通過metaClass屬性使用 << 或 = 操作符賦值到一個閉包程式碼塊將一個新方法新增到一個類。閉包引數將作為方法引數被攔截。不確定的方法引數可以使用{→ …} 語法。
Properties
ExpandoMetaClass支援兩種新增或過載屬性的機制。
第一種,支援通過賦值到一個metaCalss屬性來宣告一個可變屬性。

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 &amp;amp;amp;amp;lt;&amp;amp;amp;amp;lt; {-&amp;amp;amp;amp;gt; "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 -&amp;amp;amp;amp;gt;
   properties[System.identityHashCode(delegate) + "author"] = value
}
Book.metaClass.getAuthor = {-&amp;amp;amp;amp;gt;
   properties[System.identityHashCode(delegate) + "author"]
}

當然,這不僅僅是一個技術問題。比如在一個servlet容器裡一種儲存值得方法是放到當前request中作為request的屬性。(Grails也是這樣做的)
Constructors
建構函式可以通過constructor屬性來新增,也可以通過閉包程式碼塊使用 << 或 = 來新增。在執行時閉包引數將變成建構函式引數。

class Book {
    String title
}
Book.metaClass.constructor &amp;amp;amp;amp;lt;&amp;amp;amp;amp;lt; { String title -&amp;amp;amp;amp;gt; new Book(title:title) }

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

新增建構函式的時候需要注意,很容易導致棧溢位問題。
Static Methods
靜態方法可以通過同樣的技術來實現,僅僅是比例項方法的方法名字前多一個static修飾符。

class Book {
   String title
}

Book.metaClass.static.create &amp;amp;amp;amp;lt;&amp;amp;amp;amp;lt; { String title -&amp;amp;amp;amp;gt; new Book(title:title) }

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

Borrowing Methods
使用ExpandoMetaClass,可以實現使用Groovy方法指標從其他類中借用方法。

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

def lender = new MortgageLender()

Person.metaClass.buyHouse = lender.&amp;amp;amp;amp;amp;borrowMoney

def p = new Person()

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

動態方法名(Dynamic Method Names)
因為Groovy支援你使用字串作為屬性名同樣也支援在執行時動態建立方法和屬性。要建立一個動態名字的方法僅僅使用引用屬性名作為字串這一特性即可。

class Person {
   String name = "Fred"
}

def methodName = "Bob"

Person.metaClass."changeNameTo${methodName}" = {-&amp;amp;amp;amp;gt; delegate.name = "Bob" }

def p = new Person()

assert "Fred" == p.name

p.changeNameToBob()

assert "Bob" == p.name

同樣的概念可以用於靜態方法和屬性。
在Grails網路應用程式框架裡我們可以找到動態方法名字的例項。“動態編碼”這個概念就是動態方法名字的具體實現。
HTMLCodec類

class HTMLCodec {
    static encode = { theTarget -&amp;amp;amp;amp;gt;
        HtmlUtils.htmlEscape(theTarget.toString())
    }

    static decode = { theTarget -&amp;amp;amp;amp;gt;
    	HtmlUtils.htmlUnescape(theTarget.toString())
    }
}

上面的程式碼演示了一種編碼的實現。Grails對於每個類都有很多編碼實現可用。在執行時可以配置多個編碼類在應用程式classpath裡。在應用程式啟動框架裡新增一個encodeXXX和一個decodeXXX方法到特定的meta-classes類。XXX是編碼類的第一部分(比如encodeHTML)。這種機制在Groovy預處理程式碼中如下:

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

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

def html = '&amp;amp;amp;amp;lt;html&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;body&amp;amp;amp;amp;gt;hello&amp;amp;amp;amp;lt;/body&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;/html&amp;amp;amp;amp;gt;'

assert '&amp;amp;amp;amp;lt;html&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;body&amp;amp;amp;amp;gt;hello&amp;amp;amp;amp;lt;/body&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;/html&amp;amp;amp;amp;gt;' == html.encodeAsHTML()

Runtime Discovery
在執行時,當方法被執行的時候如果知道其他方法或屬性的存在性是非常有用的。ExpandoMetaClass提供了下面的方法來獲取:

  • getMetaMethod
  • hasMetaMethod
  • getMetaProperty
  • hasMetaProperty

為何不直接使用反射?因為Groovy不同於Java,Java的方法是真正的方法並且只能在執行時存在。Groovy是(並不總是)通過MetaMethods來呈現。MetaMethods告訴你在執行時哪些方法可用,因此你的程式碼可以適配。
過載invokeMethod,getProperty和setProperty是一種特別的用法。
GroovyObject Methods
ExpandoMetaClass的另外一個特點是支援過載invokeMethod,getProperty和setProperty。這些方法可以在groovy.lang.GroovyObject類裡找到。
下面的程式碼演示瞭如何過載invokeMethod方法:

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

Stuff.metaClass.invokeMethod = { String name, args -&amp;amp;amp;gt;
   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()

在閉包程式碼裡,第一步是通過給定的名字和引數查詢MetaMethod。如果一個方法準備就緒就委託執行,否則將返回一個預設值。
MetaMethod是一個存在於MetaClass上的方法,可以在執行時和編譯時被新增進來。
同樣的邏輯可以用來過載setProperty和getProperty

class Person {
   String name = "Fred"
}

Person.metaClass.getProperty = { String name -&amp;amp;amp;gt;
   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例項將會查詢。如果一個MetaProperty的getProperty方法已經存在,將會直接呼叫。

過載Static invokeMethod

ExpandoMetaClass甚至允許過載靜態方法,通過一個特殊的invokeMethod語法

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

Stuff.metaClass.'static'.invokeMethod = { String name, args -&amp;amp;amp;gt;
   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作為靜態MetaMehod例項的返回值。

Extending Interfaces

有時候我們需要在ExpandoMetaClass接口裡新增方法,為實現這個,必須支援在應用啟動前全域性支援ExpandoMetaClass.enableGlobally()方法。

List.metaClass.sizeDoubled = {-&amp;amp;amp;gt; delegate.size() * 2 }

def list = []

list &amp;amp;amp;lt;&amp;amp;amp;lt; 1
list &amp;amp;amp;lt;&amp;amp;amp;lt; 2

assert 4 == list.sizeDoubled()

1.8 拓展模型

1.8.1 拓展已經存在的類

拓展模型允許你新增新方法到已經存在的類中。這些類包括預編譯類,比如JDK中的類。這些新方法不同於使用metaclass或category,可以全域性使用。比如,
標準拓展方法:

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方法。這個方法進接受一個引數(String型別)。
建立一個拓展模型非常簡單

  • 寫一個像上面類似的拓展類
  • 寫一個模組描述檔案

下一步你需要使拓展模型對Groovy可見,需要將拓展模型類和可用的描述類新增到類路徑。這意味著你有以下選擇:

  • 要麼直接在類路徑下提供類檔案和模組描述檔案
  • 或者將拓展模組打包成jar包以便重用

拓展模組有兩種方法新增到一個類中

  • 例項方法(也叫作一個類的例項)
  • 靜態方法(也叫作類方法)

1.8.2 例項方法

要新增一個例項方法到一個已經存在的類,你需要建立一個拓展類。舉個例子,你想新增一個maxRetries放到到Integer類裡,它接收一個閉包只要不丟擲異常最多執行n次。你需要寫下面的程式碼:

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

(1)拓展類
(2)靜態方法的第一個引數和接收的資訊一致,也就是拓展例項
下一步,聲明瞭拓展類之後,你可以這樣呼叫它:

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

1.8.3 靜態方法

Groovy支援新增一個靜態方法到一個類裡,這種情況靜態方法必須定義在自己的檔案裡。靜態和例項拓展方法不能再同一個類裡。

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

(1)靜態拓展類
(2)靜態方法的第一個從那時候和被拓展的保持一致
這個例子,可以直接從String類裡呼叫

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

1.8.4 模組描述

Groovy允許你載入自己的拓展類,你必須宣告你的拓展幫助類。你必須建立一個名為org.codehaus.groovy.runtime.ExtensionModule 到META-INF/services 目錄裡:
org.codehaus.groovy.runtime.ExtensionModule

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

模組描述需要4個主鍵

  • moduleName:你的模組名字
  • moduleVersion:你的模組版本號。注意版本號僅僅用於檢驗你是否有將兩個不同的版本匯入同一個模組
  • extensionClasses:拓展幫助類中例項方法列表,你可以提供好幾個類,使用逗號分隔
  • staticExtensionClasses:拓展幫助類中靜態方法裂列表,你可以提供好幾個類,使用逗號分隔

注意並不要求一個模組既定義靜態幫助類又定義例項幫助類,你可以新增好幾個類到單個模組,也可以拓展不同類到單個模組。還可以使用不同的類到單個拓展類,但是建議根據特性分組拓展方法。

1.8.5 拓展模組和類路徑

你不能將一個編譯好了的拓展類當成原始碼一樣使用。也就是說使用一個拓展必須在類路徑下,而且是一個已經編譯好了的類。同城,你不能太拓展類裡新增測試類。因為測試類通常和正式原始碼會分開。

1.8.6 型別檢查能力

不像categories,拓展模組是編譯後的型別檢查。如果不能在類路徑下找到,當你呼叫拓展方法時型別檢查將會識別出來。對於靜態編譯也一樣。