387
阿裏雲
技術社區[雲棲]
運行時和編譯時元編程—運行時元編程(二)
1.7.3 ExpandoMetaclass
Groovy有一個特殊的MetaClass類叫做ExpandoMetaClass。它的特別之處在於支持動態添加或修改方法,構造函數,屬性,甚至通過使用一個閉包語法來添加或修改靜態方法。
這些特性測試場景將會非常使用,具體在測試指南將會說明。
在Groovy裏,每一個java.lang.Class類都有一個特殊的metaClass屬性,可以通過它拿到一個ExpandoMetaCalss實例。這個實例可以被用於添加方法或修改一個已經存在的方法的行為。
默認ExpandoMetaCalss是不能被繼承的,如果你需要這樣做必須在你的應用啟動前或servlet啟動類前調用ExpandoMetaClass#enableGlobally()
下麵的小節將詳細說明如何在各種場景使用ExpandoMetaCalss。
Methods
一旦ExpandoMetaClass通過metaClass屬性被調用,就可以使用<<或 = 操作符來添加方法。
注意 << 是用來添加新方法,如果一個方法已經存在使用它會拋出異常。如果你想替換一個方法可以使用 = 操作符。
對於一個不存在的metaClass屬性通過傳入一個閉包代碼塊實例來實現
5 |
Book.metaClass.titleInUpperCase &amp;amp;amp;lt;&amp;amp;amp;lt; {-&amp;amp;amp;gt; title.toUpperCase() } |
7 |
def b = new Book(title:"The Stand") |
9 |
assert "THE STAND" == b.titleInUpperCase() |
上麵的示例演示了如何通過metaClass屬性使用 << 或 = 操作符賦值到一個閉包代碼塊將一個新方法添加到一個類。閉包參數將作為方法參數被攔截。不確定的方法參數可以使用{→ …} 語法。
Properties
ExpandoMetaClass支持兩種添加或重載屬性的機製。
第一種,支持通過賦值到一個metaCalss屬性來聲明一個可變屬性。
5 |
Book.metaClass.author = "Stephen King" |
8 |
assert "Stephen King" == b.author |
第二種使用標準機製來添加getter或 setter方法:
4 |
Book.metaClass.getAuthor &amp;amp;amp;lt;&amp;amp;amp;lt; {-&amp;amp;amp;gt; "Stephen King" } |
8 |
assert "Stephen King" == b.author |
上麵的示例代碼中,閉包裏的屬性是一個製度屬性。當然添加一個類似的setter方法也是可行的,但是屬性值需要被存儲起來。具體可以看下麵的示例:
05 |
def properties = Collections.synchronizedMap([:]) |
07 |
Book.metaClass.setAuthor = { String value -&amp;amp;amp;gt; |
08 |
properties[System.identityHashCode(delegate) + "author"] = value
|
10 |
Book.metaClass.getAuthor = {-&amp;amp;amp;gt; |
11 |
properties[System.identityHashCode(delegate) + "author"]
|
當然,這不僅僅是一個技術問題。比如在一個servlet容器裏一種存儲值得方法是放到當前request中作為request的屬性。(Grails也是這樣做的)
Constructors
構造函數可以通過constructor屬性來添加,也可以通過閉包代碼塊使用 << 或 = 來添加。在運行時閉包參數將變成構造函數參數。
4 |
Book.metaClass.constructor &amp;amp;amp;lt;&amp;amp;amp;lt; { String title -&amp;amp;amp;gt; new Book(title:title) } |
6 |
def book = new Book('Groovy in Action - 2nd Edition') |
7 |
assert book.title == 'Groovy in Action - 2nd Edition' |
添加構造函數的時候需要注意,很容易導致棧溢出問題。
Static Methods
靜態方法可以通過同樣的技術來實現,僅僅是比實例方法的方法名字前多一個static修飾符。
5 |
Book.metaClass.static.create &amp;amp;amp;lt;&amp;amp;amp;lt; { String title -&amp;amp;amp;gt; new Book(title:title) } |
7 |
def b = Book.create("The Stand") |
Borrowing Methods
使用ExpandoMetaClass,可以實現使用Groovy方法指針從其他類中借用方法。
04 |
class MortgageLender { |
10 |
def lender = new MortgageLender() |
12 |
Person.metaClass.buyHouse = lender.&amp;amp;amp;amp;borrowMoney |
16 |
assert "buy house" == p.buyHouse() |
動態方法名(Dynamic Method Names)
因為Groovy支持你使用字符串作為屬性名同樣也支持在運行時動態創建方法和屬性。要創建一個動態名字的方法僅僅使用引用屬性名作為字符串這一特性即可。
05 |
def methodName = "Bob" |
07 |
Person.metaClass."changeNameTo${methodName}" = {-&amp;amp;amp;gt; delegate.name = "Bob" } |
11 |
assert "Fred" == p.name |
15 |
assert "Bob" == p.name |
同樣的概念可以用於靜態方法和屬性。
在Grails網絡應用程序框架裏我們可以找到動態方法名字的實例。“動態編碼”這個概念就是動態方法名字的具體實現。
HTMLCodec類
2 |
static encode = { theTarget -&amp;amp;amp;gt;
|
3 |
HtmlUtils.htmlEscape(theTarget.toString())
|
6 |
static decode = { theTarget -&amp;amp;amp;gt;
|
7 |
HtmlUtils.htmlUnescape(theTarget.toString())
|
上麵的代碼演示了一種編碼的實現。Grails對於每個類都有很多編碼實現可用。在運行時可以配置多個編碼類在應用程序classpath裏。在應用程序啟動框架裏添加一個encodeXXX和一個decodeXXX方法到特定的meta-classes類。XXX是編碼類的第一部分(比如encodeHTML)。這種機製在Groovy預處理代碼中如下:
01 |
def codecs = classes.findAll { it.name.endsWith('Codec') } |
03 |
codecs.each { codec -&amp;amp;amp;gt; |
04 |
Object.metaClass."encodeAs${codec.name-'Codec'}" = { codec.newInstance().encode(delegate) }
|
05 |
Object.metaClass."decodeFrom${codec.name-'Codec'}" = { codec.newInstance().decode(delegate) }
|
08 |
def html = '&amp;amp;amp;lt;html&amp;amp;amp;gt;&amp;amp;amp;lt;body&amp;amp;amp;gt;hello&amp;amp;amp;lt;/body&amp;amp;amp;gt;&amp;amp;amp;lt;/html&amp;amp;amp;gt;' |
10 |
assert '&amp;amp;amp;lt;html&amp;amp;amp;gt;&amp;amp;amp;lt;body&amp;amp;amp;gt;hello&amp;amp;amp;lt;/body&amp;amp;amp;gt;&amp;amp;amp;lt;/html&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方法:
02 |
def invokeMe() { "foo" }
|
05 |
Stuff.metaClass.invokeMethod = { String name, args -&amp;amp;gt; |
06 |
def metaMethod = Stuff.metaClass.getMetaMethod(name, args)
|
08 |
if(metaMethod) result = metaMethod.invoke(delegate,args)
|
17 |
assert "foo" == stf.invokeMe() |
18 |
assert "bar" == stf.doStuff() |
在閉包代碼裏,第一步是通過給定的名字和參數查找MetaMethod。如果一個方法準備就緒就委托執行,否則將返回一個默認值。
MetaMethod是一個存在於MetaClass上的方法,可以在運行時和編譯時被添加進來。
同樣的邏輯可以用來重載setProperty和getProperty
05 |
Person.metaClass.getProperty = { String name -&amp;amp;gt; |
06 |
def metaProperty = Person.metaClass.getMetaProperty(name)
|
08 |
if(metaProperty) result = metaProperty.getProperty(delegate)
|
17 |
assert "Fred" == p.name |
18 |
assert "Flintstone" == p.other |
這裏值得注意的一個重要問題是不是MetaMethod而是MetaProperty實例將會查找。如果一個MetaProperty的getProperty方法已經存在,將會直接調用。
重載Static invokeMethod
ExpandoMetaClass甚至允許重載靜態方法,通過一個特殊的invokeMethod語法
02 |
static invokeMe() { "foo" }
|
05 |
Stuff.metaClass.'static'.invokeMethod = { String name, args -&amp;amp;gt; |
06 |
def metaMethod = Stuff.metaClass.getStaticMetaMethod(name, args)
|
08 |
if(metaMethod) result = metaMethod.invoke(delegate,args)
|
15 |
assert "foo" == Stuff.invokeMe() |
16 |
assert "bar" == Stuff.doStuff() |
重載靜態方法的邏輯和前麵我們見到的從在實例方法的邏輯一樣。唯一的區別在於方位metaClass.static屬性需要調用getStaticMethodName作為靜態MetaMehod實例的返回值。
Extending Interfaces
有時候我們需要在ExpandoMetaClass接口裏添加方法,為實現這個,必須支持在應用啟動前全局支持ExpandoMetaClass.enableGlobally()方法。
1 |
List.metaClass.sizeDoubled = {-&amp;amp;gt; delegate.size() * 2 } |
5 |
list &amp;amp;lt;&amp;amp;lt; 1 |
6 |
list &amp;amp;lt;&amp;amp;lt; 2 |
8 |
assert 4 == list.sizeDoubled() |
1.8 拓展模型
1.8.1 拓展已經存在的類
拓展模型允許你添加新方法到已經存在的類中。這些類包括預編譯類,比如JDK中的類。這些新方法不同於使用metaclass或category,可以全局使用。比如,
標準拓展方法:
1 |
def file = new File(...) |
2 |
def contents = file.getText('utf-8') |
getText方法不存在於File類裏,當然,Groovy知道它定義在一個特殊的類裏,ResourceGroovyMethods:
ResourceGroovyMethods.java
1 |
public static String getText(File file, String charset) throws IOException { |
2 |
return IOGroovyMethods.getText(newReader(file, charset));
|
你可能已經注意到,這個拓展方法在一個幫助類(定義了各種各樣的拓展方法)中使用了static方法來定義。getText方法的第一個參數和傳入值應該一直,額外的參數和拓展方法的參數一致。這裏我們就定義了File類的getText方法。這個方法進接受一個參數(String類型)。
創建一個拓展模型非常簡單
下一步你需要使拓展模型對Groovy可見,需要將拓展模型類和可用的描述類添加到類路徑。這意味著你有以下選擇:
- 要麼直接在類路徑下提供類文件和模塊描述文件
- 或者將拓展模塊打包成jar包以便重用
拓展模塊有兩種方法添加到一個類中
- 實例方法(也叫作一個類的實例)
- 靜態方法(也叫作類方法)
1.8.2 實例方法
要添加一個實例方法到一個已經存在的類,你需要創建一個拓展類。舉個例子,你想添加一個maxRetries放到到Integer類裏,它接收一個閉包隻要不拋出異常最多執行n次。你需要寫下麵的代碼:
01 |
class MaxRetriesExtension { //(1) |
02 |
static void maxRetries(Integer self, Closure code) { //(2)
|
05 |
while (retries&amp;lt;self) {
|
09 |
} catch (Throwable err) {
|
14 |
if (retries==0 &amp;amp;&amp;amp; e) {
|
(1)拓展類
(2)靜態方法的第一個參數和接收的信息一致,也就是拓展實例
下一步,聲明了拓展類之後,你可以這樣調用它:
09 |
throw new RuntimeException("oops")
|
11 |
} catch (RuntimeException e) { |
1.8.3 靜態方法
Groovy支持添加一個靜態方法到一個類裏,這種情況靜態方法必須定義在自己的文件裏。靜態和實例拓展方法不能再同一個類裏。
1 |
class StaticStringExtension { //(1) |
2 |
static String greeting(String self) { //(2)
|
(1)靜態拓展類
(2)靜態方法的第一個從那時候和被拓展的保持一致
這個例子,可以直接從String類裏調用
1 |
assert String.greeting() == 'Hello, world!' |
1.8.4 模塊描述
Groovy允許你加載自己的拓展類,你必須聲明你的拓展幫助類。你必須創建一個名為org.codehaus.groovy.runtime.ExtensionModule 到META-INF/services 目錄裏:
org.codehaus.groovy.runtime.ExtensionModule
1 |
moduleName=Test module for specifications |
3 |
extensionClasses=support.MaxRetriesExtension |
4 |
staticExtensionClasses=support.StaticStringExtension |
模塊描述需要4個主鍵
- moduleName:你的模塊名字
- moduleVersion:你的模塊版本號。注意版本號僅僅用於檢驗你是否有將兩個不同的版本導入同一個模塊
- extensionClasses:拓展幫助類中實例方法列表,你可以提供好幾個類,使用逗號分隔
- staticExtensionClasses:拓展幫助類中靜態方法裂列表,你可以提供好幾個類,使用逗號分隔
注意並不要求一個模塊既定義靜態幫助類又定義實例幫助類,你可以添加好幾個類到單個模塊,也可以拓展不同類到單個模塊。還可以使用不同的類到單個拓展類,但是建議根據特性分組拓展方法。
1.8.5 拓展模塊和類路徑
你不能將一個編譯好了的拓展類當成源碼一樣使用。也就是說使用一個拓展必須在類路徑下,而且是一個已經編譯好了的類。同城,你不能太拓展類裏添加測試類。因為測試類通常和正式源碼會分開。
1.8.6 類型檢查能力
不像categories,拓展模塊是編譯後的類型檢查。如果不能在類路徑下找到,當你調用拓展方法時類型檢查將會識別出來。對於靜態編譯也一樣。
最後更新:2017-05-22 11:01:59