閱讀397 返回首頁    go 阿裏雲 go 技術社區[雲棲]


運行時和編譯時元編程—運行時元編程(一)

運行時和編譯時元編程 第一部分

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 攔截器 – 實現了groovy.lang.GroovyInterceptable接口並且具有方法攔截能力的Groovy對象,我們將在GroovyInterceptable這一節詳細討論。

對於每次方法調用,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源碼類似下麵這樣

01 package groovy.lang;
02  
03 public interface GroovyObject {
04  
05     Object invokeMethod(String name, Object args);
06  
07     Object getProperty(String propertyName);
08  
09     void setProperty(String propertyName, Object newValue);
10  
11     MetaClass getMetaClass();
12  
13     void setMetaClass(MetaClass metaClass);
14 }

1.1.1 invokeMethod

根據運行時元編程的規定,當你調用的方法不是Groovy對象時將會調用這個方法。這兒有一個簡單的示例演示重載invokeMethod()方法:

01 class SomeGroovyClass {
02  
03     def invokeMethod(String name, Object args) {
04         return "called invokeMethod $name $args"
05     }
06  
07     def test() {
08         return 'method exists'
09     }
10 }
11  
12 def someGroovyClass = new SomeGroovyClass()
13  
14 assert someGroovyClass.test() == 'method exists'
15 assert someGroovyClass.someMethod() == 'called invokeMethod someMethod []'

1.1.2 get/setProperty

通過重載當前對象的getProperty()方法可以使每次讀取屬性時被攔截。下麵是一個簡單的示例:

01 class SomeGroovyClass {
02  
03     def property1 = 'ha'
04     def field2 = 'ho'
05     def field4 = 'hu'
06  
07     def getField1() {
08         return 'getHa'
09     }
10  
11     def getProperty(String name) {
12         if (name != 'field3')
13             return metaClass.getProperty(this, name)                  //(1)
14         else
15             return 'field3'
16     }
17 }
18 def someGroovyClass = new SomeGroovyClass()
19  
20 assert someGroovyClass.field1 == 'getHa'
21 assert someGroovyClass.field2 == 'ho'
22 assert someGroovyClass.field3 == 'field3'
23 assert someGroovyClass.field4 == 'hu'

(1) 將請求的getter轉到除field3之外的所有屬性
你可以重載setProperty()方法來攔截寫屬性:

01 class POGO {
02  
03     String property
04  
05     void setProperty(String name, Object value) {
06         this.@"$name" = 'overriden'
07     }
08 }
09  
10 def pogo = new POGO()
11 pogo.property = 'a'
12  
13 assert pogo.property == 'overriden'

1.1.3 get/setMetaClass

你可以訪問一個對象的metaClass或者通過改變默認的攔截機製來設置實現你自己的MetaClass。比如說你通過寫你自己的MetaClass實現接口來將一套攔截機製分配到一個對象上:

1 // getMetaclass
2 someObject.metaClass
3  
4 // setMetaClass
5 someObject.metaClass = new OwnMetaClassImplementation()

你可以在GroovyInterceptable專題裏找到更多的例子。

1.2 get/setAttribute

這個功能和MetaClass實現類相關。在該類默認的實現裏,你可以無需調用他們的getter和setters方法來訪問屬性。下麵是一個示例:

01 class SomeGroovyClass {
02  
03     def field1 = 'ha'
04     def field2 = 'ho'
05  
06     def getField1() {
07         return 'getHa'
08     }
09 }
10  
11 def someGroovyClass = new SomeGroovyClass()
12  
13 assert someGroovyClass.metaClass.getAttribute(someGroovyClass, 'field1') == 'ha'
14 assert someGroovyClass.metaClass.getAttribute(someGroovyClass, 'field2') == 'ho'
01 class POGO {
02  
03     private String field
04     String property1
05  
06     void setProperty1(String property1) {
07         this.property1 = "setProperty1"
08     }
09 }
10  
11 def pogo = new POGO()
12 pogo.metaClass.setAttribute(pogo, 'field', 'ha')
13 pogo.metaClass.setAttribute(pogo, 'property1', 'ho')
14  
15 assert pogo.field == 'ha'
16 assert pogo.property1 == 'ho'

1.3 MethodMissing

Groovy支持methodMissing的概念。這個方法不同於invokeMethod,它隻能在方法分發失敗的情況下調用,當給定的名字或給定的參數無法找到時被調用:

1 class Foo {
2  
3    def methodMissing(String name, def args) {
4         return "this is me"
5    }
6 }
7  
8 assert new Foo().someUnknownMethod(42l) == 'this is me'

當我們使用methodMissing的時候,如果下一次同樣一個方法被調用其返回的結果可能是緩存的。比如說,考慮在GORM的動態查找器,有一個methodMissing的實現,下麵是具體的代碼:

01 class GORM {
02  
03    def dynamicMethods = [...] // an array of dynamic methods that use regex
04  
05    def methodMissing(String name, args) {
06        def method = dynamicMethods.find { it.match(name) }
07        if(method) {
08           GORM.metaClass."$name" = { Object[] varArgs ->
09              method.invoke(delegate, name, varArgs)
10           }
11           return method.invoke(delegate,name, args)
12        }
13        else throw new MissingMethodException(name, delegate, args)
14    }
15 }

注意,如果我們發現一個方法要被調用,我們會使用ExpandoMetaClass動態注冊一個新的方法在上麵。這就是為什麼下次相同的方法被調用將會更加快。使用methodMissing並沒有invokeMethod的開銷大。而且如果是第二次調用將基本沒有開銷。

1.4 propertyMissing

