Groovy 支持兩種元編程:運(yùn)行時(shí)元編程和編譯時(shí)元編程。第一種方式允許在運(yùn)行時(shí)改變類(lèi)模式和程序行為,第二種方式則只發(fā)生在編譯時(shí)。兩種方式都有一定的優(yōu)缺點(diǎn),下面就來(lái)詳細(xì)介紹一下它們。
運(yùn)行時(shí)元編程,可以將一些決策(諸如解析、注入甚至合成類(lèi)和接口的方法)推遲到運(yùn)行時(shí)來(lái)完成。為了深入了解 Groovy 的 MOP,我們需要理解 Groovy 的對(duì)象以及 Groovy 處理方法。在 Groovy 中,我們主要與三類(lèi)對(duì)象打交道:POJO、POGO,還有 Groovy 攔截器。Groovy 的元編程支持所有類(lèi)型的對(duì)象,但是它們采用的方式卻各不相同。
java.lang.Object
且默認(rèn)實(shí)現(xiàn)了 groovy.lang.GroovyObject
接口。 每當(dāng)調(diào)用一個(gè)方法時(shí),Groovy 會(huì)判斷該方法是 POJO 還是 POGO。對(duì)于 POJO 對(duì)象,Groovy 會(huì)從 groovy.lang.MetaClassRegistry
讀取它的 MetaClass
,并委托方法調(diào)用;對(duì)于 POGO 對(duì)象,Groovy 將要采取更多的執(zhí)行步驟,如下圖所示:
http://wiki.jikexueyuan.com/project/groovy-introduction/images/GroovyInterceptions.png" alt="Groovy攔截機(jī)制" />
圖 1 Groovy 攔截機(jī)制
groovy.lang.GroovyObject 是 Groovy 中的關(guān)鍵接口,地位類(lèi)似于 Java 中的 Object
類(lèi)。在 groovy.lang.GroovyObjectSupport 類(lèi)中有一個(gè) GroovyObject
的默認(rèn)實(shí)現(xiàn),負(fù)責(zé)將調(diào)用傳輸給 groovy.lang.MetaClass 對(duì)象。GroovyObject
源看起來(lái)如下所示:
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);
}
invokeMethod
根據(jù)運(yùn)行時(shí)元編程的Schema,當(dāng)你所調(diào)用的方法沒(méi)有在 Groovy 對(duì)象中提供的時(shí)候,調(diào)用該方法。下面這個(gè)例子中,使用了一個(gè)重寫(xiě)的 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 []'
getProperty
與 setProperty
每次對(duì)屬性的讀取都可以通過(guò)重寫(xiě)當(dāng)前對(duì)象的 getProperty()
來(lái)攔截,下面是一個(gè)簡(jiǎn)單的例子:
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'
getMetaClass
和 setMetaClass
可以訪(fǎng)問(wèn)一個(gè)對(duì)象 metaClass
,或者自定義 MetaClass
實(shí)現(xiàn)來(lái)改變默認(rèn)的攔截機(jī)制。比如,你可以自己編寫(xiě) MetaClass
接口的實(shí)現(xiàn),并將它賦予對(duì)象,從而改變攔截機(jī)制。
// getMetaclass
someObject.metaClass
// setMetaClass
someObject.metaClass = new OwnMetaClassImplementation()
你可以在下文的 GroovyInterceptable 主題中看到更多的范例。
該功能與 MetaClass
實(shí)現(xiàn)有關(guān)。在默認(rèn)的實(shí)現(xiàn)中,可以不用調(diào)用 getter 與 setter 而訪(fǎng)問(wèn)字段。下列例子就反映了這種方法。
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'
methodMissing
Groovy 支持 methodMissing
這一概念。該方法與 invokeMethod
的不同之處在于:只有當(dāng)方法分派失敗,找不到指定名稱(chēng)或帶有指定實(shí)參的方法時(shí),才會(huì)調(diào)用該方法。
class Foo {
def methodMissing(String name, def args) {
return "this is me"
}
}
assert new Foo().someUnknownMethod(42l) == 'this is me'
通常,在使用 methodMissing
時(shí),可能會(huì)將結(jié)果緩存起來(lái),以備下次調(diào)用同樣方法時(shí)使用。
比如像下面這樣在 GORM 類(lèi)中的動(dòng)態(tài)查找器。它們是根據(jù) methodMissing
來(lái)實(shí)現(xiàn)的:
class GORM {
def dynamicMethods = [...] // 一些利用正則表達(dá)式的動(dòng)態(tài)方法
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)
}
}
注意,假如找到一個(gè)調(diào)用的方法,就會(huì)立刻使用 ExpandoMetaClass
動(dòng)態(tài)地注冊(cè)一個(gè)新方法。這樣當(dāng)下次調(diào)用同一方法時(shí)就會(huì)更方便。使用 methodMissing
,并不會(huì)產(chǎn)生像調(diào)用 invokeMethod
那么大的開(kāi)銷(xiāo),第二次調(diào)用代價(jià)也并不昂貴。
Groovy 支持 propertyMissing
的概念,用來(lái)攔截失敗的屬性解析嘗試。對(duì) getter 方法而言,propertyMissing
接受一個(gè)包含屬性名的 String
參數(shù):
class Foo {
def propertyMissing(String name) { name }
}
assert new Foo().boo == 'boo'
當(dāng) Groovy 運(yùn)行時(shí)無(wú)法找到指定屬性的 getter 方法時(shí),才會(huì)調(diào)用 propertyMissing(String)
方法。
對(duì)于 setter 方法,可以添加第二個(gè) propertyMissing
定義來(lái)接收一個(gè)附加值參數(shù)。
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"
對(duì)于 methodMissing
來(lái)說(shuō),最佳實(shí)踐應(yīng)該是在運(yùn)行時(shí)動(dòng)態(tài)注冊(cè)新屬性,從而改善總體的查找性能。
另外,處理靜態(tài)方法和屬性的 methodMissing
和 propertyMissing
方法可以通過(guò) ExpandoMetaClass 來(lái)添加。
GroovyInterceptable
groovy.lang.GroovyInterceptable 接口是一種標(biāo)記接口,繼承自超接口 GroovyObject
,用于通知 Groovy 運(yùn)行時(shí)通過(guò)方法分派器機(jī)制時(shí)應(yīng)攔截的方法。
package groovy.lang;
public interface GroovyInterceptable extends GroovyObject {
}
當(dāng) Groovy 對(duì)象實(shí)現(xiàn)了 GroovyInterceptable
接口時(shí),它的 invokeMethod()
方法就會(huì)在任何方法調(diào)用時(shí)調(diào)用。
下面就列舉一個(gè)這種類(lèi)型的方法:
class Interception implements GroovyInterceptable {
def definedMethod() { }
def invokeMethod(String name, Object args) {
'invokedMethod'
}
}
下面這段代碼測(cè)試顯示,無(wú)論方法是否存在,調(diào)用方法都將返回同樣的值。
class InterceptableTest extends GroovyTestCase {
void testCheckInterception() {
def interception = new Interception()
assert interception.definedMethod() == 'invokedMethod'
assert interception.someMethod() == 'invokedMethod'
}
}
我們不能使用默認(rèn)的 Groovy 方法(比如 println
),因?yàn)檫@些方法已經(jīng)被注入到了 Groovy 所有的對(duì)象中,自然會(huì)被攔截。
如果想要攔截所有的方法調(diào)用,但又不想實(shí)現(xiàn) GroovyInterceptable
這個(gè)接口,那么我們可以在一個(gè)對(duì)象的 MetaClass
上實(shí)現(xiàn) invokeMethod()
。該方法同時(shí)適于 POGO 與 POJO 對(duì)象,如下所示:
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'
}
}
參看MetaClasses 一節(jié)內(nèi)容了解 MetaClass
的更多內(nèi)容。
如果一個(gè)不受控制的類(lèi)有額外的方法,在某些情況下反而是有用的。為了實(shí)現(xiàn)這種功能,Groovy 從 Objective-C 那里借用并實(shí)現(xiàn)了一個(gè)概念,叫做:類(lèi)別(Categories)。
類(lèi)別功能是利用類(lèi)別類(lèi)(category classes)來(lái)實(shí)現(xiàn)的。類(lèi)別類(lèi)的特殊之處在于,需要遵循特定的預(yù)定義規(guī)則才能定義擴(kuò)展方法。
系統(tǒng)已經(jīng)包括了一些類(lèi)別,可以為類(lèi)添加相應(yīng)功能,從而使它們?cè)?Groovy 環(huán)境中更為實(shí)用。
類(lèi)別類(lèi)默認(rèn)是不能啟用的。要想使用定義在類(lèi)別類(lèi)中的方法,必須要使用 GDK 所提供的 use
范圍方法,并且可用于每一個(gè) Groovy 對(duì)象實(shí)例內(nèi)部。
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
方法將類(lèi)別類(lèi)作為第一個(gè)形式參數(shù),將一個(gè)閉包代碼段作為第二個(gè)形式參數(shù)。在 Category
中,可以訪(fǎng)問(wèn)類(lèi)別的任何方法。如上述代碼所示,甚至 JDK 的 java.lang.Integer
或 java.util.Date
類(lèi)都可以通過(guò)用戶(hù)定義方法來(lái)豐富與增強(qiáng)。
類(lèi)別不需要直接暴露給用戶(hù)代碼,如下所示:
class JPACategory{
// 下面讓我們無(wú)需通過(guò) JSR 委員會(huì)的支持來(lái)增強(qiáng)JPA EntityManager
static void persistAll(EntityManager em , Object[] entities) { //添加一個(gè)接口保存所有
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 {
//清除所有資源
}
}
// 用戶(hù)代碼。他們經(jīng)常會(huì)在出現(xiàn)異常時(shí)忘記關(guān)閉資源,有些甚至?xí)浱峤?,所以不能指望他們?EntityManager em; //probably injected
transactionContext (em) {
em.persistAll(obj1, obj2, obj3)
// 在這里制定一些邏輯代碼,使范例更合理。
em.persistAll(obj2, obj4, obj6)
}
通過(guò)查看 groovy.time.TimeCategory
類(lèi),我們就會(huì)發(fā)現(xiàn),擴(kuò)展方法都聲明為 static
方法。實(shí)際上,要想使類(lèi)別類(lèi)的方法能成功地添加到 use
代碼段內(nèi)的類(lèi)中,這是類(lèi)別類(lèi)必須滿(mǎn)足的條件之一。
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();
}
// ...
另外一個(gè)必備條件是靜態(tài)方法的第一個(gè)實(shí)參必須定義方法一旦被啟用時(shí),該方法所連接的類(lèi)型;而另一個(gè)實(shí)參則是常見(jiàn)的方法用于形參的實(shí)參。
由于形參和靜態(tài)方法的規(guī)范,類(lèi)別方法定義可能會(huì)比普通方法定義稍微不太直觀。因此,作為替代方案,Groovy 引入了 @Category
標(biāo)記,利用這一標(biāo)記,可在編譯時(shí)將標(biāo)注的類(lèi)轉(zhuǎn)化為類(lèi)別類(lèi)。
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
標(biāo)記的優(yōu)點(diǎn)在于,在使用實(shí)例方法時(shí),可以不需要把目標(biāo)類(lèi)別當(dāng)做第一個(gè)形參。目標(biāo)類(lèi)別類(lèi)作為實(shí)參提供給標(biāo)記使用。
關(guān)于 @Category
的另外介紹,可參看 編譯時(shí)元編程
(待定)
(待定)
(待定)
(待定)
(待定)
Groovy 提供了一種叫做 ExpandoMetaClass
的特殊 MetaClass
。其特殊之處在于,它允許可以使用靈活的閉包語(yǔ)法來(lái)動(dòng)態(tài)添加或改變方法、構(gòu)造函數(shù)、屬性,甚至靜態(tài)方法。
對(duì)于測(cè)試向?qū)?/a>中所展示的模擬或存根情況,使用這些修改會(huì)特別有用。
每一個(gè) Groovy 所提供的 java.lang.Class
都帶有一個(gè)特殊的 metaClass
屬性,它將提供一個(gè) ExpandoMetaClass
實(shí)例的引用。該實(shí)例可用于添加方法或改變已有方法的行為。
默認(rèn)情況下,ExpandoMetaClass
不支持繼承。為了啟用繼承,必須在應(yīng)用程序開(kāi)始運(yùn)作前(比如在 main 方法或 servlet bootstrap 中)就調(diào)用 ExpandoMetaClass#enableGlobally()
。
下面這些內(nèi)容詳細(xì)介紹了 ExpandoMetaClass
在不同情況下的應(yīng)用。
一旦通過(guò)調(diào)用 metaClass
屬性訪(fǎng)問(wèn)了 ExpandoMetaClass
,就可以通過(guò)左移(<<
)或等于號(hào)(=
)操作符來(lái)添加方法。
注意,左移操作符是用于追加(append)一個(gè)新的方法。如果方法已經(jīng)存在,則會(huì)拋出一個(gè)異常。如果需要替代(replace)一個(gè)方法,則需要使用 =
操作符。
下例展示了操作符是如何應(yīng)用于 metaClass
的一個(gè)不存在的屬性上,從而傳入 Closure
代碼塊的一個(gè)實(shí)例的。
class Book {
String title
}
Book.metaClass.titleInUpperCase << {-> title.toUpperCase() }
def b = new Book(title:"The Stand")
assert "THE STAND" == b.titleInUpperCase()
上例顯示,通過(guò)訪(fǎng)問(wèn) metaClass
屬性,可將一個(gè)新方法添加到一個(gè)類(lèi)上,并可使用 <<
或 =
操作符來(lái)指定一個(gè) Closure
代碼塊。Closure
形參被解析為方法形參。形參方法可以通過(guò) {→ …?}
格式來(lái)添加。
ExpandoMetaClass
支持兩種方式來(lái)添加或重寫(xiě)屬性。
首先,只需通過(guò)為 metaClass
賦予一個(gè)值,就可以聲明一個(gè)可變屬性(mutable property):
class Book {
String title
}
Book.metaClass.author = "Stephen King"
def b = new Book()
assert "Stephen King" == b.author
另一個(gè)方式是,通過(guò)使用添加實(shí)例方法的標(biāo)準(zhǔn)機(jī)制來(lái)添加 getter 和(或)setter 方法:
class Book {
String title
}
Book.metaClass.getAuthor << {-> "Stephen King" }
def b = new Book()
assert "Stephen King" == b.author
在上述源代碼實(shí)例中,屬性由閉包所指定,并且是一個(gè)只讀屬性。添加一個(gè)相等的 setter 方法也是可行的,但屬性值需要存儲(chǔ)起來(lái)以備后續(xù)使用。這種做法可以參照下面的例子:
class Book {
String title
}
def properties = Collections.synchronizedMap([:])
Book.metaClass.setAuthor = { String value ->
properties[System.identityHashCode(delegate) + "author"] = value
}
Book.metaClass.getAuthor = {->
properties[System.identityHashCode(delegate) + "author"]
}
但這并不是唯一的辦法。比如在一個(gè) servlet 容器中,將當(dāng)前執(zhí)行請(qǐng)求中的值當(dāng)作請(qǐng)求屬性保存起來(lái)(就像 Grails 中的某些情況一樣)。
構(gòu)造函數(shù)可以通過(guò)特殊的 constructor
屬性來(lái)添加。<<
或 =
操作符都可以用于指定 Closure
代碼段。當(dāng)代碼在運(yùn)行時(shí)執(zhí)行時(shí),Closure
實(shí)參會(huì)成為構(gòu)造函數(shù)的實(shí)參。
class Book {
String title
}
Book.metaClass.constructor << { String title -> new Book(title:title) }
def book = new Book('Groovy in Action - 2nd Edition')
assert book.title == 'Groovy in Action - 2nd Edition'
但在添加構(gòu)造函數(shù)時(shí)要格外注意,因?yàn)檫@極易造成棧溢出。
添加靜態(tài)方法的方法與添加實(shí)例方法基本一樣,只不過(guò)要在方法名前加上 static
修飾符。
class Book {
String title
}
Book.metaClass.static.create << { String title -> new Book(title:title) }
def b = Book.create("The Stand")
利用 ExpandoMetaClass
,可以使用 Groovy 方法點(diǎn)標(biāo)記法從其他類(lèi)中借用方法。
class Person {
String name
}
class MortgageLender {
def borrowMoney() {
"buy house"
}
}
def lender = new MortgageLender()
Person.metaClass.buyHouse = lender.&borrowMoney
def p = new Person()
assert "buy house" == p.buyHouse()
在 Groovy 中,既然可以使用字符串作為屬性名,那么反過(guò)來(lái),也可以在運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建方法與屬性名。要想創(chuàng)建具有動(dòng)態(tài)名稱(chēng)的方法,只需使用將字符串引用為屬性名的語(yǔ)言特性。
class Person {
String name = "Fred"
}
def methodName = "Bob"
Person.metaClass."changeNameTo${methodName}" = {-> delegate.name = "Bob" }
def p = new Person()
assert "Fred" == p.name
p.changeNameToBob()
assert "Bob" == p.name
同樣的概念可以應(yīng)用于靜態(tài)方法與屬性。
Grails Web 應(yīng)用框架可以算是動(dòng)態(tài)方法名的一個(gè)應(yīng)用范例?!皠?dòng)態(tài)編解碼器”的概念正是通過(guò)動(dòng)態(tài)方法名來(lái)實(shí)現(xiàn)的。
HTMLCodec
類(lèi)
class HTMLCodec {
static encode = { theTarget ->
HtmlUtils.htmlEscape(theTarget.toString())
}
static decode = { theTarget ->
HtmlUtils.htmlUnescape(theTarget.toString())
}
}
上例實(shí)現(xiàn)了一個(gè)編解碼器。Grails 提供了多種編解碼器實(shí)現(xiàn),每種實(shí)現(xiàn)都定義在一個(gè)類(lèi)中。在運(yùn)行時(shí),會(huì)在應(yīng)用類(lèi)路徑上出現(xiàn)多個(gè)編解碼器類(lèi)。在應(yīng)用啟動(dòng)時(shí),框架會(huì)將 encodeXXX
和 decodeXXX
方法添加到特定的元類(lèi)中,這里的 XXX
是指編解碼器類(lèi)名的前面部分(如 encodeHTML
)。下面采用了一些 Groovy 偽碼來(lái)表示這種機(jī)制:
def codecs = classes.findAll { it.name.endsWith('Codec') }
codecs.each { codec ->
Object.metaClass."encodeAs${codec.name-'Codec'}" = { codec.newInstance().encode(delegate) }
Object.metaClass."decodeFrom${codec.name-'Codec'}" = { codec.newInstance().decode(delegate) }
}
def html = '<html><body>hello</body></html>'
assert '<html><body>hello</body></html>' == html.encodeAsHTML()
在運(yùn)行時(shí)階段執(zhí)行某個(gè)方法時(shí),還有其他什么方法或?qū)傩源嬖??這個(gè)問(wèn)題往往是非常有用的。ExpandoMetaClass
提供了下列方法(截止目前):
getMetaMethod
hasMetaMethod
getMetaProperty
hasMetaProperty
為什么不能單純使用反射呢?因?yàn)?Groovy 的獨(dú)特性——它包含兩種方法,一種是“真正”的方法,而另一種則是只在運(yùn)行時(shí)才能獲取并使用的方法。后者有時(shí)(但也并不總是被)稱(chēng)為元方法(MetaMethods)。元方法告訴我們?cè)谶\(yùn)行時(shí)究竟能夠使用何種方法,從而使代碼能夠適應(yīng)。
這一點(diǎn)特別適用于重寫(xiě) invokeMethod
、getProperty
和/或 setProperty
時(shí)。
ExpandoMetaClass
的另一個(gè)特性是能夠允許重寫(xiě) invokeMethod
、getProperty
和 setProperty
。groovy.lang.GroovyObject
類(lèi)中能找到這三個(gè)方法。
下面范例展示了如何重寫(xiě) invokeMethod
:
class Stuff {
def invokeMe() { "foo" }
}
Stuff.metaClass.invokeMethod = { String name, args ->
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()
重寫(xiě)靜態(tài)方法的邏輯跟之前我們見(jiàn)過(guò)的重寫(xiě)實(shí)例方法的邏輯基本相同,唯一不同之處在于對(duì) metaClass.static
屬性的訪(fǎng)問(wèn),以及為了獲取靜態(tài) MetaMethod
實(shí)例而對(duì) getStaticMethodName
的調(diào)用。
invokeMethod
ExpandoMetaClass
甚至可以允許利用一種特殊的 invokeMethod
格式重寫(xiě)靜態(tài)方法。
class Stuff {
static invokeMe() { "foo" }
}
Stuff.metaClass.'static'.invokeMethod = { String name, args ->
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()
可以利用 ExpandoMetaClass
為接口添加方法,但要想這樣做,必須在應(yīng)用啟動(dòng)前使用 ExpandoMetaClass.enableGlobally()
方法實(shí)施全局啟用。
List.metaClass.sizeDoubled = {-> delegate.size() * 2 }
def list = []
list << 1
list << 2
assert 4 == list.sizeDoubled()
利用擴(kuò)展模塊,可以為現(xiàn)有類(lèi)添加新方法,這些類(lèi)中可以包括 JDK 中那樣的預(yù)編譯類(lèi)。這些新方法與通過(guò)元類(lèi)或類(lèi)別定義的方法不同,它們是全局可用的。比如當(dāng)你編寫(xiě):
標(biāo)準(zhǔn)擴(kuò)展方法
def file = new File(...)
def contents = file.getText('utf-8')
File
類(lèi)中并不存在 getText
方法,但 Groovy 知道它的定義是在一個(gè)特殊類(lèi)中 ResourceGroovyMethods
:
ResourceGroovyMethods.java
public static String getText(File file, String charset) throws IOException {
return IOGroovyMethods.getText(newReader(file, charset));
}
你可能還注意到擴(kuò)展方法是在“輔助”類(lèi)(定義了多種擴(kuò)展方法)中通過(guò)一個(gè)靜態(tài)方法來(lái)定義的。getText
的第一個(gè)實(shí)參對(duì)應(yīng)著接受者,而另一個(gè)形參則對(duì)應(yīng)著擴(kuò)展方法的實(shí)參。因此,我們才在 File
類(lèi)(因?yàn)榈谝粋€(gè)實(shí)參是 File
類(lèi)型)中定義了一個(gè)名為 getText
的方法,它只傳遞了一個(gè)實(shí)參(String
類(lèi)型的編碼)。
創(chuàng)建擴(kuò)展模塊的過(guò)程非常簡(jiǎn)單:
然后,還必須讓 Groovy 能找到該擴(kuò)展模塊,這只需將擴(kuò)展模塊類(lèi)和描述符放入類(lèi)路徑即可。這意味著有以下兩種方法:
擴(kuò)展模塊可以為類(lèi)添加兩種方法:
為現(xiàn)有類(lèi)添加實(shí)例方法,需要?jiǎng)?chuàng)建一個(gè)擴(kuò)展類(lèi)。比如想在 Integer
上加一個(gè) maxRetries
方法,可以采取下面的方式:
MaxRetriesExtension.groovy
class MaxRetriesExtension { //1??
static void maxRetries(Integer self, Closure code) { //2??
int retries = 0
Throwable e
while (retries<self) {
try {
code.call()
break
} catch (Throwable err) {
e = err
retries++
}
}
if (retries==0 && e) {
throw e
}
}
}
1?? 擴(kuò)展類(lèi)
2?? 靜態(tài)方法的第一個(gè)實(shí)際參數(shù)對(duì)應(yīng)著消息的接受者,也就是擴(kuò)展實(shí)例。
然后,在已經(jīng)聲明了擴(kuò)展類(lèi)之后,你可以這樣調(diào)用它:
int i=0
5.maxRetries {
i++
}
assert i == 1
i=0
try {
5.maxRetries {
throw new RuntimeException("oops")
}
} catch (RuntimeException e) {
assert i == 5
}
也可以為類(lèi)添加靜態(tài)方法。這種情況下,靜態(tài)方法需要在自己的文件中定義。
StaticStringExtension.groovy
class StaticStringExtension { // 1??
static String greeting(String self) { // 2??
'Hello, world!'
}
}
1?? 靜態(tài)擴(kuò)展類(lèi) 2?? 靜態(tài)方法的第一個(gè)實(shí)參對(duì)應(yīng)著將要擴(kuò)展并且還未使用的類(lèi)
在這種情況下,可以直接在 String
類(lèi)中調(diào)用它:
assert String.greeting() == 'Hello, world!'
為了使 Groovy 能夠加載擴(kuò)展方法,你必須聲明擴(kuò)展輔助類(lèi)。必須在 META-INF/services
目錄中創(chuàng)建一個(gè)名為 org.codehaus.groovy.runtime.ExtensionModule
的文件。
org.codehaus.groovy.runtime.ExtensionModule
moduleName=Test module for specifications
moduleVersion=1.0-test
extensionClasses=support.MaxRetriesExtension
staticExtensionClasses=support.StaticStringExtension
該模塊描述符需要 4 個(gè)鍵:
注意,模塊并不一定要既能定義靜態(tài)輔助類(lèi),又能定義實(shí)例輔助類(lèi)。你可以在一個(gè)模塊中添加幾個(gè)類(lèi),也可以在單一模塊中擴(kuò)展不同的類(lèi),甚至還可以在單一的擴(kuò)展類(lèi)中使用不同的類(lèi),但強(qiáng)烈建議將擴(kuò)展方法按功能集分入不同的類(lèi)。
值得注意的是,不能在代碼使用已編譯擴(kuò)展模塊的時(shí)候,你無(wú)法使用它。這意味著,要想使用擴(kuò)展模塊,在將要使用它的代碼被編譯前,它就必須以已編譯類(lèi)的形式出現(xiàn)在類(lèi)路徑上。這其實(shí)就是說(shuō),與擴(kuò)展類(lèi)同一源單位中不能出現(xiàn)測(cè)試類(lèi)(test class),然而,測(cè)試源通常在實(shí)際中與常規(guī)源是分開(kāi)的,在構(gòu)建的另一個(gè)步驟中執(zhí)行,所以這根本不會(huì)造成任何不良影響。
與類(lèi)別不同的是,擴(kuò)展模塊與類(lèi)型檢查是兼容的:如果在類(lèi)路徑上存在這些模塊,類(lèi)檢查器就會(huì)知道擴(kuò)展方法,并不會(huì)說(shuō)明調(diào)用的時(shí)間。它們與靜態(tài)編譯也是兼容的。
在 Groovy 中,編譯時(shí)元編程能夠容許編譯時(shí)生成代碼。這種轉(zhuǎn)換會(huì)影響程序的抽象語(yǔ)法樹(shù)(AST,Abstract Syntax Tree),這也就是我們?cè)?Groovy 中把它成為 AST 轉(zhuǎn)換的原因。AST 轉(zhuǎn)換能使我們實(shí)時(shí)了解編譯過(guò)程,繼而修改 AST,從而繼續(xù)編譯過(guò)程,生成常規(guī)的字節(jié)碼。與運(yùn)行時(shí)元編程相比,在類(lèi)文件自身中(也就是說(shuō),在字節(jié)碼內(nèi))就可以看到變化。這一點(diǎn)是非常重要的,比如說(shuō)當(dāng)你想讓轉(zhuǎn)換成為類(lèi)抽象一部分時(shí)(實(shí)現(xiàn)接口,繼承抽象類(lèi),等等),或者甚至當(dāng)需要讓類(lèi)可從 Java (或其他的 JVM 語(yǔ)言)中調(diào)用時(shí)。例如,AST 轉(zhuǎn)換可以為一個(gè)類(lèi)添加一些方法。如果用運(yùn)行時(shí)元編程來(lái)實(shí)現(xiàn)的話(huà),新方法只能可見(jiàn)于 Groovy;而用編譯時(shí)元編程來(lái)實(shí)現(xiàn),新方法也可以在 Java 中顯現(xiàn)出來(lái)。最后一點(diǎn)也同樣重要,編譯時(shí)元編程的性能要好過(guò)運(yùn)行時(shí)元編程(因?yàn)椴辉傩枰跏蓟^(guò)程)。
本節(jié)中,我們將要探討與 Groovy 分發(fā)版所綁定的各種編譯時(shí)轉(zhuǎn)換,而在隨后的一節(jié)中,再來(lái)介紹如何實(shí)現(xiàn)自定義的 AST 轉(zhuǎn)換,以及這一技術(shù)的優(yōu)點(diǎn)。
Groovy 有很多可用的 AST 轉(zhuǎn)換,它們可滿(mǎn)足不同的需求:減少樣板文件(代碼生成),實(shí)現(xiàn)設(shè)計(jì)模式(委托等模式),記錄日志,聲明并發(fā),克隆,更安全地記錄腳本,編譯微調(diào),實(shí)現(xiàn) Swing 模式,測(cè)試并最終管理各種依賴(lài)。如果發(fā)現(xiàn)沒(méi)有任何一個(gè)轉(zhuǎn)換能夠滿(mǎn)足特定需求,還可以自定義轉(zhuǎn)換,詳情請(qǐng)看:開(kāi)發(fā)自定義 AST 轉(zhuǎn)換。
AST 轉(zhuǎn)換可分為兩大類(lèi):
Groovy 并不帶有任何的全局 AST 轉(zhuǎn)換,但你可以在這里找到一些可用的本地 AST 轉(zhuǎn)換:
這一類(lèi)轉(zhuǎn)換包含能夠去除樣板文件代碼的 AST 轉(zhuǎn)換。樣板文件代碼通常是一種必須編寫(xiě)然而又沒(méi)有任何有用信息的代碼。通過(guò)自動(dòng)生成這種樣板文件代碼,剩下必須要寫(xiě)的代碼就變得清晰而簡(jiǎn)潔起來(lái),從而就減少了因?yàn)闃影逦募a不正確而引入的錯(cuò)誤。
@ToString
AST 轉(zhuǎn)換能夠生成人類(lèi)可讀的類(lèi)的 toString
形式。比如,像下面這樣注解 Person
類(lèi)會(huì)自動(dòng)為你生成 toString
方法。
import groovy.transform.ToString
@ToString
class Person {
String firstName
String lastName
}
根據(jù)這種定義,下列斷言就得以通過(guò),意味著已經(jīng)生成了一個(gè) toString
方法,它會(huì)從類(lèi)中獲取字段值,并將它們打印出來(lái)。
def p = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p.toString() == 'Person(Jack, Nicholson)'
@ToString
標(biāo)注接受以下列表中顯示的幾個(gè)參數(shù)。
屬性 | 默認(rèn)值 | 描述 | 范例 |
---|---|---|---|
includeNames | false | 是否在生成的 toString 中包含屬性名 | @ToString(includeNames=true) class Person { String firstName String lastName } def p = new Person(firstName: 'Jack', lastName: 'Nicholson') assert p.toString() == 'Person(firstName:Jack, lastName:Nicholson)' |
excludes | 空列表 | 從 toString 中排除的屬性列表 | @ToString(excludes=['firstName']) class Person { String firstName String lastName } def p = new Person(firstName: 'Jack', lastName: 'Nicholson') assert p.toString() == 'Person(Nicholson)' |
includes | 空列表 | toString 中包含的字段列表 | @ToString(includes=['lastName']) class Person { String firstName String lastName } def p = new Person(firstName: 'Jack', lastName: 'Nicholson') assert p.toString() == 'Person(Nicholson)' |
includeSuper | False | 超類(lèi)是否應(yīng)在 toString 中 | @ToString class Id { long id } @ToString(includeSuper=true) class Person extends Id { String firstName String lastName } def p = new Person(id:1, firstName: 'Jack', lastName: 'Nicholson') assert p.toString() == 'Person(Jack, Nicholson, Id(1))' |
includeSuperProperties | False | 超屬性是否應(yīng)包含在 toString 中 | class Person { String name } @ToString(includeSuperProperties = true, includeNames = true) class BandMember extends Person { String bandName } def bono = new BandMember(name:'Bono', bandName: 'U2').toString() assert bono.toString() == 'BandMember(bandName:U2, name:Bono)' |
includeFields | False | 除了屬性之外,字段是否應(yīng)包括在 toString 中 | @ToString(includeFields=true) class Person { String firstName String lastName private int age void test() { age = 42 } } def p = new Person(firstName: 'Jack', lastName: 'Nicholson') p.test() assert p.toString() == 'Person(Jack, Nicholson, 42)' |
ignoreNulls | False | 是否應(yīng)顯示帶有 null 值的屬性/字段 | @ToString(ignoreNulls=true) class Person { String firstName String lastName } def p = new Person(firstName: 'Jack') assert p.toString() == 'Person(Jack)' |
includePackage | False | 在 toString 中使用完全限定的類(lèi)名,而非簡(jiǎn)單類(lèi)名 | @ToString(includePackage=true) class Person { String firstName String lastName } def p = new Person(firstName: 'Jack', lastName:'Nicholson') assert p.toString() == 'acme.Person(Jack, Nicholson)' |
cache | False | 緩存 toString 字符串。如果類(lèi)不可變,是否應(yīng)只設(shè)為 true | @ToString(cache=true) class Person { String firstName String lastName } def p = new Person(firstName: 'Jack', lastName:'Nicholson') def s1 = p.toString() def s2 = p.toString() assert s1 == s2 assert s1 == 'Person(Jack, Nicholson)' assert s1.is(s2) // 同一實(shí)例 |
@EqualsAndHashCode
AST 轉(zhuǎn)換主要目的是為了生成 equals
和 hashCode
方法。生成的散列碼遵循 Josh Bloch 所著的 Effective Java 中所介紹的最佳實(shí)踐:
import groovy.transform.EqualsAndHashCode
@EqualsAndHashCode
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
def p2 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1==p2
assert p1.hashCode() == p2.hashCode()
下面是一些用來(lái)調(diào)整 @EqualsAndHashCode
行為的選項(xiàng):
屬性 | 默認(rèn)值 | 描述 | 范例 |
excludes | 空列表 | 從 equals / hashCode 中需要排除的屬性列表 | import groovy.transform.EqualsAndHashCode @EqualsAndHashCode(excludes=['firstName']) class Person { String firstName String lastName } def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson') def p2 = new Person(firstName: 'Bob', lastName: 'Nicholson') assert p1==p2 assert p1.hashCode() == p2.hashCode() |
includes | 空列表 | equals/hashCode 所包括的字段列表 | import groovy.transform.EqualsAndHashCode @EqualsAndHashCode(includes=['lastName']) class Person { String firstName String lastName } def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson') def p2 = new Person(firstName: 'Bob', lastName: 'Nicholson') assert p1==p2 assert p1.hashCode() == p2.hashCode() |
callSuper | False | 在 equals 或 hashcode 計(jì)算中是否包含 super | import groovy.transform.EqualsAndHashCode @EqualsAndHashCode class Living { String race } @EqualsAndHashCode(callSuper=true) class Person extends Living { String firstName String lastName } def p1 = new Person(race:'Human', firstName: 'Jack', lastName: 'Nicholson') def p2 = new Person(race: 'Human beeing', firstName: 'Jack', lastName: 'Nicholson') assert p1!=p2 assert p1.hashCode() != p2.hashCode() |
includeFields | False | 除了屬性之外,是否應(yīng)將字段包含在 equals / hashCode 之中 | @ToString(includeFields=true) class Person { String firstName String lastName private int age void test() { age = 42 } } def p = new Person(firstName: 'Jack', lastName: 'Nicholson') p.test() assert p.toString() == 'Person(Jack, Nicholson, 42)' |
cache | False | 緩存 hashCode 計(jì)算。如果類(lèi)不可改變,是否只應(yīng)將其設(shè)為 true。 | @ToString(cache=true) class Person { String firstName String lastName } def p = new Person(firstName: 'Jack', lastName:'Nicholson') def s1 = p.toString() def s2 = p.toString() assert s1 == s2 assert s1 == 'Person(Jack, Nicholson)' assert s1.is(s2) // 同一實(shí)例 |
useCanEqual | True | equals 是否應(yīng)調(diào)用 canEqual 輔助方法 | 參看 http://www.artima.com/lejava/articles/equality.html |
@TupleCo