groovy有一個特殊的metaclass類叫做expandometaclass。它的特别之處在于支援動态添加或修改方法,構造函數,屬性,甚至通過使用一個閉包文法來添加或修改靜态方法。
在groovy裡,每一個java.lang.class類都有一個特殊的metaclass屬性,可以通過它拿到一個expandometacalss執行個體。這個執行個體可以被用于添加方法或修改一個已經存在的方法的行為。
預設expandometacalss是不能被繼承的,如果你需要這樣做必須在你的應用啟動前或servlet啟動類前調用expandometaclass#enableglobally()
下面的小節将詳細說明如何在各種場景使用expandometacalss。
methods
一旦expandometaclass通過metaclass屬性被調用,就可以使用<<或 = 操作符來添加方法。
注意 << 是用來添加新方法,如果一個方法已經存在使用它會抛出異常。如果你想替換一個方法可以使用 = 操作符。
對于一個不存在的metaclass屬性通過傳入一個閉包代碼塊執行個體來實作
<code>1</code>
<code>class book {</code>
<code>2</code>
<code> </code><code>string title</code>
<code>3</code>
<code>}</code>
<code>4</code>
<code>5</code>
<code>book.metaclass.titleinuppercase &amp;amp;amp;amp;lt;&amp;amp;amp;amp;lt; {-&amp;amp;amp;amp;gt; title.touppercase() }</code>
<code>6</code>
<code>7</code>
<code>def b = new book(title:"the stand")</code>
<code>8</code>
<code>9</code>
<code>assert "the stand" == b.titleinuppercase()</code>
上面的示例示範了如何通過metaclass屬性使用 << 或 = 操作符指派到一個閉包代碼塊将一個新方法添加到一個類。閉包參數将作為方法參數被攔截。不确定的方法參數可以使用{→ …} 文法。
properties
expandometaclass支援兩種添加或重載屬性的機制。
第一種,支援通過指派到一個metacalss屬性來聲明一個可變屬性。
<code>book.metaclass.author = "stephen king"</code>
<code>def b = new book()</code>
<code>assert "stephen king" == b.author</code>
第二種使用标準機制來添加getter或 setter方法:
<code> </code><code>string title</code>
<code>book.metaclass.getauthor &amp;amp;amp;amp;lt;&amp;amp;amp;amp;lt; {-&amp;amp;amp;amp;gt; "stephen king" }</code>
上面的示例代碼中,閉包裡的屬性是一個制度屬性。當然添加一個類似的setter方法也是可行的,但是屬性值需要被存儲起來。具體可以看下面的示例:
<code>01</code>
<code>02</code>
<code>03</code>
<code>04</code>
<code>05</code>
<code>def properties = collections.synchronizedmap([:])</code>
<code>06</code>
<code>07</code>
<code>book.metaclass.setauthor = { string value -&amp;amp;amp;amp;gt;</code>
<code>08</code>
<code> </code><code>properties[system.identityhashcode(delegate) + "author"] = value</code>
<code>09</code>
<code>10</code>
<code>book.metaclass.getauthor = {-&amp;amp;amp;amp;gt;</code>
<code>11</code>
<code> </code><code>properties[system.identityhashcode(delegate) + "author"]</code>
<code>12</code>
當然,這不僅僅是一個技術問題。比如在一個servlet容器裡一種存儲值得方法是放到目前request中作為request的屬性。(grails也是這樣做的)
constructors
構造函數可以通過constructor屬性來添加,也可以通過閉包代碼塊使用 << 或 = 來添加。在運作時閉包參數将變成構造函數參數。
<code> </code><code>string title</code>
<code>book.metaclass.constructor &amp;amp;amp;amp;lt;&amp;amp;amp;amp;lt; { string title -&amp;amp;amp;amp;gt; new book(title:title) }</code>
<code>def book = new book('groovy in action - 2nd edition')</code>
<code>assert book.title == 'groovy in action - 2nd edition'</code>
添加構造函數的時候需要注意,很容易導緻棧溢出問題。
static methods
靜态方法可以通過同樣的技術來實作,僅僅是比執行個體方法的方法名字前多一個static修飾符。
<code>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) }</code>
<code>def b = book.create("the stand")</code>
borrowing methods
使用expandometaclass,可以實作使用groovy方法指針從其他類中借用方法。
<code>class person {</code>
<code> </code><code>string name</code>
<code>class mortgagelender {</code>
<code> </code><code>def borrowmoney() {</code>
<code> </code><code>"buy house"</code>
<code> </code><code>}</code>
<code>def lender = new mortgagelender()</code>
<code>person.metaclass.buyhouse = lender.&amp;amp;amp;amp;amp;borrowmoney</code>
<code>13</code>
<code>14</code>
<code>def p = new person()</code>
<code>15</code>
<code>16</code>
<code>assert "buy house" == p.buyhouse()</code>
動态方法名(dynamic method names)
因為groovy支援你使用字元串作為屬性名同樣也支援在運作時動态建立方法和屬性。要建立一個動态名字的方法僅僅使用引用屬性名作為字元串這一特性即可。
<code> </code><code>string name = "fred"</code>
<code>def methodname = "bob"</code>
<code>person.metaclass."changenameto${methodname}" = {-&amp;amp;amp;amp;gt; delegate.name = "bob" }</code>
<code>assert "fred" == p.name</code>
<code>p.changenametobob()</code>
<code>assert "bob" == p.name</code>
同樣的概念可以用于靜态方法和屬性。
在grails網絡應用程式架構裡我們可以找到動态方法名字的執行個體。“動态編碼”這個概念就是動态方法名字的具體實作。
htmlcodec類
<code>class htmlcodec {</code>
<code> </code><code>static encode = { thetarget -&amp;amp;amp;amp;gt;</code>
<code> </code><code>htmlutils.htmlescape(thetarget.tostring())</code>
<code> </code><code>}</code>
<code> </code><code>static decode = { thetarget -&amp;amp;amp;amp;gt;</code>
<code> </code><code>htmlutils.htmlunescape(thetarget.tostring())</code>
上面的代碼示範了一種編碼的實作。grails對于每個類都有很多編碼實作可用。在運作時可以配置多個編碼類在應用程式classpath裡。在應用程式啟動架構裡添加一個encodexxx和一個decodexxx方法到特定的meta-classes類。xxx是編碼類的第一部分(比如encodehtml)。這種機制在groovy預處理代碼中如下:
<code>def codecs = classes.findall { it.name.endswith('codec') }</code>
<code>codecs.each { codec -&amp;amp;amp;amp;gt;</code>
<code> </code><code>object.metaclass."encodeas${codec.name-'codec'}" = { codec.newinstance().encode(delegate) }</code>
<code> </code><code>object.metaclass."decodefrom${codec.name-'codec'}" = { codec.newinstance().decode(delegate) }</code>
<code>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;'</code>
<code>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()</code>
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方法:
<code>class stuff {</code>
<code> </code><code>def invokeme() { "foo" }</code>
<code>stuff.metaclass.invokemethod = { string name, args -&amp;amp;amp;gt;</code>
<code> </code><code>def metamethod = stuff.metaclass.getmetamethod(name, args)</code>
<code> </code><code>def result</code>
<code> </code><code>if(metamethod) result = metamethod.invoke(delegate,args)</code>
<code> </code><code>else {</code>
<code> </code><code>result = "bar"</code>
<code> </code><code>result</code>
<code>def stf = new stuff()</code>
<code>17</code>
<code>assert "foo" == stf.invokeme()</code>
<code>18</code>
<code>assert "bar" == stf.dostuff()</code>
在閉包代碼裡,第一步是通過給定的名字和參數查找metamethod。如果一個方法準備就緒就委托執行,否則将傳回一個預設值。
metamethod是一個存在于metaclass上的方法,可以在運作時和編譯時被添加進來。
同樣的邏輯可以用來重載setproperty和getproperty
<code>person.metaclass.getproperty = { string name -&amp;amp;amp;gt;</code>
<code> </code><code>def metaproperty = person.metaclass.getmetaproperty(name)</code>
<code> </code><code>if(metaproperty) result = metaproperty.getproperty(delegate)</code>
<code> </code><code>result = "flintstone"</code>
<code>assert "flintstone" == p.other</code>
這裡值得注意的一個重要問題是不是metamethod而是metaproperty執行個體将會查找。如果一個metaproperty的getproperty方法已經存在,将會直接調用。
重載static invokemethod
expandometaclass甚至允許重載靜态方法,通過一個特殊的invokemethod文法
<code> </code><code>static invokeme() { "foo" }</code>
<code>stuff.metaclass.'static'.invokemethod = { string name, args -&amp;amp;amp;gt;</code>
<code> </code><code>def metamethod = stuff.metaclass.getstaticmetamethod(name, args)</code>
<code>assert "foo" == stuff.invokeme()</code>
<code>assert "bar" == stuff.dostuff()</code>
重載靜态方法的邏輯和前面我們見到的從在執行個體方法的邏輯一樣。唯一的差別在于方位metaclass.static屬性需要調用getstaticmethodname作為靜态metamehod執行個體的傳回值。
extending interfaces
有時候我們需要在expandometaclass接口裡添加方法,為實作這個,必須支援在應用啟動前全局支援expandometaclass.enableglobally()方法。
<code>list.metaclass.sizedoubled = {-&amp;amp;amp;gt; delegate.size() * 2 }</code>
<code>def list = []</code>
<code>list &amp;amp;amp;lt;&amp;amp;amp;lt; 1</code>
<code>list &amp;amp;amp;lt;&amp;amp;amp;lt; 2</code>
<code>assert 4 == list.sizedoubled()</code>
拓展模型允許你添加新方法到已經存在的類中。這些類包括預編譯類,比如jdk中的類。這些新方法不同于使用metaclass或category,可以全局使用。比如,
标準拓展方法:
<code>def file = new file(...)</code>
<code>def contents = file.gettext('utf-8')</code>
gettext方法不存在于file類裡,當然,groovy知道它定義在一個特殊的類裡,resourcegroovymethods:
resourcegroovymethods.java
<code>public static string gettext(file file, string charset) throws ioexception {</code>
<code> </code><code>return iogroovymethods.gettext(newreader(file, charset));</code>
你可能已經注意到,這個拓展方法在一個幫助類(定義了各種各樣的拓展方法)中使用了static方法來定義。gettext方法的第一個參數和傳入值應該一直,額外的參數和拓展方法的參數一緻。這裡我們就定義了file類的gettext方法。這個方法進接受一個參數(string類型)。
建立一個拓展模型非常簡單
寫一個像上面類似的拓展類
寫一個子產品描述檔案
下一步你需要使拓展模型對groovy可見,需要将拓展模型類和可用的描述類添加到類路徑。這意味着你有以下選擇:
要麼直接在類路徑下提供類檔案和子產品描述檔案
或者将拓展子產品打包成jar包以便重用
拓展子產品有兩種方法添加到一個類中
執行個體方法(也叫作一個類的執行個體)
靜态方法(也叫作類方法)
要添加一個執行個體方法到一個已經存在的類,你需要建立一個拓展類。舉個例子,你想添加一個maxretries放到到integer類裡,它接收一個閉包隻要不抛出異常最多執行n次。你需要寫下面的代碼:
<code>class maxretriesextension { //(1)</code>
<code> </code><code>static void maxretries(integer self, closure code) { //(2)</code>
<code> </code><code>int retries = 0</code>
<code> </code><code>throwable e</code>
<code> </code><code>while (retries&amp;amp;lt;self) {</code>
<code> </code><code>try {</code>
<code> </code><code>code.call()</code>
<code> </code><code>break</code>
<code> </code><code>} catch (throwable err) {</code>
<code> </code><code>e = err</code>
<code> </code><code>retries++</code>
<code> </code><code>}</code>
<code> </code><code>}</code>
<code> </code><code>if (retries==0 &amp;amp;amp;&amp;amp;amp; e) {</code>
<code> </code><code>throw e</code>
(1)拓展類
(2)靜态方法的第一個參數和接收的資訊一緻,也就是拓展執行個體
<code>int i=0</code>
<code>5.maxretries {</code>
<code> </code><code>i++</code>
<code>assert i == 1</code>
<code>i=0</code>
<code>try {</code>
<code> </code><code>5.maxretries {</code>
<code> </code><code>throw new runtimeexception("oops")</code>
<code>} catch (runtimeexception e) {</code>
<code> </code><code>assert i == 5</code>
groovy支援添加一個靜态方法到一個類裡,這種情況靜态方法必須定義在自己的檔案裡。靜态和執行個體拓展方法不能再同一個類裡。
<code>class staticstringextension { //(1)</code>
<code> </code><code>static string greeting(string self) { //(2)</code>
<code> </code><code>'hello, world!'</code>
(1)靜态拓展類
(2)靜态方法的第一個從那時候和被拓展的保持一緻
這個例子,可以直接從string類裡調用
<code>assert string.greeting() == 'hello, world!'</code>
groovy允許你加載自己的拓展類,你必須聲明你的拓展幫助類。你必須建立一個名為org.codehaus.groovy.runtime.extensionmodule 到meta-inf/services 目錄裡:
org.codehaus.groovy.runtime.extensionmodule
<code>modulename=test module for specifications</code>
<code>moduleversion=1.0-test</code>
<code>extensionclasses=support.maxretriesextension</code>
<code>staticextensionclasses=support.staticstringextension</code>
子產品描述需要4個主鍵
modulename:你的子產品名字
moduleversion:你的子產品版本号。注意版本号僅僅用于檢驗你是否有将兩個不同的版本導入同一個子產品
extensionclasses:拓展幫助類中執行個體方法清單,你可以提供好幾個類,使用逗号分隔
staticextensionclasses:拓展幫助類中靜态方法裂清單,你可以提供好幾個類,使用逗号分隔
注意并不要求一個子產品既定義靜态幫助類又定義執行個體幫助類,你可以添加好幾個類到單個子產品,也可以拓展不同類到單個子產品。還可以使用不同的類到單個拓展類,但是建議根據特性分組拓展方法。
你不能将一個編譯好了的拓展類當成源碼一樣使用。也就是說使用一個拓展必須在類路徑下,而且是一個已經編譯好了的類。同城,你不能太拓展類裡添加測試類。因為測試類通常和正式源碼會分開。
不像categories,拓展子產品是編譯後的類型檢查。如果不能在類路徑下找到,當你調用拓展方法時類型檢查将會識别出來。對于靜态編譯也一樣。