Groovy支持propertyMissing的概念,用於攔截可能存在的屬性獲取失敗。在getter方法裏,propertyMissing使用單個String類型的參數來代表屬性名字:

1 class Foo {
2    def propertyMissing(String name) { name }
3 }
4  
5 assert new Foo().boo == 'boo'

在Groovy運行時,propertyMissing(String)方法隻有在沒有任何getter方法可以被給定的property所找到才會被調用。
對於setter方法,可以添加第二個propertyMissing定義來添加一個額外的值參數

01 class Foo {
02    def storage = [:]
03    def propertyMissing(String name, value) { storage[name] = value }
04    def propertyMissing(String name) { storage[name] }
05 }
06  
07 def f = new Foo()
08 f.foo = "bar"
09  
10 assert f.foo == "bar"

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

1.5 GroovyInterceptable

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

1 package groovy.lang;
2  
3 public interface GroovyInterceptable extends GroovyObject {
4 }

當一個Groovy對象實現了GroovyInterceptable接口,它的invokeMethod()將在任何方法調用時被調用。下麵是這個類型的一個簡單示例:

1 class Interception implements GroovyInterceptable {
2  
3     def definedMethod() { }
4  
5     def invokeMethod(String name, Object args) {
6         'invokedMethod'
7     }
8 }

下一塊代碼是一個測試類,不管調用存在的方法還是不存在的方法都將返回相同的結果。

1 class InterceptableTest extends GroovyTestCase {
2  
3     void testCheckInterception() {
4         def interception = new Interception()
5  
6         assert interception.definedMethod() == 'invokedMethod'
7         assert interception.someMethod() == 'invokedMethod'
8     }
9 }

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

01 class InterceptionThroughMetaClassTest extends GroovyTestCase {
02  
03     void testPOJOMetaClassInterception() {
04         String invoking = 'ha'
05         invoking.metaClass.invokeMethod = { String name, Object args ->
06             'invoked'
07         }
08  
09         assert invoking.length() == 'invoked'
10         assert invoking.someMethod() == 'invoked'
11     }
12  
13     void testPOGOMetaClassInterception() {
14         Entity entity = new Entity('Hello')
15         entity.metaClass.invokeMethod = { String name, Object args ->
16             'invoked'
17         }
18  
19         assert entity.build(new Object()) == 'invoked'
20         assert entity.someMethod() == 'invoked'
21     }
22 }

關於MetaClass類的詳細信息可以在MetaClass章節找到。

1.6 Categories

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

Category類默認是不能使用的,要使用這些定義在一個category類的方法需要使用 use 方法,這個方法是GDK提供的一個內置於Groovy對象中的實例:

1 use(TimeCategory)  {
2     println 1.minute.from.now           //(1)
3     println 10.hours.ago
4  
5     def someDate = new Date()          //(2)
6     println someDate - 3.months
7 }

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

01 class JPACategory{
02   // Let's enhance JPA EntityManager without getting into the JSR committee
03   static void persistAll(EntityManager em , Object[] entities) { //add an interface to save all
04     entities?.each { em.persist(it) }
05   }
06 }
07  
08 def transactionContext = {
09   EntityManager em, Closure c ->
10   def tx = em.transaction
11   try {
12     tx.begin()
13     use(JPACategory) {
14       c()
15     }
16     tx.commit()
17   } catch (e) {
18     tx.rollback()
19   } finally {
20     //cleanup your resource here
21   }
22 }
23  
24 // user code, they always forget to close resource in exception, some even forget to commit, let's not rely on them.
25 EntityManager em; //probably injected
26 transactionContext (em) {
27  em.persistAll(obj1, obj2, obj3)
28  // let's do some logics here to make the example sensible
29  em.persistAll(obj2, obj4, obj6)
30 }

如果我們去看groovy.time.TimeCategory類的嗲嗎我們會發現拓展方法都是被聲明為static方法。事實上,一個category類的方法要能被成功地加到use代碼塊裏必須要這樣寫:

01 public class TimeCategory {
02  
03     public static Date plus(final Date date, final BaseDuration duration) {
04         return duration.plus(date);
05     }
06  
07     public static Date minus(final Date date, final BaseDuration duration) {
08         final Calendar cal = Calendar.getInstance();
09  
10         cal.setTime(date);
11         cal.add(Calendar.YEAR, -duration.getYears());
12         cal.add(Calendar.MONTH, -duration.getMonths());
13         cal.add(Calendar.DAY_OF_YEAR, -duration.getDays());
14         cal.add(Calendar.HOUR_OF_DAY, -duration.getHours());
15         cal.add(Calendar.MINUTE, -duration.getMinutes());
16         cal.add(Calendar.SECOND, -duration.getSeconds());
17         cal.add(Calendar.MILLISECOND, -duration.getMillis());
18  
19         return cal.getTime();
20     }
21  
22     // ...

另外一個要求是靜態方法的第一個參數必須定義類型,隻要方法被激活。另外一個參數可以作為一個普通的參數當成方法的變量。
因為參數和靜態方法的轉變,category方法的定義可能比一般的方法定義不那麼直觀。不過Groovy提供了一個@Category注解,可以在編譯時將一個類轉化為category類。

01 class Distance {
02     def number
03     String toString() { "${number}m" }
04 }
05  
06 @Category(Number)
07 class NumberCategory {
08     Distance getMeters() {
09         new Distance(number: this)
10     }
11 }
12  
13 use (NumberCategory)  {
14     assert 42.meters.toString() == '42m'
15 }

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

最後更新:2017-05-22 11:01:57

  上一篇:go  《Ansible權威指南》一1.2 Ansible發展史
  下一篇:go  《Ansible權威指南》一1.1 Ansible是什麼