天天看點

運作時和編譯時元程式設計—運作時元程式設計運作時和編譯時元程式設計 第一部分

原文連結   譯文連結   譯者:jackwang

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

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

通過運作時元程式設計,我們可以推遲運作時的分支決策(譯者注:該處原文為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有更複雜的不知,我們在下圖示範:

運作時和編譯時元程式設計—運作時元程式設計運作時和編譯時元程式設計 第一部分

groovy.lang.groovyobject的地位和java中的object類一樣,是一個主接口。groovyobject有一個預設的實作類groovy.lang.groovyobjectsupport,這個類的主要職責是轉換groovy.lang.metaclass對象的調用。groovyobject源碼類似下面這樣

根據運作時元程式設計的規定,當你調用的方法不是groovy對象時将會調用這個方法。這兒有一個簡單的示例示範重載invokemethod()方法:

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

(1) 将請求的getter轉到除field3之外的所有屬性

你可以重載setproperty()方法來攔截寫屬性:

你可以通路一個對象的metaclass或者通過改變預設的攔截機制來設定實作你自己的metaclass。比如說你通過寫你自己的metaclass實作接口來将一套攔截機制配置設定到一個對象上:

你可以在groovyinterceptable專題裡找到更多的例子。

這個功能和metaclass實作類相關。在該類預設的實作裡,你可以無需調用他們的getter和setters方法來通路屬性。下面是一個示例:

groovy支援methodmissing的概念。這個方法不同于invokemethod,它隻能在方法分發失敗的情況下調用,當給定的名字或給定的參數無法找到時被調用:

當我們使用methodmissing的時候,如果下一次同樣一個方法被調用其傳回的結果可能是緩存的。比如說,考慮在gorm的動态查找器,有一個methodmissing的實作,下面是具體的代碼:

注意,如果我們發現一個方法要被調用,我們會使用expandometaclass動态注冊一個新的方法在上面。這就是為什麼下次相同的方法被調用将會更加快。使用methodmissing并沒有invokemethod的開銷大。而且如果是第二次調用将基本沒有開銷。

groovy支援propertymissing的概念,用于攔截可能存在的屬性擷取失敗。在getter方法裡,propertymissing使用單個string類型的參數來代表屬性名字:

在groovy運作時,propertymissing(string)方法隻有在沒有任何getter方法可以被給定的property所找到才會被調用。

對于setter方法,可以添加第二個propertymissing定義來添加一個額外的值參數

methodmissing方法的最适用地方在動态注冊新的屬性時能極大提供查找屬性所花費的性能。

methodmissing和propertymissing方法可以通過expandometaclass來添加靜态方法和屬性。

groovy.lang.groovyinterceptable接口是一個繼承了groovyobject的标記接口,在groovy運作時,用于标記所有方法可以通過groovy的方法分發機制被攔截。

當一個groovy對象實作了groovyinterceptable接口,它的invokemethod()将在任何方法調用時被調用。下面是這個類型的一個簡單示例:

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

我們不能使用預設的groovy方法比如println,因為這些方法是被注入到groovy對象中區,是以它們也會被攔截。

如果我們想攔截所有所有方法但又不想實作groovyinterceptable接口,我們可以在一個對象的metaclass類上實作invokemethod()。對于pogos和pojos,這種方式都是可以的。下面是一個示例:

關于metaclass類的詳細資訊可以在metaclass章節找到。

有這樣一種場景,如果能讓一個類的某些方法不受控制将會是很有用的。為了實作這種可能性,groovy從object-c借用實作了一個特性,叫做categories。

categories特性實作了所謂的category類,一個category類是需要滿足某些特定的預定義的規則來定義一些拓展方法。

下面有幾個categories是在groovy環境中系統提供的一些額外功能:

groovy.time.timecategory

groovy.servlet.servletcategory

groovy.xml.dom.domcategory

category類預設是不能使用的,要使用這些定義在一個category類的方法需要使用 use 方法,這個方法是gdk提供的一個内置于groovy對象中的執行個體:

(1) timecategory添加一個方法到integer

(2) timecategory添加一個方法到date

use 方法把category類作為第一個參數,一個閉包代碼塊作為第二個參數。在closure裡可以通路catetory。從上面的例子可以看到,即便是jdk的類,比如java.lang.integer或java.util.date也是可以被包含到使用者定義的方法裡的。

一個category不需要直接暴露給使用者代碼,下面的示例說明了這一點:

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

另外一個要求是靜态方法的第一個參數必須定義類型,隻要方法被激活。另外一個參數可以作為一個普通的參數當成方法的變量。

因為參數和靜态方法的轉變,category方法的定義可能比一般的方法定義不那麼直覺。不過groovy提供了一個@category注解,可以在編譯時将一個類轉化為category類。

使用@category注解可以直接使用示例方法二不必将目标類型作為第一個參數的好處。目标類型類在注解裡作為了一個參數。

在編譯時元程式設計章節裡有@category的詳細說明。

(tbd)

delegating metaclass

magic package(maksym stavyskyi)

groovy有一個特殊的metaclass類叫做expandometaclass。它的特别之處在于支援動态添加或修改方法,構造函數,屬性,甚至通過使用一個閉包文法來添加或修改靜态方法。

這些特性測試場景将會非常使用,具體在測試指南将會說明。

在groovy裡,每一個java.lang.class類都有一個特殊的metaclass屬性,可以通過它拿到一個expandometacalss執行個體。這個執行個體可以被用于添加方法或修改一個已經存在的方法的行為。

預設expandometacalss是不能被繼承的,如果你需要這樣做必須在你的應用啟動前或servlet啟動類前調用expandometaclass#enableglobally()

下面的小節将詳細說明如何在各種場景使用expandometacalss。

methods

一旦expandometaclass通過metaclass屬性被調用,就可以使用<<或 = 操作符來添加方法。

注意 << 是用來添加新方法,如果一個方法已經存在使用它會抛出異常。如果你想替換一個方法可以使用 = 操作符。

對于一個不存在的metaclass屬性通過傳入一個閉包代碼塊執行個體來實作

上面的示例示範了如何通過metaclass屬性使用 << 或 = 操作符指派到一個閉包代碼塊将一個新方法添加到一個類。閉包參數将作為方法參數被攔截。不确定的方法參數可以使用{→ …} 文法。

properties

expandometaclass支援兩種添加或重載屬性的機制。

第一種,支援通過指派到一個metacalss屬性來聲明一個可變屬性。

第二種使用标準機制來添加getter或 setter方法:

上面的示例代碼中,閉包裡的屬性是一個制度屬性。當然添加一個類似的setter方法也是可行的,但是屬性值需要被存儲起來。具體可以看下面的示例:

當然,這不僅僅是一個技術問題。比如在一個servlet容器裡一種存儲值得方法是放到目前request中作為request的屬性。(grails也是這樣做的)

constructors

構造函數可以通過constructor屬性來添加,也可以通過閉包代碼塊使用 << 或 = 來添加。在運作時閉包參數将變成構造函數參數。

添加構造函數的時候需要注意,很容易導緻棧溢出問題。

static methods

靜态方法可以通過同樣的技術來實作,僅僅是比執行個體方法的方法名字前多一個static修飾符。

borrowing methods

使用expandometaclass,可以實作使用groovy方法指針從其他類中借用方法。

動态方法名(dynamic method names)

因為groovy支援你使用字元串作為屬性名同樣也支援在運作時動态建立方法和屬性。要建立一個動态名字的方法僅僅使用引用屬性名作為字元串這一特性即可。

同樣的概念可以用于靜态方法和屬性。

在grails網絡應用程式架構裡我們可以找到動态方法名字的執行個體。“動态編碼”這個概念就是動态方法名字的具體實作。

htmlcodec類

上面的代碼示範了一種編碼的實作。grails對于每個類都有很多編碼實作可用。在運作時可以配置多個編碼類在應用程式classpath裡。在應用程式啟動架構裡添加一個encodexxx和一個decodexxx方法到特定的meta-classes類。xxx是編碼類的第一部分(比如encodehtml)。這種機制在groovy預處理代碼中如下:

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方法:

在閉包代碼裡,第一步是通過給定的名字和參數查找metamethod。如果一個方法準備就緒就委托執行,否則将傳回一個預設值。

metamethod是一個存在于metaclass上的方法,可以在運作時和編譯時被添加進來。

同樣的邏輯可以用來重載setproperty和getproperty

這裡值得注意的一個重要問題是不是metamethod而是metaproperty執行個體将會查找。如果一個metaproperty的getproperty方法已經存在,将會直接調用。

重載static invokemethod

expandometaclass甚至允許重載靜态方法,通過一個特殊的invokemethod文法

重載靜态方法的邏輯和前面我們見到的從在執行個體方法的邏輯一樣。唯一的差別在于方位metaclass.static屬性需要調用getstaticmethodname作為靜态metamehod執行個體的傳回值。

extending interfaces

有時候我們需要在expandometaclass接口裡添加方法,為實作這個,必須支援在應用啟動前全局支援expandometaclass.enableglobally()方法。

拓展模型允許你添加新方法到已經存在的類中。這些類包括預編譯類,比如jdk中的類。這些新方法不同于使用metaclass或category,可以全局使用。比如,

标準拓展方法:

gettext方法不存在于file類裡,當然,groovy知道它定義在一個特殊的類裡,resourcegroovymethods:

resourcegroovymethods.java

你可能已經注意到,這個拓展方法在一個幫助類(定義了各種各樣的拓展方法)中使用了static方法來定義。gettext方法的第一個參數和傳入值應該一直,額外的參數和拓展方法的參數一緻。這裡我們就定義了file類的gettext方法。這個方法進接受一個參數(string類型)。

建立一個拓展模型非常簡單

寫一個像上面類似的拓展類

寫一個子產品描述檔案

下一步你需要使拓展模型對groovy可見,需要将拓展模型類和可用的描述類添加到類路徑。這意味着你有以下選擇:

要麼直接在類路徑下提供類檔案和子產品描述檔案

或者将拓展子產品打包成jar包以便重用

拓展子產品有兩種方法添加到一個類中

執行個體方法(也叫作一個類的執行個體)

靜态方法(也叫作類方法)

要添加一個執行個體方法到一個已經存在的類,你需要建立一個拓展類。舉個例子,你想添加一個maxretries放到到integer類裡,它接收一個閉包隻要不抛出異常最多執行n次。你需要寫下面的代碼:

(1)拓展類

(2)靜态方法的第一個參數和接收的資訊一緻,也就是拓展執行個體

下一步,聲明了拓展類之後,你可以這樣調用它:

groovy支援添加一個靜态方法到一個類裡,這種情況靜态方法必須定義在自己的檔案裡。靜态和執行個體拓展方法不能再同一個類裡。

(1)靜态拓展類

(2)靜态方法的第一個從那時候和被拓展的保持一緻

這個例子,可以直接從string類裡調用

groovy允許你加載自己的拓展類,你必須聲明你的拓展幫助類。你必須建立一個名為org.codehaus.groovy.runtime.extensionmodule 到meta-inf/services 目錄裡:

org.codehaus.groovy.runtime.extensionmodule

子產品描述需要4個主鍵

modulename:你的子產品名字

moduleversion:你的子產品版本号。注意版本号僅僅用于檢驗你是否有将兩個不同的版本導入同一個子產品

extensionclasses:拓展幫助類中執行個體方法清單,你可以提供好幾個類,使用逗号分隔

staticextensionclasses:拓展幫助類中靜态方法裂清單,你可以提供好幾個類,使用逗号分隔

注意并不要求一個子產品既定義靜态幫助類又定義執行個體幫助類,你可以添加好幾個類到單個子產品,也可以拓展不同類到單個子產品。還可以使用不同的類到單個拓展類,但是建議根據特性分組拓展方法。

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

不像categories,拓展子產品是編譯後的類型檢查。如果不能在類路徑下找到,當你調用拓展方法時類型檢查将會識别出來。對于靜态編譯也一樣。