執行時間和編譯時間元程式設計
Groovy 語言支援兩種元程式設計:執行時間和編譯時間。第一種允許在執行時間變更類別模型和程式行為,而第二種只在編譯時間發生。兩者都有優缺點,我們將在本節詳細說明。
1. 執行時間元程式設計
透過執行時間元程式設計,我們可以將攔截、注入甚至合成類別和介面的方法決策延後到執行時間。為了深入了解 Groovy 的元物件協定 (MOP),我們需要了解 Groovy 物件和 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。對於 POJO,Groovy 會從 groovy.lang.MetaClassRegistry 中取得其 MetaClass
並將方法呼叫委派給它。對於 POGO,Groovy 會採取更多步驟,如下圖所示

1.1. GroovyObject 介面
groovy.lang.GroovyObject 是 Groovy 中的主要介面,就像 Object
類別在 Java 中一樣。GroovyObject
在 groovy.lang.GroovyObjectSupport 類別中有一個預設實作,負責將呼叫傳輸到 groovy.lang.MetaClass 物件。GroovyObject
來源看起來像這樣
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);
}
1.1.1. invokeMethod
此方法主要用於與 GroovyInterceptable 介面或物件的 MetaClass
結合使用,它會攔截所有方法呼叫。
當在 Groovy 物件上呼叫的方法不存在時,也會呼叫它。以下是使用覆寫的 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 []'
不過,不建議使用 invokeMethod
來攔截遺失的方法。如果目的是僅在方法傳送失敗時攔截方法呼叫,請改用 methodMissing。
1.1.2. get/setProperty
可以透過覆寫目前物件的 getProperty()
方法來攔截對屬性的每個讀取存取。以下是簡單的範例
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'
1 | 將要求轉送給 field3 以外所有屬性的 getter。 |
可以透過覆寫 setProperty()
方法來攔截對屬性的寫入存取
class POGO {
String property
void setProperty(String name, Object value) {
this.@"$name" = 'overridden'
}
}
def pogo = new POGO()
pogo.property = 'a'
assert pogo.property == 'overridden'
1.1.3. get/setMetaClass
可以存取物件的 metaClass
或設定自己的 MetaClass
實作,以變更預設的攔截機制。例如,可以撰寫自己的 MetaClass
介面實作,並將它指定給物件,以變更攔截機制
// getMetaclass
someObject.metaClass
// setMetaClass
someObject.metaClass = new OwnMetaClassImplementation()
可以在 GroovyInterceptable 主題中找到其他範例。 |
1.2. get/setAttribute
此功能與 MetaClass
實作相關。在預設實作中,可以在不呼叫 getter 和 setter 的情況下存取欄位。以下範例示範此方法
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'
1.3. methodMissing
Groovy 支援 methodMissing
的概念。此方法與 invokeMethod
不同,因為它只在方法傳送失敗時呼叫,也就是找不到具有給定名稱和/或給定引數的方法時
class Foo {
def methodMissing(String name, def args) {
return "this is me"
}
}
assert new Foo().someUnknownMethod(42l) == 'this is me'
通常在使用 methodMissing
時,可以快取結果,以便下次呼叫相同方法時使用。
例如,考慮 GORM 中的動態尋找器。這些尋找器是以 methodMissing
的方式實作。程式碼類似於下列內容
class GORM {
def dynamicMethods = [...] // an array of dynamic methods that use regex
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)
}
}
請注意,如果找到要呼叫的方法,我們會使用 ExpandoMetaClass 動態註冊一個新的方法。這樣做的目的是,下次呼叫相同方法時,會更有效率。使用 methodMissing
的這種方式沒有 invokeMethod
的負擔,而且從第二次呼叫開始,負擔也不大。
1.4. propertyMissing
Groovy 支援 propertyMissing
的概念,用於攔截屬性解析嘗試失敗的情況。對於 getter 方法,propertyMissing
會取得包含屬性名稱的單一 String
引數
class Foo {
def propertyMissing(String name) { name }
}
assert new Foo().boo == 'boo'
propertyMissing(String)
方法僅在 Groovy 執行時期找不到給定屬性的 getter 方法時才會呼叫。
對於 setter 方法,可以新增第二個 propertyMissing
定義,其中包含一個額外的值引數
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"
與 methodMissing
一樣,最佳做法是在執行時期動態註冊新屬性,以改善整體查詢效能。
1.5. static methodMissing
methodMissing
方法的靜態變體可透過 ExpandoMetaClass 新增,或可使用 $static_methodMissing
方法在類別層級實作。
class Foo {
static def $static_methodMissing(String name, Object args) {
return "Missing static method name is $name"
}
}
assert Foo.bar() == 'Missing static method name is bar'
1.6. static propertyMissing
propertyMissing
方法的靜態變體可透過 ExpandoMetaClass 新增,或可使用 $static_propertyMissing
方法在類別層級實作。
class Foo {
static def $static_propertyMissing(String name) {
return "Missing static property name is $name"
}
}
assert Foo.foobar == 'Missing static property name is foobar'
1.7. GroovyInterceptable
groovy.lang.GroovyInterceptable 介面是擴充 GroovyObject
的標記介面,用於通知 Groovy 執行時期,應透過 Groovy 執行時期的方法調用機制攔截所有方法。
package groovy.lang;
public interface GroovyInterceptable extends GroovyObject {
}
當 Groovy 物件實作 GroovyInterceptable
介面時,其 invokeMethod()
會在任何方法呼叫中呼叫。以下您可以看到此類型的物件的簡單範例
class Interception implements GroovyInterceptable {
def definedMethod() { }
def invokeMethod(String name, Object args) {
'invokedMethod'
}
}
下一段程式碼是一個測試,顯示呼叫現有和不存在的方法都會傳回相同的值。
class InterceptableTest extends GroovyTestCase {
void testCheckInterception() {
def interception = new Interception()
assert interception.definedMethod() == 'invokedMethod'
assert interception.someMethod() == 'invokedMethod'
}
}
我們無法使用預設的 groovy 方法,例如 println ,因為這些方法會注入到所有 Groovy 物件中,所以也會被攔截。
|
如果我們想要攔截所有方法呼叫,但不想實作 GroovyInterceptable
介面,我們可以在物件的 MetaClass
上實作 invokeMethod()
。此方法適用於 POGO 和 POJO,如下例所示
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'
}
}
有關 MetaClass 的其他資訊,請參閱 MetaClasses 區段。
|
1.8. Categories
在某些情況下,如果一個不受控的類別具有其他方法,會很有用。為了啟用此功能,Groovy 實作了一個從 Objective-C 借來的功能,稱為 Categories。
Categories 是使用所謂的 類別類別 來實作的。類別類別的特殊之處在於,它需要符合某些預先定義的規則來定義擴充方法。
系統中包含一些類別,用於新增功能到類別中,讓它們在 Groovy 環境中更易於使用
類別範疇預設並未啟用。要使用類別範疇中定義的方法,必須套用 GDK 提供且在每個 Groovy 物件實例中可用的範圍化 use
方法
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
方法會將類別範疇作為第一個參數,並將封閉式程式碼區塊作為第二個參數。在 Closure
內部,可以使用類別範疇方法。如上方的範例所示,甚至連 JDK 類別(例如 java.lang.Integer
或 java.util.Date
)都可以使用使用者定義的方法進行豐富化。
類別範疇不需要直接公開給使用者程式碼,下列範例也適用
class JPACategory{
// Let's enhance JPA EntityManager without getting into the JSR committee
static void persistAll(EntityManager em , Object[] entities) { //add an interface to save all
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 {
//cleanup your resource here
}
}
// user code, they always forget to close resource in exception, some even forget to commit, let's not rely on them.
EntityManager em; //probably injected
transactionContext (em) {
em.persistAll(obj1, obj2, obj3)
// let's do some logics here to make the example sensible
em.persistAll(obj2, obj4, obj6)
}
當我們檢視 groovy.time.TimeCategory
類別時,會發現擴充方法全部都宣告為 static
方法。事實上,這是類別範疇類別必須符合的要求之一,才能在 use
程式碼區塊內將其方法成功新增至類別
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();
}
// ...
另一個要求是靜態方法的第一個引數必須定義方法在啟用後附加到的類型。其他引數是方法會作為參數接收的正常引數。
由於參數和靜態方法慣例,類別範疇方法定義可能比一般方法定義略不直觀。作為替代方案,Groovy 附帶 @Category
註解,可在編譯時將帶註解的類別轉換為類別範疇類別。
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
註解的優點是可以使用實例方法,而不需要目標類型作為第一個參數。目標類型類別會作為引數提供給註解。
在 編譯時元程式設計 區段中有一個關於 @Category 的不同區段。
|
1.9. 元類別
如前所述,元類別在方法解析中扮演著核心角色。對於 Groovy 程式碼的每個方法呼叫,Groovy 都會找到給定物件的 MetaClass
,並透過 groovy.lang.MetaClass#invokeMethod(java.lang.Class,java.lang.Object,java.lang.String,java.lang.Object,boolean,boolean) 將方法解析委派給元類別,這不應與 groovy.lang.GroovyObject#invokeMethod(java.lang.String,java.lang.Object) 混淆,後者碰巧是元類別最終可能會呼叫的方法。
1.9.1. 預設元類別 MetaClassImpl
預設情況下,物件會取得實作預設方法查詢的 MetaClassImpl
實例。此方法查詢包括在物件類別中查詢方法(「一般」方法),但如果未以這種方式找到方法,它會改為呼叫 methodMissing
,最後呼叫 groovy.lang.GroovyObject#invokeMethod(java.lang.String,java.lang.Object)
class Foo {}
def f = new Foo()
assert f.metaClass =~ /MetaClassImpl/
1.9.2. 自訂元類別
您可以變更任何物件或類別的 metaclass,並以自訂的 MetaClass
groovy.lang.MetaClass 實作取代。通常您會想要延伸現有的 metaclass 之一,例如 MetaClassImpl
、DelegatingMetaClass
、ExpandoMetaClass
或 ProxyMetaClass
;否則您將需要實作完整的 method lookup 邏輯。在使用新的 metaclass 實例之前,您應該呼叫 groovy.lang.MetaClass#initialize(),否則 metaclass 可能會或可能不會按照預期運作。
委派 metaclass
如果您只需要裝飾現有的 metaclass,DelegatingMetaClass
會簡化該使用案例。舊的 metaclass 實作仍然可透過 super
存取,讓您能輕鬆地將事前轉換套用至輸入、路由至其他方法,以及對輸出進行後處理。
class Foo { def bar() { "bar" } }
class MyFooMetaClass extends DelegatingMetaClass {
MyFooMetaClass(MetaClass metaClass) { super(metaClass) }
MyFooMetaClass(Class theClass) { super(theClass) }
Object invokeMethod(Object object, String methodName, Object[] args) {
def result = super.invokeMethod(object,methodName.toLowerCase(), args)
result.toUpperCase();
}
}
def mc = new MyFooMetaClass(Foo.metaClass)
mc.initialize()
Foo.metaClass = mc
def f = new Foo()
assert f.BAR() == "BAR" // the new metaclass routes .BAR() to .bar() and uppercases the result
Magic package
您可以透過提供 metaclass 特製 (magic) 的類別名稱和套件名稱,在啟動時變更 metaclass。若要變更 java.lang.Integer
的 metaclass,只要將類別 groovy.runtime.metaclass.java.lang.IntegerMetaClass
放入 classpath 中即可。這很有用,例如在使用架構時,如果您想在架構執行您的程式碼之前進行 metaclass 變更。Magic package 的一般形式為 groovy.runtime.metaclass.[package].[class]MetaClass
。在以下範例中,[package]
是 java.lang
,而 [class]
是 Integer
// file: IntegerMetaClass.groovy
package groovy.runtime.metaclass.java.lang;
class IntegerMetaClass extends DelegatingMetaClass {
IntegerMetaClass(MetaClass metaClass) { super(metaClass) }
IntegerMetaClass(Class theClass) { super(theClass) }
Object invokeMethod(Object object, String name, Object[] args) {
if (name =~ /isBiggerThan/) {
def other = name.split(/isBiggerThan/)[1].toInteger()
object > other
} else {
return super.invokeMethod(object,name, args);
}
}
}
使用 groovyc IntegerMetaClass.groovy
編譯上述檔案後,將會產生 ./groovy/runtime/metaclass/java/lang/IntegerMetaClass.class
。以下範例將使用這個新的 metaclass
// File testInteger.groovy
def i = 10
assert i.isBiggerThan5()
assert !i.isBiggerThan15()
println i.isBiggerThan5()
使用 groovy -cp . testInteger.groovy
執行該檔案後,IntegerMetaClass
將會在 classpath 中,因此它將成為 java.lang.Integer
的 metaclass,攔截對 isBiggerThan*()
方法的 method calls。
1.9.3. 每個實例 metaclass
您可以分別變更個別物件的 metaclass,因此可以讓同一個類別有多個物件具有不同的 metaclass。
class Foo { def bar() { "bar" }}
class FooMetaClass extends DelegatingMetaClass {
FooMetaClass(MetaClass metaClass) { super(metaClass) }
Object invokeMethod(Object object, String name, Object[] args) {
super.invokeMethod(object,name,args).toUpperCase()
}
}
def f1 = new Foo()
def f2 = new Foo()
f2.metaClass = new FooMetaClass(f2.metaClass)
assert f1.bar() == "bar"
assert f2.bar() == "BAR"
assert f1.metaClass =~ /MetaClassImpl/
assert f2.metaClass =~ /FooMetaClass/
assert f1.class.toString() == "class Foo"
assert f2.class.toString() == "class Foo"
1.9.4. ExpandoMetaClass
Groovy 附帶一個特殊的 MetaClass
,稱為 ExpandoMetaClass
。它的特別之處在於,它允許使用簡潔的 closure 語法動態新增或變更方法、建構函式、屬性和甚至靜態方法。
套用這些修改在模擬或 stubbing 情境中特別有用,如 測試指南 中所示。
Groovy 會為每個 java.lang.Class
提供一個特殊的 metaClass
屬性,它會提供您一個參考到 ExpandoMetaClass
實例。然後可以使用這個實例來新增方法或變更現有方法的行為。
預設情況下,ExpandoMetaClass 沒有繼承。若要啟用此功能,您必須在應用程式啟動之前呼叫 ExpandoMetaClass#enableGlobally() ,例如在 main 方法或 servlet bootstrap 中。
|
以下各節將詳細說明如何在各種情況下使用 ExpandoMetaClass
。
方法
呼叫 metaClass
屬性存取 ExpandoMetaClass
後,可以使用左移運算子 <<
或 =
運算子新增方法。
請注意,左移運算子用於附加新方法。如果類別或介面宣告了具有相同名稱和參數類型的公開方法,包括從超類別和超介面繼承的方法,但不包括在執行階段新增至 metaClass 的方法,系統會擲回例外。如果您要取代類別或介面宣告的方法,可以使用 = 運算子。
|
這些運算子會套用在 metaClass
的不存在屬性上,並傳遞 Closure
程式碼區塊的執行個體。
class Book {
String title
}
Book.metaClass.titleInUpperCase << {-> title.toUpperCase() }
def b = new Book(title:"The Stand")
assert "THE STAND" == b.titleInUpperCase()
上述範例顯示如何透過存取 metaClass
屬性,並使用 <<
或 =
運算子指定 Closure
程式碼區塊,來將新方法新增至類別。Closure
參數會詮釋為方法參數。可以使用 {→ …}
語法新增不帶參數的方法。
屬性
ExpandoMetaClass
支援兩種機制來新增或覆寫屬性。
首先,它支援透過將值指定給 metaClass
的屬性來宣告可變屬性
class Book {
String title
}
Book.metaClass.author = "Stephen King"
def b = new Book()
assert "Stephen King" == b.author
另一種方式是使用新增執行個體方法的標準機制來新增 getter 和/或 setter 方法。
class Book {
String title
}
Book.metaClass.getAuthor << {-> "Stephen King" }
def b = new Book()
assert "Stephen King" == b.author
在上述原始碼範例中,屬性是由封閉所決定,而且是唯讀屬性。可以新增等效的 setter 方法,但屬性值需要儲存起來以供稍後使用。這可以用以下範例所示的方式來完成。
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"]
}
不過這不是唯一的技術。例如在 servlet 容器中,一種方式可能是將值儲存在目前執行的要求中,作為要求屬性(就像在 Grails 中某些情況下所做的那樣)。
建構函式
可以使用特殊的 constructor
屬性來新增建構函式。<<
或 =
運算子都可以用來指定 Closure
程式碼區塊。當程式碼在執行階段執行時,Closure
參數會變成建構函式參數。
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'
不過,新增建構函式時請小心,因為很容易發生堆疊溢位問題。 |
靜態方法
靜態方法可以使用與實例方法相同的技術來新增,方法名稱前加上 static
限定詞。
class Book {
String title
}
Book.metaClass.static.create << { String title -> new Book(title:title) }
def b = Book.create("The Stand")
借用方法
使用 ExpandoMetaClass
可以使用 Groovy 的方法指標語法從其他類別借用方法。
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 允許您使用字串作為屬性名稱,這進而允許您在執行時動態建立方法和屬性名稱。若要建立具有動態名稱的方法,只需使用參考屬性名稱為字串的語言功能。
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
相同的概念可以套用在靜態方法和屬性上。
動態方法名稱的一個應用可以在 Grails 網路應用程式架構中找到。「動態編解碼器」的概念是透過使用動態方法名稱來實作的。
HTMLCodec
類別class HTMLCodec {
static encode = { theTarget ->
HtmlUtils.htmlEscape(theTarget.toString())
}
static decode = { theTarget ->
HtmlUtils.htmlUnescape(theTarget.toString())
}
}
上面的範例顯示編解碼器實作。Grails 附帶各種編解碼器實作,每個實作都定義在單一類別中。在執行時,應用程式類別路徑中會有多個編解碼器類別。在應用程式啟動時,架構會將 encodeXXX
和 decodeXXX
方法新增到特定元類別,其中 XXX
是編解碼器類別名稱的第一部分 (例如 encodeHTML
)。此機制在以下以一些 Groovy 偽程式碼展示
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()
執行時偵測
在執行時,通常會想要知道在執行方法時存在哪些其他方法或屬性。ExpandoMetaClass
提供以下方法 (截至撰寫本文為止)
-
getMetaMethod
-
hasMetaMethod
-
getMetaProperty
-
hasMetaProperty
為什麼不能只使用反射?因為 Groovy 不同,它有「真實」方法和僅在執行時可用的方法。這些有時 (但並非總是) 表示為 MetaMethods。MetaMethods 會告訴您在執行時有哪些方法可用,因此您的程式碼可以進行調整。
這在覆寫 invokeMethod
、getProperty
和/或 setProperty
時特別有用。
GroovyObject 方法
ExpandoMetaClass
的另一個功能是允許覆寫 invokeMethod
、getProperty
和 setProperty
方法,所有這些方法都可以在 groovy.lang.GroovyObject
類中找到。
以下範例顯示如何覆寫 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()
Closure
程式碼的第一個步驟是查詢給定名稱和引數的 MetaMethod
。如果可以找到該方法,則一切正常,並且委派給它。如果沒有,則傳回一個虛擬值。
MetaMethod 是已知存在於 MetaClass 上的方法,無論是在執行時期還是編譯時期新增的。
|
相同的邏輯可用于覆寫 setProperty
或 getProperty
。
class Person {
String name = "Fred"
}
Person.metaClass.getProperty = { String name ->
def metaProperty = Person.metaClass.getMetaProperty(name)
def result
if(metaProperty) result = metaProperty.getProperty(delegate)
else {
result = "Flintstone"
}
result
}
def p = new Person()
assert "Fred" == p.name
assert "Flintstone" == p.other
這裡需要注意的重要事項是,查詢 MetaProperty
執行個體,而不是 MetaMethod
。如果存在,則呼叫 MetaProperty
的 getProperty
方法,傳遞委派。
覆寫靜態 invokeMethod
ExpandoMetaClass
甚至允許使用特殊的 invokeMethod
語法覆寫靜態方法。
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()
用於覆寫靜態方法的邏輯與我們之前看到的用於覆寫執行個體方法的邏輯相同。唯一的區別是存取 metaClass.static
屬性,以及呼叫 getStaticMethodName
以擷取靜態 MetaMethod
執行個體。
擴充介面
可以使用 ExpandoMetaClass
在介面上新增方法。但是,要執行此操作,必須在應用程式啟動之前使用 ExpandoMetaClass.enableGlobally()
方法在全域啟用它。
List.metaClass.sizeDoubled = {-> delegate.size() * 2 }
def list = []
list << 1
list << 2
assert 4 == list.sizeDoubled()
1.10. 擴充模組
1.10.1. 擴充現有類別
延伸模組可讓您新增方法至現有類別,包括預先編譯的類別,例如 JDK 的類別。這些新方法與透過元類別或使用類別定義的方法不同,它們可用於全球。例如,當您撰寫
def file = new File(...)
def contents = file.getText('utf-8')
getText
方法不存在於 File
類別中。然而,Groovy 知道它,因為它定義在特殊類別 ResourceGroovyMethods
中
public static String getText(File file, String charset) throws IOException {
return IOGroovyMethods.getText(newReader(file, charset));
}
您可能會注意到延伸方法使用輔助類別中的靜態方法定義(其中定義了各種延伸方法)。getText
方法的第一個引數對應於接收器,而其他引數對應於延伸方法的引數。因此,在此我們在 File
類別中定義一個稱為 *getText* 的方法(因為第一個引數為 File
類型),它將單一引數作為參數(編碼 String
)。
建立延伸模組的程序很簡單
-
撰寫如上方的延伸類別
-
撰寫模組描述檔
然後您必須讓 Groovy 看見延伸模組,這就像讓延伸模組類別和描述檔在類別路徑中可用一樣簡單。這表示您有以下選擇
-
直接在類別路徑中提供類別和模組描述檔
-
或將您的延伸模組打包成 jar 以利重複使用
延伸模組可以新增兩種方法至類別
-
執行個體方法(在類別執行個體上呼叫)
-
靜態方法(在類別本身上呼叫)
1.10.2. 執行個體方法
若要將執行個體方法新增至現有類別,您需要建立延伸類別。例如,假設您想在 Integer
上新增一個 maxRetries
方法,它接受一個閉包並執行它最多 *n* 次,直到沒有引發例外。為此,您只需要撰寫以下內容
class MaxRetriesExtension { (1)
static void maxRetries(Integer self, Closure code) { (2)
assert self >= 0
int retries = self
Throwable e = null
while (retries > 0) {
try {
code.call()
break
} catch (Throwable err) {
e = err
retries--
}
}
if (retries == 0 && e) {
throw e
}
}
}
1 | 延伸類別 |
2 | 靜態方法的第一個參數對應到訊息的接收者,也就是擴充的實例 |
然後,在 宣告您的擴充類別 之後,您可以這樣呼叫它
int i=0
5.maxRetries {
i++
}
assert i == 1
i=0
try {
5.maxRetries {
i++
throw new RuntimeException("oops")
}
} catch (RuntimeException e) {
assert i == 5
}
1.10.3. 靜態方法
也可以在類別中加入靜態方法。在這種情況下,靜態方法必須定義在自己的檔案中。靜態和實例擴充方法不能存在於同一個類別中。
class StaticStringExtension { (1)
static String greeting(String self) { (2)
'Hello, world!'
}
}
1 | 靜態擴充類別 |
2 | 靜態方法的第一個參數對應到要擴充的類別,而且不用 |
在這種情況下,您可以直接在 String
類別上呼叫它
assert String.greeting() == 'Hello, world!'
1.10.4. 模組描述
為了讓 Groovy 能夠載入您的擴充方法,您必須宣告您的擴充輔助類別。您必須在 META-INF/groovy
目錄中建立一個名為 org.codehaus.groovy.runtime.ExtensionModule
的檔案
moduleName=Test module for specifications moduleVersion=1.0-test extensionClasses=support.MaxRetriesExtension staticExtensionClasses=support.StaticStringExtension
模組描述需要 4 個金鑰
-
moduleName : 模組名稱
-
moduleVersion: 模組版本。請注意,版本號碼只用於檢查您不會在兩個不同的版本中載入同一個模組。
-
extensionClasses: 實例方法的擴充輔助類別清單。您可以提供多個類別,只要它們以逗號分隔即可。
-
staticExtensionClasses: 靜態方法的擴充輔助類別清單。您可以提供多個類別,只要它們以逗號分隔即可。
請注意,模組不需要同時定義靜態輔助程式和實例輔助程式,而且您可以在單一模組中加入多個類別。您也可以在單一模組中擴充不同的類別,這沒有問題。甚至可以在單一擴充類別中使用不同的類別,但建議將擴充方法依功能集分組到類別中。
1.10.5. 擴充模組與類別路徑
值得注意的是,您無法使用與使用它的程式碼同時編譯的擴充。這表示要使用擴充,它必須在使用它的程式碼編譯之前以編譯類別的形式出現在類別路徑中。通常,這表示您無法將測試類別與擴充類別本身放在同一個原始程式碼單元中。由於一般來說,測試原始程式碼與一般原始程式碼分開,而且在建置的另一個步驟中執行,因此這不是問題。
1.10.6. 與類型檢查的相容性
與類別不同,擴充模組與類型檢查相容:如果在類別路徑中找到擴充模組,類型檢查器就會知道擴充方法,並且在您呼叫它們時不會抱怨。它也相容於靜態編譯。
2. 編譯時間元程式設計
Groovy 中的編譯時間元程式設計允許在編譯時產生程式碼。這些轉換會改變程式的抽象語法樹 (AST),這就是為什麼我們在 Groovy 中稱之為 AST 轉換。AST 轉換讓您可以連結到編譯程序,修改 AST,並繼續編譯程序以產生常規的位元組碼。與執行時間元程式設計相比,這有在類別檔案本身(也就是說,在位元組碼中)使變更可見的優點。在位元組碼中使變更可見很重要,例如,如果您希望轉換成為類別合約的一部分(實作介面、擴充抽象類別,…),或者如果您需要您的類別可以從 Java(或其他 JVM 語言)呼叫。例如,AST 轉換可以將方法新增到類別中。如果您使用執行時間元程式設計執行此動作,新方法將只能從 Groovy 中可見。如果您使用編譯時間元程式設計執行相同動作,該方法將也可以從 Java 中可見。最後但並非最不重要的一點是,編譯時間元程式設計的效能可能會更好(因為不需要初始化階段)。
在本節中,我們將從說明與 Groovy 發行版綑綁在一起的各種編譯時間轉換開始。在後續的章節中,我們將說明您如何實作您自己的 AST 轉換,以及此技術的缺點。
2.1. 可用的 AST 轉換
Groovy 附帶各種 AST 轉換,涵蓋不同的需求:減少樣板(程式碼產生)、實作設計模式(委派,…)、記錄、宣告式並行處理、複製、更安全的指令碼編寫、調整編譯、實作 Swing 模式、測試,最後是管理相依性。如果這些 AST 轉換沒有涵蓋您的需求,您仍然可以實作您自己的 AST 轉換,如開發您自己的 AST 轉換章節中所示。
AST 轉換可以分為兩類
-
全域 AST 轉換會在編譯類別路徑中找到時,透明地、全域地套用
-
透過在原始碼中加上標記,可以套用區域性 AST 轉換。與全域性 AST 轉換不同,區域性 AST 轉換可以支援參數。
Groovy 沒有附帶任何全域性 AST 轉換,但您可以在此處找到可供您在程式碼中使用的區域性 AST 轉換清單
2.1.1. 程式碼產生轉換
此類別的轉換包含有助於移除樣板程式碼的 AST 轉換。這通常是您必須撰寫但不會載有任何有用資訊的程式碼。透過自動產生此樣板程式碼,您必須撰寫的程式碼會變得簡潔明瞭,而且減少因樣板程式碼不正確而造成錯誤的機會。
@groovy.transform.ToString
@ToString
AST 轉換會產生人類可讀的類別 toString
表示法。例如,如下註解 Person
類別會自動為您產生 toString
方法
import groovy.transform.ToString
@ToString
class Person {
String firstName
String lastName
}
有了這個定義,下列斷言就會通過,表示已產生一個 toString
方法,從類別中擷取欄位值並將其列印出來
def p = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p.toString() == 'Person(Jack, Nicholson)'
@ToString
註解接受下列表格中摘要的幾個參數
屬性 | 預設值 | 說明 | 範例 |
---|---|---|---|
excludes |
空清單 |
要從 toString 中排除的屬性清單 |
|
includes |
未定義的標記清單(表示所有欄位) |
要包含在 toString 中的欄位清單 |
|
includeSuper |
False |
是否應將超類別包含在 toString 中 |
|
includeNames |
false |
是否在產生的 toString 中包含屬性名稱。 |
|
includeFields |
False |
除了屬性之外,是否應將欄位包含在 toString 中 |
|
includeSuperProperties |
False |
是否應將超屬性包含在 toString 中 |
|
includeSuperFields |
False |
是否應將可見的超欄位包含在 toString 中 |
|
ignoreNulls |
False |
是否應顯示具有 null 值的屬性/欄位 |
|
includePackage |
True |
在 toString 中使用完全限定類別名稱,而非簡單名稱 |
|
allProperties |
True |
在 toString 中包含所有 JavaBean 屬性 |
|
cache |
False |
快取 toString 字串。僅當類別為不可變時,才應設定為 true。 |
|
allNames |
False |
是否應在產生的 toString 中包含具有內部名稱的欄位和/或屬性 |
|
@groovy.transform.EqualsAndHashCode
@EqualsAndHashCode
AST 轉換旨在為您產生 equals
和 hashCode
方法。產生的雜湊碼遵循 Josh Bloch 在 Effective Java 中所述的最佳實務
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()
有幾個選項可調整 @EqualsAndHashCode
的行為
屬性 | 預設值 | 說明 | 範例 |
---|---|---|---|
excludes |
空清單 |
從 equals/hashCode 中排除的屬性清單 |
|
includes |
未定義的標記清單(表示所有欄位) |
要包含在 equals/hashCode 中的欄位清單 |
|
cache |
False |
快取 hashCode 計算。僅當類別為不可變時,才應設定為 true。 |
|
callSuper |
False |
是否在 equals 和 hashCode 計算中包含 super |
|
includeFields |
False |
除了屬性之外,是否應在 equals/hashCode 中包含欄位 |
|
useCanEqual |
True |
equals 是否應呼叫 canEqual 輔助方法。 |
|
allProperties |
False |
是否應在 equals 和 hashCode 計算中包含 JavaBean 屬性 |
|
allNames |
False |
是否應在 equals 和 hashCode 計算中包含具有內部名稱的欄位和/或屬性 |
|
@groovy.transform.TupleConstructor
@TupleConstructor
註解旨在透過為您產生建構函式,來消除樣板程式碼。建立一個元組建構函式,每個屬性(以及每個欄位)都有參數。每個參數都有預設值(如果存在,則使用屬性的初始值,否則根據屬性類型使用 Java 的預設值)。
實作詳細資料
通常您不需要了解產生的建構函式的實作詳細資料;您只需以正常方式使用它們即可。但是,如果您想新增多個建構函式、了解 Java 整合選項或滿足某些依賴注入架構的需求,那麼了解一些詳細資料會很有用。
如前所述,產生的建構函式已套用預設值。在後續的編譯階段,接著套用 Groovy 編譯器的標準預設值處理行為。最終結果是將多個建構函式置於類別的位元組碼中。這提供了易於理解的語意,而且對於 Java 整合目的也很有用。例如,以下程式碼將產生 3 個建構函式
import groovy.transform.TupleConstructor
@TupleConstructor
class Person {
String firstName
String lastName
}
// traditional map-style constructor
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
// generated tuple constructor
def p2 = new Person('Jack', 'Nicholson')
// generated tuple constructor with default value for second property
def p3 = new Person('Jack')
第一個建構函式是無參數建構函式,只要您沒有 final 屬性,就能允許傳統的 map 式建構。Groovy 會呼叫無參數建構函式,然後在底層呼叫相關的設定函式。值得注意的是,如果第一個屬性(或欄位)的類型為 LinkedHashMap,或者只有一個 Map、AbstractMap 或 HashMap 屬性(或欄位),則 map 式命名參數將不可用。
其他建構函式是透過按定義順序取得屬性來產生的。Groovy 會產生與屬性數量(或欄位,視選項而定)一樣多的建構函式。
將 defaults
屬性(請參閱可用的組態選項表格)設定為 false
,會停用正常的預設值行為,表示
-
只會產生一個建構函式
-
嘗試使用初始值會產生錯誤
-
無法使用地圖式命名參數
此屬性通常只會在其他 Java 架構預期只有一個建構函式的狀況下使用,例如注入架構或 JUnit 參數化執行器。
不變性支援
如果 @PropertyOptions
註解也出現在具有 @TupleConstructor
註解的類別中,則產生的建構函式可能包含自訂屬性處理邏輯。例如,@PropertyOptions
註解上的 propertyHandler
屬性可以設定為 ImmutablePropertyHandler
,這會新增不變類別所需的邏輯(防禦性複製、複製等)。當您使用 @Immutable
元註解時,這通常會在幕後自動發生。某些註解屬性可能不受所有屬性處理器支援。
自訂選項
@TupleConstructor
AST 轉換接受多個註解屬性
屬性 | 預設值 | 說明 | 範例 |
---|---|---|---|
excludes |
空清單 |
從元組建構函式產生中排除的屬性清單 |
|
includes |
未定義清單(表示所有欄位) |
要包含在元組建構函式產生中的欄位清單 |
|
includeProperties |
True |
是否應將屬性包含在元組建構函式產生中 |
|
includeFields |
False |
是否應將欄位包含在元組建構函式產生中,除了屬性之外 |
|
includeSuperProperties |
True |
是否應將超級類別的屬性包含在元組建構函式產生中 |
|
includeSuperFields |
False |
是否應將超級類別的欄位包含在元組建構函式產生中 |
|
callSuper |
False |
是否應在呼叫父類別建構函式時呼叫超級屬性,而不是將其設定為屬性 |
|
force |
False |
預設情況下,如果已定義建構函式,轉換不會執行任何動作。將此屬性設定為 true,建構函式將會產生,而確保沒有定義重複的建構函式是您的責任。 |
|
defaults |
True |
表示已啟用建構函數參數的預設值處理。設定為 false 以取得只有一個建構函數,但已停用初始值支援和命名參數。 |
|
useSetters |
False |
預設情況下,轉換會直接從對應的建構函數參數設定每個屬性的後備欄位。將此屬性設定為 true,建構函數會改為呼叫 setter(如果存在)。通常視為不良風格,在建構函數內呼叫可覆寫的 setter。避免這種不良風格是您的責任。 |
|
allNames |
False |
是否應在建構函數中包含具有內部名稱的欄位和/或屬性 |
|
allProperties |
False |
是否應在建構函數中包含 JavaBean 屬性 |
|
pre |
empty |
包含要插入在產生的建構函數開頭的陳述式的封閉區塊 |
|
post |
empty |
包含要插入在產生的建構函數結尾的陳述式的封閉區塊 |
|
將 defaults
註解屬性設定為 false
,並將 force
註解屬性設定為 true
,允許透過使用不同情況的不同自訂選項來建立多個元組建構函數(前提是每個情況具有不同的類型簽章),如下列範例所示
class Named {
String name
}
@ToString(includeSuperProperties=true, ignoreNulls=true, includeNames=true, includeFields=true)
@TupleConstructor(force=true, defaults=false)
@TupleConstructor(force=true, defaults=false, includeFields=true)
@TupleConstructor(force=true, defaults=false, includeSuperProperties=true)
class Book extends Named {
Integer published
private Boolean fiction
Book() {}
}
assert new Book("Regina", 2015).toString() == 'Book(published:2015, name:Regina)'
assert new Book(2015, false).toString() == 'Book(published:2015, fiction:false)'
assert new Book(2015).toString() == 'Book(published:2015)'
assert new Book().toString() == 'Book()'
assert Book.constructors.size() == 4
類似地,以下是使用不同 includes
選項的另一個範例
@ToString(includeSuperProperties=true, ignoreNulls=true, includeNames=true, includeFields=true)
@TupleConstructor(force=true, defaults=false, includes='name,year')
@TupleConstructor(force=true, defaults=false, includes='year,fiction')
@TupleConstructor(force=true, defaults=false, includes='name,fiction')
class Book {
String name
Integer year
Boolean fiction
}
assert new Book("Regina", 2015).toString() == 'Book(name:Regina, year:2015)'
assert new Book(2015, false).toString() == 'Book(year:2015, fiction:false)'
assert new Book("Regina", false).toString() == 'Book(name:Regina, fiction:false)'
assert Book.constructors.size() == 3
@groovy.transform.MapConstructor
@MapConstructor
註解旨在透過為您產生一個映射建構函數來消除樣板程式碼。會建立一個映射建構函數,以便根據提供的映射中具有屬性名稱的鍵的值來設定類別中的每個屬性。用法如本範例所示
import groovy.transform.*
@ToString
@MapConstructor
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1.toString() == 'Person(Jack, Nicholson)'
產生的建構函數大致如下
public Person(Map args) {
if (args.containsKey('firstName')) {
this.firstName = args.get('firstName')
}
if (args.containsKey('lastName')) {
this.lastName = args.get('lastName')
}
}
@groovy.transform.Canonical
@Canonical
元註解結合 @ToString、@EqualsAndHashCode 和 @TupleConstructor 註解
import groovy.transform.Canonical
@Canonical
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1.toString() == 'Person(Jack, Nicholson)' // Effect of @ToString
def p2 = new Person('Jack','Nicholson') // Effect of @TupleConstructor
assert p2.toString() == 'Person(Jack, Nicholson)'
assert p1==p2 // Effect of @EqualsAndHashCode
assert p1.hashCode()==p2.hashCode() // Effect of @EqualsAndHashCode
可以使用 @Immutable 元註解來產生類似的不可變類別。@Canonical
元註解支援其彙總的註解中找到的組態選項。請參閱這些註解以取得更多詳細資料。
import groovy.transform.Canonical
@Canonical(excludes=['lastName'])
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1.toString() == 'Person(Jack)' // Effect of @ToString(excludes=['lastName'])
def p2 = new Person('Jack') // Effect of @TupleConstructor(excludes=['lastName'])
assert p2.toString() == 'Person(Jack)'
assert p1==p2 // Effect of @EqualsAndHashCode(excludes=['lastName'])
assert p1.hashCode()==p2.hashCode() // Effect of @EqualsAndHashCode(excludes=['lastName'])
@Canonical
元註解可以與明確使用其一個或多個組成註解結合使用,如下所示
import groovy.transform.Canonical
@Canonical(excludes=['lastName'])
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1.toString() == 'Person(Jack)' // Effect of @ToString(excludes=['lastName'])
def p2 = new Person('Jack') // Effect of @TupleConstructor(excludes=['lastName'])
assert p2.toString() == 'Person(Jack)'
assert p1==p2 // Effect of @EqualsAndHashCode(excludes=['lastName'])
assert p1.hashCode()==p2.hashCode() // Effect of @EqualsAndHashCode(excludes=['lastName'])
@Canonical
中的任何適用的註解屬性都會傳遞到明確的註解,但明確註解中已存在的屬性優先。
@groovy.transform.InheritConstructors
@InheritConstructor
AST 轉換旨在為您產生與超級建構函式相符的建構函式。這在覆寫例外類別時特別有用
import groovy.transform.InheritConstructors
@InheritConstructors
class CustomException extends Exception {}
// all those are generated constructors
new CustomException()
new CustomException("A custom message")
new CustomException("A custom message", new RuntimeException())
new CustomException(new RuntimeException())
// Java 7 only
// new CustomException("A custom message", new RuntimeException(), false, true)
@InheritConstructor
AST 轉換支援下列設定選項
屬性 | 預設值 | 說明 | 範例 |
---|---|---|---|
constructorAnnotations |
False |
是否在複製期間從建構函式傳遞註解 |
|
parameterAnnotations |
False |
在複製建構函式時是否從建構函式參數傳遞註解 |
|
@groovy.lang.Category
@Category
AST 轉換簡化了 Groovy 類別的建立。過去,Groovy 類別是以這種方式撰寫的
class TripleCategory {
public static Integer triple(Integer self) {
3*self
}
}
use (TripleCategory) {
assert 9 == 3.triple()
}
@Category
轉換讓您可以使用實例樣式類別撰寫相同內容,而不是靜態類別樣式。這消除了每個方法的第一個引數為接收者的需求。類別可以這樣撰寫
@Category(Integer)
class TripleCategory {
public Integer triple() { 3*this }
}
use (TripleCategory) {
assert 9 == 3.triple()
}
請注意,混合的類別可以使用 this
參照。值得注意的是,在類別類別中使用實例欄位本質上是不安全的:類別不是有狀態的(例如特質)。
@groovy.transform.IndexedProperty
@IndexedProperty
註解旨在為清單/陣列類型的屬性產生索引化 getter/setter。如果您想從 Java 使用 Groovy 類別,這特別有用。雖然 Groovy 支援 GPath 存取屬性,但 Java 無法使用。@IndexedProperty
註解會產生下列形式的索引化屬性
class SomeBean {
@IndexedProperty String[] someArray = new String[2]
@IndexedProperty List someList = []
}
def bean = new SomeBean()
bean.setSomeArray(0, 'value')
bean.setSomeList(0, 123)
assert bean.someArray[0] == 'value'
assert bean.someList == [123]
@groovy.lang.Lazy
@Lazy
AST 轉換實作欄位的延遲初始化。例如,下列程式碼
class SomeBean {
@Lazy LinkedList myField
}
會產生下列程式碼
List $myField
List getMyField() {
if ($myField!=null) { return $myField }
else {
$myField = new LinkedList()
return $myField
}
}
用於初始化欄位的預設值是宣告類型的預設建構函式。可以在屬性指定右側使用閉包定義預設值,如下例所示
class SomeBean { @Lazy LinkedList myField = { ['a','b','c']}() }
在這種情況下,產生的程式碼如下所示
List $myField List getMyField() { if ($myField!=null) { return $myField } else { $myField = { ['a','b','c']}() return $myField } }
如果欄位宣告為 volatile,則會使用double-checked locking模式同步初始化。
使用 soft=true
參數,輔助欄位將使用 SoftReference
,提供實作快取的簡單方式。在這種情況下,如果垃圾回收器決定收集參考,則下次存取欄位時將發生初始化。
@groovy.lang.Newify
@Newify
AST 轉換用於提供建構物件的替代語法
-
使用
Python
風格
@Newify([Tree,Leaf]) class TreeBuilder { Tree tree = Tree(Leaf('A'),Leaf('B'),Tree(Leaf('C'))) }
-
或使用
Ruby
風格
@Newify([Tree,Leaf]) class TreeBuilder { Tree tree = Tree.new(Leaf.new('A'),Leaf.new('B'),Tree.new(Leaf.new('C'))) }
Ruby
版本可以透過將 auto
旗標設定為 false
來停用。
@groovy.transform.Sortable
@Sortable
AST 轉換用於協助撰寫 Comparable
類別,並通常透過多個屬性輕鬆排序。它很容易使用,如下例所示,我們在其中註解 Person
類別
import groovy.transform.Sortable
@Sortable class Person {
String first
String last
Integer born
}
產生的類別具有下列屬性
-
它實作
Comparable
介面 -
它包含一個
compareTo
方法,其實作基於first
、last
和born
屬性的自然排序 -
它有三個傳回比較器的函式:
comparatorByFirst
、comparatorByLast
和comparatorByBorn
。
產生的 compareTo
方法會如下所示
public int compareTo(java.lang.Object obj) {
if (this.is(obj)) {
return 0
}
if (!(obj instanceof Person)) {
return -1
}
java.lang.Integer value = this.first <=> obj.first
if (value != 0) {
return value
}
value = this.last <=> obj.last
if (value != 0) {
return value
}
value = this.born <=> obj.born
if (value != 0) {
return value
}
return 0
}
作為產生的比較器的範例,comparatorByFirst
比較器將有一個 compare
方法,如下所示
public int compare(java.lang.Object arg0, java.lang.Object arg1) {
if (arg0 == arg1) {
return 0
}
if (arg0 != null && arg1 == null) {
return -1
}
if (arg0 == null && arg1 != null) {
return 1
}
return arg0.first <=> arg1.first
}
Person
類別可以在任何預期 Comparable
的地方使用,產生的比較器可以在任何預期 Comparator
的地方使用,如下例所示
def people = [
new Person(first: 'Johnny', last: 'Depp', born: 1963),
new Person(first: 'Keira', last: 'Knightley', born: 1985),
new Person(first: 'Geoffrey', last: 'Rush', born: 1951),
new Person(first: 'Orlando', last: 'Bloom', born: 1977)
]
assert people[0] > people[2]
assert people.sort()*.last == ['Rush', 'Depp', 'Knightley', 'Bloom']
assert people.sort(false, Person.comparatorByFirst())*.first == ['Geoffrey', 'Johnny', 'Keira', 'Orlando']
assert people.sort(false, Person.comparatorByLast())*.last == ['Bloom', 'Depp', 'Knightley', 'Rush']
assert people.sort(false, Person.comparatorByBorn())*.last == ['Rush', 'Depp', 'Bloom', 'Knightley']
通常,所有屬性都會在產生的 compareTo
方法中使用,優先順序為定義順序。您可以透過在 includes
或 excludes
註解屬性中提供屬性名稱清單,將某些屬性包含或排除在產生的 compareTo
方法中。如果使用 includes
,提供的屬性名稱順序將決定比較時屬性的優先順序。為了說明,請考慮下列 Person
類別定義
@Sortable(includes='first,born') class Person {
String last
int born
String first
}
它將有兩個比較器方法 comparatorByFirst
和 comparatorByBorn
,而產生的 compareTo
方法將如下所示
public int compareTo(java.lang.Object obj) {
if (this.is(obj)) {
return 0
}
if (!(obj instanceof Person)) {
return -1
}
java.lang.Integer value = this.first <=> obj.first
if (value != 0) {
return value
}
value = this.born <=> obj.born
if (value != 0) {
return value
}
return 0
}
這個 Person
類別可以使用如下
def people = [
new Person(first: 'Ben', last: 'Affleck', born: 1972),
new Person(first: 'Ben', last: 'Stiller', born: 1965)
]
assert people.sort()*.last == ['Stiller', 'Affleck']
@Sortable
AST 轉換的行為可以使用下列額外的參數進一步更改
屬性 | 預設值 | 說明 | 範例 |
---|---|---|---|
allProperties |
True |
是否使用 JavaBean 屬性(在原生屬性之後排序) |
|
allNames |
False |
是否使用具有「內部」名稱的屬性 |
|
includeSuperProperties |
False |
是否也使用超類屬性(首先排序) |
|
@groovy.transform.builder.Builder
@Builder
AST 轉換用於協助撰寫可以使用流暢 API 呼叫建立的類別。此轉換支援多種建構策略來涵蓋各種情況,並有許多組態選項可以自訂建構程序。如果您是 AST 黑客,您也可以定義您自己的策略類別。下表列出與 Groovy 綑綁在一起的可用策略,以及每個策略支援的組態選項。
策略 |
說明 |
builderClassName |
builderMethodName |
buildMethodName |
prefix |
includes/excludes |
includeSuperProperties |
allNames |
|
鏈結的 setter |
n/a |
n/a |
n/a |
是,預設為「set」 |
是 |
n/a |
是,預設為 |
|
明確的建構器類別,未觸及正在建構的類別 |
n/a |
n/a |
是,預設為「build」 |
是,預設為「」 |
是 |
是,預設為 |
是,預設為 |
|
建立一個巢狀的輔助類別 |
是,預設為 <TypeName>Builder |
是,預設為「builder」 |
是,預設為「build」 |
是,預設為「」 |
是 |
是,預設為 |
是,預設為 |
|
建立一個巢狀的輔助類別,提供類型安全的流暢建立 |
是,預設為 <TypeName>Initializer |
是,預設為「createInitializer」 |
是,預設為「create」,但通常只在內部使用 |
是,預設為「」 |
是 |
是,預設為 |
是,預設為 |
若要使用 SimpleStrategy
,請使用 @Builder
註解為您的 Groovy 類別加上註解,並指定策略,如本範例所示
import groovy.transform.builder.*
@Builder(builderStrategy=SimpleStrategy)
class Person {
String first
String last
Integer born
}
然後,只要像這裡所示以鏈結的方式呼叫 setter
def p1 = new Person().setFirst('Johnny').setLast('Depp').setBorn(1963)
assert "$p1.first $p1.last" == 'Johnny Depp'
對於每個屬性,將建立一個產生的 setter,如下所示
public Person setFirst(java.lang.String first) {
this.first = first
return this
}
您可以指定一個前綴,如本範例所示
import groovy.transform.builder.*
@Builder(builderStrategy=SimpleStrategy, prefix="")
class Person {
String first
String last
Integer born
}
呼叫鏈結的 setter 將如下所示
def p = new Person().first('Johnny').last('Depp').born(1963)
assert "$p.first $p.last" == 'Johnny Depp'
你可以結合使用 SimpleStrategy
和 @TupleConstructor
。如果你的 @Builder
註解沒有明確的 includes
或 excludes
註解屬性,但你的 @TupleConstructor
註解有,則 @TupleConstructor
中的屬性會重複用於 @Builder
。任何結合 @TupleConstructor
的註解別名(例如 @Canonical
)也適用相同的規則。
如果你有想要在建構過程中呼叫的 setter,可以使用 useSetters
註解屬性。有關詳細資訊,請參閱 JavaDoc。
此策略不支援 builderClassName
、buildMethodName
、builderMethodName
、forClass
和 includeSuperProperties
等註解屬性。
Groovy 已經內建建構機制。如果內建機制符合你的需求,請不要急著使用 @Builder 。以下是一些範例
|
def p2 = new Person(first: 'Keira', last: 'Knightley', born: 1985)
def p3 = new Person().with {
first = 'Geoffrey'
last = 'Rush'
born = 1951
}
若要使用 ExternalStrategy
,請使用 @Builder
註解建立並註解 Groovy 建構器類別,指定建構器所屬的類別(使用 forClass
),並指出使用 ExternalStrategy
。假設你有以下類別,而且想要為其建立建構器
class Person {
String first
String last
int born
}
你可以明確建立並使用建構器類別,如下所示
import groovy.transform.builder.*
@Builder(builderStrategy=ExternalStrategy, forClass=Person)
class PersonBuilder { }
def p = new PersonBuilder().first('Johnny').last('Depp').born(1963).build()
assert "$p.first $p.last" == 'Johnny Depp'
請注意,你提供的建構器類別(通常為空)會填入適當的 setter 和 build 方法。產生的 build 方法看起來會像這樣
public Person build() {
Person _thePerson = new Person()
_thePerson.first = first
_thePerson.last = last
_thePerson.born = born
return _thePerson
}
你為其建立建構器的類別可以是遵循一般 JavaBean 約定的任何 Java 或 Groovy 類別,例如沒有參數的建構函式和屬性的 setter。以下是一個使用 Java 類別的範例
import groovy.transform.builder.*
@Builder(builderStrategy=ExternalStrategy, forClass=javax.swing.DefaultButtonModel)
class ButtonModelBuilder {}
def model = new ButtonModelBuilder().enabled(true).pressed(true).armed(true).rollover(true).selected(true).build()
assert model.isArmed()
assert model.isPressed()
assert model.isEnabled()
assert model.isSelected()
assert model.isRollover()
可以使用 prefix
、includes
、excludes
和 buildMethodName
註解屬性自訂產生的建構器。以下是一個說明各種自訂範例
import groovy.transform.builder.*
import groovy.transform.Canonical
@Canonical
class Person {
String first
String last
int born
}
@Builder(builderStrategy=ExternalStrategy, forClass=Person, includes=['first', 'last'], buildMethodName='create', prefix='with')
class PersonBuilder { }
def p = new PersonBuilder().withFirst('Johnny').withLast('Depp').create()
assert "$p.first $p.last" == 'Johnny Depp'
@Builder
的 builderMethodName
和 builderClassName
註解屬性不適用於此策略。
你可以結合使用 ExternalStrategy
和 @TupleConstructor
。如果你的 @Builder
註解沒有明確的 includes
或 excludes
註解屬性,但你為其建立建構器的類別的 @TupleConstructor
註解有,則 @TupleConstructor
中的屬性會重複用於 @Builder
。任何結合 @TupleConstructor
的註解別名(例如 @Canonical
)也適用相同的規則。
若要使用 DefaultStrategy
,請使用 @Builder
註解註解你的 Groovy 類別,如下面的範例所示
import groovy.transform.builder.Builder
@Builder
class Person {
String firstName
String lastName
int age
}
def person = Person.builder().firstName("Robert").lastName("Lewandowski").age(21).build()
assert person.firstName == "Robert"
assert person.lastName == "Lewandowski"
assert person.age == 21
如果你願意,可以使用 builderClassName
、buildMethodName
、builderMethodName
、prefix
、includes
和 excludes
註解屬性自訂建構程式的各個面向,其中一些屬性已用於此範例中
import groovy.transform.builder.Builder
@Builder(buildMethodName='make', builderMethodName='maker', prefix='with', excludes='age')
class Person {
String firstName
String lastName
int age
}
def p = Person.maker().withFirstName("Robert").withLastName("Lewandowski").make()
assert "$p.firstName $p.lastName" == "Robert Lewandowski"
此策略也支援註解靜態方法和建構函式。在此情況下,靜態方法或建構函式參數會成為用於建構目的的屬性,而靜態方法的回傳類型會成為正在建構的目標類別。如果您在類別中使用多個 @Builder
註解(在類別、方法或建構函式位置),則您必須確保產生的輔助類別和工廠方法具有唯一名稱(即,最多只能使用一個預設名稱值)。以下是強調方法和建構函式使用情況的範例(並說明唯一名稱所需的重新命名)。
import groovy.transform.builder.*
import groovy.transform.*
@ToString
@Builder
class Person {
String first, last
int born
Person(){}
@Builder(builderClassName='MovieBuilder', builderMethodName='byRoleBuilder')
Person(String roleName) {
if (roleName == 'Jack Sparrow') {
this.first = 'Johnny'; this.last = 'Depp'; this.born = 1963
}
}
@Builder(builderClassName='NameBuilder', builderMethodName='nameBuilder', prefix='having', buildMethodName='fullName')
static String join(String first, String last) {
first + ' ' + last
}
@Builder(builderClassName='SplitBuilder', builderMethodName='splitBuilder')
static Person split(String name, int year) {
def parts = name.split(' ')
new Person(first: parts[0], last: parts[1], born: year)
}
}
assert Person.splitBuilder().name("Johnny Depp").year(1963).build().toString() == 'Person(Johnny, Depp, 1963)'
assert Person.byRoleBuilder().roleName("Jack Sparrow").build().toString() == 'Person(Johnny, Depp, 1963)'
assert Person.nameBuilder().havingFirst('Johnny').havingLast('Depp').fullName() == 'Johnny Depp'
assert Person.builder().first("Johnny").last('Depp').born(1963).build().toString() == 'Person(Johnny, Depp, 1963)'
此策略不支援 forClass
註解屬性。
若要使用 InitializerStrategy
,請使用 @Builder
註解註解您的 Groovy 類別,並如本範例中所示指定策略
import groovy.transform.builder.*
import groovy.transform.*
@ToString
@Builder(builderStrategy=InitializerStrategy)
class Person {
String firstName
String lastName
int age
}
您的類別將被鎖定為具有單一公開建構函式,採用「完全設定」的初始化函式。它還將具有用於建立初始化函式的工廠方法。它們的使用方式如下
@CompileStatic
def firstLastAge() {
assert new Person(Person.createInitializer().firstName("John").lastName("Smith").age(21)).toString() == 'Person(John, Smith, 21)'
}
firstLastAge()
任何不涉及設定所有屬性(但順序不重要)的初始化函式使用嘗試都會導致編譯錯誤。如果您不需要這種嚴格性,則不需要使用 @CompileStatic
。
您可以將 InitializerStrategy
與 @Canonical
和 @Immutable
結合使用。如果您的 @Builder
註解沒有明確的 includes
或 excludes
註解屬性,但您的 @Canonical
註解有,則來自 @Canonical
的屬性將重新用於 @Builder
。以下是一個使用 @Builder
搭配 @Immutable
的範例
import groovy.transform.builder.*
import groovy.transform.*
import static groovy.transform.options.Visibility.PRIVATE
@Builder(builderStrategy=InitializerStrategy)
@Immutable
@VisibilityOptions(PRIVATE)
class Person {
String first
String last
int born
}
def publicCons = Person.constructors
assert publicCons.size() == 1
@CompileStatic
def createFirstLastBorn() {
def p = new Person(Person.createInitializer().first('Johnny').last('Depp').born(1963))
assert "$p.first $p.last $p.born" == 'Johnny Depp 1963'
}
createFirstLastBorn()
如果你有想要在建構過程中呼叫的 setter,可以使用 useSetters
註解屬性。有關詳細資訊,請參閱 JavaDoc。
此策略也支援註解靜態方法和建構函式。在此情況下,靜態方法或建構函式參數會成為用於建構目的的屬性,而靜態方法的回傳類型會成為正在建構的目標類別。如果您在類別中使用多個 @Builder
註解(在類別、方法或建構函式位置),則您必須確保產生的輔助類別和工廠方法具有唯一名稱(即,最多只能使用一個預設名稱值)。有關方法和建構函式使用情況的範例,但使用 DefaultStrategy
策略,請參閱該策略的文件。
此策略不支援註解屬性 forClass
。
@groovy.transform.AutoImplement
@AutoImplement
AST 轉換會為從超類別或介面找到的任何抽象方法提供虛擬實作。虛擬實作對於找到的所有抽象方法都是相同的,可以是
-
基本上是空的(對於空方法和具有回傳類型的函式而言,回傳該類型的預設值)
-
會擲回指定例外狀況的陳述式(可選擇訊息)
-
一些使用者提供的程式碼
第一個範例說明預設情況。我們的類別使用 @AutoImplement
註解,具有超類別和單一介面,如下所示
import groovy.transform.AutoImplement
@AutoImplement
class MyNames extends AbstractList<String> implements Closeable { }
提供 Closeable
介面的 void close()
函式,並保持為空。也提供超類別中三個抽象函式的實作。get
、addAll
和 size
函式的回傳類型分別為 String
、boolean
和 int
,預設值為 null
、false
和 0
。我們可以使用我們的類別(並檢查其中一個函式的預期回傳類型),使用下列程式碼
assert new MyNames().size() == 0
也值得檢查等效的產生程式碼
class MyNames implements Closeable extends AbstractList<String> {
String get(int param0) {
return null
}
boolean addAll(Collection<? extends String> param0) {
return false
}
void close() throws Exception {
}
int size() {
return 0
}
}
第二個範例說明最簡單的例外狀況情況。我們的類別使用 @AutoImplement
註解,具有超類別,註解屬性指出如果呼叫任何我們的「虛擬」函式,應擲回 IOException
。以下是類別定義
@AutoImplement(exception=IOException)
class MyWriter extends Writer { }
我們可以使用類別(並檢查預期的例外狀況是否擲回給其中一個函式),使用下列程式碼
import static groovy.test.GroovyAssert.shouldFail
shouldFail(IOException) {
new MyWriter().flush()
}
也值得檢查等效的產生程式碼,其中提供三個空函式,全部擲回提供的例外狀況
class MyWriter extends Writer {
void flush() throws IOException {
throw new IOException()
}
void write(char[] param0, int param1, int param2) throws IOException {
throw new IOException()
}
void close() throws Exception {
throw new IOException()
}
}
第三個範例說明提供訊息的例外狀況情況。我們的類別使用 @AutoImplement
註解,實作介面,並具有註解屬性,指出應為任何提供的函式擲回訊息為「不受 MyIterator 支援」的 UnsupportedOperationException
。以下是類別定義
@AutoImplement(exception=UnsupportedOperationException, message='Not supported by MyIterator')
class MyIterator implements Iterator<String> { }
我們可以使用類別(並檢查預期的例外狀況是否擲回,且具有其中一個函式的正確訊息),使用下列程式碼
def ex = shouldFail(UnsupportedOperationException) {
new MyIterator().hasNext()
}
assert ex.message == 'Not supported by MyIterator'
也值得檢查等效的產生程式碼,其中提供三個空函式,全部擲回提供的例外狀況
class MyIterator implements Iterator<String> {
boolean hasNext() {
throw new UnsupportedOperationException('Not supported by MyIterator')
}
String next() {
throw new UnsupportedOperationException('Not supported by MyIterator')
}
}
第四個範例說明使用者提供程式碼的情況。我們的類別加上 @AutoImplement
註解,實作介面,有一個明確覆寫的 hasNext
方法,並有一個註解屬性,其中包含任何提供方法的提供程式碼。以下是類別定義
@AutoImplement(code = { throw new UnsupportedOperationException('Should never be called but was called on ' + new Date()) })
class EmptyIterator implements Iterator<String> {
boolean hasNext() { false }
}
我們可以使用類別(並檢查是否擲回預期的例外狀況,且其訊息為預期格式)並使用下列程式碼
def ex = shouldFail(UnsupportedOperationException) {
new EmptyIterator().next()
}
assert ex.message.startsWith('Should never be called but was called on ')
檢查已提供 next
方法的等效產生程式碼也很值得
class EmptyIterator implements java.util.Iterator<String> {
boolean hasNext() {
false
}
String next() {
throw new UnsupportedOperationException('Should never be called but was called on ' + new Date())
}
}
@groovy.transform.NullCheck
@NullCheck
AST 轉換會將 null 檢查防護陳述式新增至建構函式和方法,當提供 null 參數時,這些方法會提早失敗。它可以視為防禦性程式設計的一種形式。註解可以新增至個別方法或建構函式,或新增至類別,後者會套用至所有方法/建構函式。
@NullCheck
String longerOf(String first, String second) {
first.size() >= second.size() ? first : second
}
assert longerOf('cat', 'canary') == 'canary'
def ex = shouldFail(IllegalArgumentException) {
longerOf('cat', null)
}
assert ex.message == 'second cannot be null'
2.1.2. 類別設計註解
此類別的註解旨在透過使用宣告式樣式簡化眾所周知設計模式(委派、單例…)的實作。
@groovy.transform.BaseScript
@BaseScript
用於腳本中,以指出腳本應從自訂腳本基礎類別延伸,而非 groovy.lang.Script
。請參閱 特定領域語言 的文件,以取得進一步詳細資料。
@groovy.lang.Delegate
@Delegate
AST 轉換旨在實作委派設計模式。在下列類別中
class Event {
@Delegate Date when
String title
}
when
屬性加上 @Delegate
註解,表示 Event
類別會將呼叫委派給 Date
方法至 when
屬性。在此情況下,產生的程式碼如下所示
class Event {
Date when
String title
boolean before(Date other) {
when.before(other)
}
// ...
}
然後您可以直接在 Event
類別上呼叫 before
方法,例如
def ev = new Event(title:'Groovy keynote', when: Date.parse('yyyy/MM/dd', '2013/09/10'))
def now = new Date()
assert ev.before(now)
除了註解屬性(或欄位)之外,您也可以註解方法。在此情況下,方法可以視為委派的 getter 或工廠方法。以下是一個範例類別(相當不尋常),它有一個委派池,會以循環的方式存取
class Test {
private int robinCount = 0
private List<List> items = [[0], [1], [2]]
@Delegate
List getRoundRobinList() {
items[robinCount++ % items.size()]
}
void checkItems(List<List> testValue) {
assert items == testValue
}
}
以下是該類別的範例用法
def t = new Test()
t << 'fee'
t << 'fi'
t << 'fo'
t << 'fum'
t.checkItems([[0, 'fee', 'fum'], [1, 'fi'], [2, 'fo']])
以這種循環的方式使用標準清單會違反清單的許多預期屬性,因此不要期待上述類別會在這簡單範例之外執行任何有用的操作。
可以使用下列參數變更 @Delegate
AST 轉換的行為
屬性 | 預設值 | 說明 | 範例 |
---|---|---|---|
介面 |
True |
欄位實作的介面是否也應由類別實作 |
|
已棄用 |
false |
如果為 true,也會委派註解為 @Deprecated 的方法 |
|
methodAnnotations |
False |
是否將委派方法的方法註解傳遞到委派方法。 |
|
parameterAnnotations |
False |
是否將委派方法的方法參數註解傳遞到委派方法。 |
|
excludes |
空陣列 |
要從委派中排除的方法清單。如需更精細的控制,另請參閱 |
|
includes |
未定義的標記陣列(表示所有方法) |
要包含在委派中的方法清單。如需更精細的控制,另請參閱 |
|
excludeTypes |
空陣列 |
包含要從委派中排除的方法簽章的介面清單 |
|
includeTypes |
未定義的標記陣列(表示預設沒有清單) |
包含要包含在委派中的方法簽章的介面清單 |
|
allNames |
False |
是否也將委派模式套用至具有內部名稱的方法 |
|
@groovy.transform.Immutable
@Immutable
元註解結合下列註解
@Immutable
元註解簡化了不可變類別的建立。不可變類別很有用,因為它們通常較容易推理,而且本質上是執行緒安全的。請參閱 Effective Java,最小化可變性,以取得有關如何在 Java 中達成不可變類別的所有詳細資訊。@Immutable
元註解會自動為您執行 Effective Java 中所述的大部分事項。若要使用元註解,您只需像以下範例中一樣註解類別
import groovy.transform.Immutable
@Immutable
class Point {
int x
int y
}
不可變類別的要求之一是,沒有辦法修改類別內的任何狀態資訊。達成此目標的要求之一是,對每個屬性使用不可變類別,或在建構函式和屬性 getter 中對任何可變屬性執行特殊編碼,例如防禦性複製輸入和防禦性複製輸出。在 @ImmutableBase
、@MapConstructor
和 @TupleConstructor
之間,屬性會標示為不可變,或自動處理許多已知案例的特殊編碼。會為您提供各種機制,讓您可以擴充允許的已處理屬性類型。請參閱 @ImmutableOptions
和 @KnownImmutable
以取得詳細資訊。
將 @Immutable
套用至類別的結果與套用 @Canonical 元註解的結果相當類似,但產生的類別會有額外的邏輯來處理不可變性。您會觀察到這一點,例如,嘗試修改屬性會導致擲出 ReadOnlyPropertyException
,因為屬性的後援欄位會自動變為 final。
@Immutable
元註解支援在它彙總的註解中找到的組態選項。請參閱這些註解以取得更多詳細資料。
@groovy.transform.ImmutableBase
使用 @ImmutableBase
產生的不變類別會自動變成 final。此外,會檢查每個屬性的類型,並對類別進行各種檢查,例如,目前不允許公開的執行個體欄位。如果需要,它也會產生一個 copyWith
建構函式。
支援下列註解屬性
屬性 | 預設值 | 說明 | 範例 |
---|---|---|---|
copyWith |
false |
一個布林值,表示是否要產生 |
|
@groovy.transform.PropertyOptions
這個註解讓您能夠指定一個自訂屬性處理常式,供轉換在類別建構期間使用。它會被主要的 Groovy 編譯器忽略,但會被其他轉換參照,例如 @TupleConstructor
、@MapConstructor
和 @ImmutableBase
。它經常在幕後由 @Immutable
元註解使用。
@groovy.transform.VisibilityOptions
這個註解讓您能夠為由另一個轉換產生的建構指定自訂可見性。它會被主要的 Groovy 編譯器忽略,但會被其他轉換參照,例如 @TupleConstructor
、@MapConstructor
和 @NamedVariant
。
@groovy.transform.ImmutableOptions
Groovy 的不變性支援仰賴已預先定義的不變類別清單(例如 java.net.URI
或 java.lang.String
),如果您使用清單中沒有的類型,就會失敗,您可以透過 @ImmutableOptions
註解的下列註解屬性,將已知的類型新增到不變類型清單中
屬性 | 預設值 | 說明 | 範例 |
---|---|---|---|
knownImmutableClasses |
空清單 |
被視為不變的類別清單。 |
|
knownImmutables |
空清單 |
被視為不變的屬性名稱清單。 |
|
如果您將一個類型視為不變,但它不是自動處理的類型之一,那麼您必須正確編寫該類別的程式碼,以確保不變性。
@groovy.transform.KnownImmutable
@KnownImmutable
註解實際上並不會觸發任何 AST 轉換。它只是一個標記註解。你可以使用註解註解你的類別(包括 Java 類別),它們將被識別為不可變類別中成員的可接受類型。這可以讓你不用從 @ImmutableOptions
明確使用 knownImmutables
或 knownImmutableClasses
註解屬性。
@groovy.transform.Memoized
@Memoized
AST 轉換簡化了快取的實作,允許方法呼叫的結果透過使用 @Memoized
註解方法來快取。讓我們想像以下的方法
long longComputation(int seed) {
// slow computation
Thread.sleep(100*seed)
System.nanoTime()
}
這模擬了一個長計算,基於方法的實際參數。沒有 @Memoized
,每個方法呼叫將需要幾秒鐘,而且它會傳回一個隨機結果
def x = longComputation(1)
def y = longComputation(1)
assert x!=y
新增 @Memoized
會透過新增快取來變更方法的語意,基於參數
@Memoized
long longComputation(int seed) {
// slow computation
Thread.sleep(100*seed)
System.nanoTime()
}
def x = longComputation(1) // returns after 100 milliseconds
def y = longComputation(1) // returns immediately
def z = longComputation(2) // returns after 200 milliseconds
assert x==y
assert x!=z
快取的大小可以使用兩個選用參數來設定
-
protectedCacheSize:保證在垃圾回收後不會清除的結果數量
-
maxCacheSize:可以在記憶體中保留的最大結果數量
預設情況下,快取的大小是無限的,而且沒有快取結果會受到垃圾回收的保護。設定 protectedCacheSize>0 會建立一個無限快取,其中一些結果受到保護。設定 maxCacheSize>0 會建立一個有限快取,但沒有任何垃圾回收保護。設定兩者會建立一個有限且受保護的快取。
@groovy.transform.TailRecursive
@TailRecursive
註解可以用來自動將方法結尾的遞迴呼叫轉換為相同程式碼的等效迭代版本。這可以避免因為過多遞迴呼叫而導致堆疊溢位。以下是計算階乘時使用的範例
import groovy.transform.CompileStatic
import groovy.transform.TailRecursive
@CompileStatic
class Factorial {
@TailRecursive
static BigInteger factorial( BigInteger i, BigInteger product = 1) {
if( i == 1) {
return product
}
return factorial(i-1, product*i)
}
}
assert Factorial.factorial(1) == 1
assert Factorial.factorial(3) == 6
assert Factorial.factorial(5) == 120
assert Factorial.factorial(50000).toString().size() == 213237 // Big number and no Stack Overflow
目前,註解只會對自我遞迴方法呼叫起作用,也就是說,再次對同一個方法進行單一遞迴呼叫。如果你有涉及簡單相互遞迴的場景,請考慮使用 Closure 和 trampoline()
。另外請注意,目前只處理非 void 方法(void 呼叫將導致編譯錯誤)。
目前,某些形式的方法重載可以欺騙編譯器,而某些非尾部遞迴呼叫會被錯誤地視為尾部遞迴。 |
@groovy.lang.Singleton
@Singleton
註解可用於在類別上實作單例設計模式。預設會透過類別初始化立即定義單例實體,或延遲定義,後者會使用雙重檢查鎖定來初始化欄位。
@Singleton
class GreetingService {
String greeting(String name) { "Hello, $name!" }
}
assert GreetingService.instance.greeting('Bob') == 'Hello, Bob!'
預設情況下,單例會在類別初始化時立即建立,並透過 instance
屬性取得。可以使用 property
參數變更單例的名稱
@Singleton(property='theOne')
class GreetingService {
String greeting(String name) { "Hello, $name!" }
}
assert GreetingService.theOne.greeting('Bob') == 'Hello, Bob!'
也可以使用 lazy
參數讓初始化延遲
class Collaborator {
public static boolean init = false
}
@Singleton(lazy=true,strict=false)
class GreetingService {
static void init() {}
GreetingService() {
Collaborator.init = true
}
String greeting(String name) { "Hello, $name!" }
}
GreetingService.init() // make sure class is initialized
assert Collaborator.init == false
GreetingService.instance
assert Collaborator.init == true
assert GreetingService.instance.greeting('Bob') == 'Hello, Bob!'
在此範例中,我們也將 strict
參數設定為 false,這讓我們可以定義自己的建構函式。
@groovy.lang.Mixin
已棄用。請考慮改用特質。
2.1.3. 記錄改進
Groovy 提供一系列 AST 轉換,有助於整合最廣泛使用的記錄架構。針對每個常見架構,都有轉換和相關註解。這些轉換提供簡化的宣告式方法來使用記錄架構。在每種情況下,轉換都會
-
將靜態最終
log
欄位新增到對應於記錄器的註解類別 -
將所有對
log.level()
的呼叫包裝到適當的log.isLevelEnabled
保護中,具體取決於底層架構
這些轉換支援兩個參數
-
value
(預設為log
)對應於記錄器欄位的名稱 -
category
(預設為類別名稱)是記錄器類別的名稱
值得注意的是,使用其中一個註解來註解類別並不會阻止你使用一般長手寫法來使用記錄架構。
@groovy.util.logging.Log
第一個可用的記錄 AST 轉換是 @Log
註解,它依賴於 JDK 記錄架構。撰寫
@groovy.util.logging.Log
class Greeter {
void greet() {
log.info 'Called greeter'
println 'Hello, world!'
}
}
等於撰寫
import java.util.logging.Level
import java.util.logging.Logger
class Greeter {
private static final Logger log = Logger.getLogger(Greeter.name)
void greet() {
if (log.isLoggable(Level.INFO)) {
log.info 'Called greeter'
}
println 'Hello, world!'
}
}
@groovy.util.logging.Commons
Groovy 使用 @Commons
註解支援 Apache Commons Logging 架構。撰寫
@groovy.util.logging.Commons
class Greeter {
void greet() {
log.debug 'Called greeter'
println 'Hello, world!'
}
}
等於撰寫
import org.apache.commons.logging.LogFactory
import org.apache.commons.logging.Log
class Greeter {
private static final Log log = LogFactory.getLog(Greeter)
void greet() {
if (log.isDebugEnabled()) {
log.debug 'Called greeter'
}
println 'Hello, world!'
}
}
您仍需要將適當的 commons-logging jar 新增至您的類別路徑。
@groovy.util.logging.Log4j
Groovy 使用 @Log4j
標註支援 Apache Log4j 1.x 架構。撰寫
@groovy.util.logging.Log4j
class Greeter {
void greet() {
log.debug 'Called greeter'
println 'Hello, world!'
}
}
等於撰寫
import org.apache.log4j.Logger
class Greeter {
private static final Logger log = Logger.getLogger(Greeter)
void greet() {
if (log.isDebugEnabled()) {
log.debug 'Called greeter'
}
println 'Hello, world!'
}
}
您仍需要將適當的 log4j jar 新增至您的類別路徑。此標註也可以搭配相容的 reload4j log4j 直接替換使用,只要使用該專案的 jar 即可,不必使用 log4j jar。
@groovy.util.logging.Log4j2
Groovy 使用 @Log4j2
標註支援 Apache Log4j 2.x 架構。撰寫
@groovy.util.logging.Log4j2
class Greeter {
void greet() {
log.debug 'Called greeter'
println 'Hello, world!'
}
}
等於撰寫
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
class Greeter {
private static final Logger log = LogManager.getLogger(Greeter)
void greet() {
if (log.isDebugEnabled()) {
log.debug 'Called greeter'
}
println 'Hello, world!'
}
}
您仍需要將適當的 log4j2 jar 新增至您的類別路徑。
@groovy.util.logging.Slf4j
Groovy 使用 @Slf4j
標註支援 Java 簡單記錄門面 (SLF4J) 架構。撰寫
@groovy.util.logging.Slf4j
class Greeter {
void greet() {
log.debug 'Called greeter'
println 'Hello, world!'
}
}
等於撰寫
import org.slf4j.LoggerFactory
import org.slf4j.Logger
class Greeter {
private static final Logger log = LoggerFactory.getLogger(Greeter)
void greet() {
if (log.isDebugEnabled()) {
log.debug 'Called greeter'
}
println 'Hello, world!'
}
}
您仍需要將適當的 slf4j jar 新增至您的類別路徑。
@groovy.util.logging.PlatformLog
Groovy 使用 @PlatformLog
標註支援 Java 平台記錄 API 和服務 架構。撰寫
@groovy.util.logging.PlatformLog
class Greeter {
void greet() {
log.info 'Called greeter'
println 'Hello, world!'
}
}
等於撰寫
import java.lang.System.Logger
import java.lang.System.LoggerFinder
import static java.lang.System.Logger.Level.INFO
class Greeter {
private static final transient Logger log =
LoggerFinder.loggerFinder.getLogger(Greeter.class.name, Greeter.class.module)
void greet() {
log.log INFO, 'Called greeter'
println 'Hello, world!'
}
}
您需要使用 JDK 9+ 才能使用此功能。
2.1.4. 宣告式並行
Groovy 語言提供一組標註,旨在以宣告式方法簡化常見的並行模式。
@groovy.transform.Synchronized
@Synchronized
AST 轉換運作方式類似於 synchronized
關鍵字,但會鎖定不同的物件以確保更安全的並行。它可以套用於任何方法或靜態方法
import groovy.transform.Synchronized
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
class Counter {
int cpt
@Synchronized
int incrementAndGet() {
cpt++
}
int get() {
cpt
}
}
撰寫此程式碼等同於建立一個鎖定物件,並將整個方法包裝在同步區塊中
class Counter {
int cpt
private final Object $lock = new Object()
int incrementAndGet() {
synchronized($lock) {
cpt++
}
}
int get() {
cpt
}
}
預設情況下,@Synchronized
會建立一個名為 $lock
的欄位 (或靜態方法的 $LOCK
),但您可以透過指定值屬性來指定使用任何欄位,如下列範例所示
import groovy.transform.Synchronized
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
class Counter {
int cpt
private final Object myLock = new Object()
@Synchronized('myLock')
int incrementAndGet() {
cpt++
}
int get() {
cpt
}
}
@groovy.transform.WithReadLock
和 @groovy.transform.WithWriteLock
@WithReadLock
AST 轉換與 @WithWriteLock
轉換搭配使用,以提供讀/寫同步,使用 JDK 提供的 ReentrantReadWriteLock
設施。註解可以新增至方法或靜態方法。它會透明地建立一個 $reentrantLock
最終欄位(或靜態方法的 $REENTRANTLOCK
),並會新增適當的同步程式碼。例如,下列程式碼
import groovy.transform.WithReadLock
import groovy.transform.WithWriteLock
class Counters {
public final Map<String,Integer> map = [:].withDefault { 0 }
@WithReadLock
int get(String id) {
map.get(id)
}
@WithWriteLock
void add(String id, int num) {
Thread.sleep(200) // emulate long computation
map.put(id, map.get(id)+num)
}
}
等於下列程式碼
import groovy.transform.WithReadLock as WithReadLock
import groovy.transform.WithWriteLock as WithWriteLock
public class Counters {
private final Map<String, Integer> map
private final java.util.concurrent.locks.ReentrantReadWriteLock $reentrantlock
public int get(java.lang.String id) {
$reentrantlock.readLock().lock()
try {
map.get(id)
}
finally {
$reentrantlock.readLock().unlock()
}
}
public void add(java.lang.String id, int num) {
$reentrantlock.writeLock().lock()
try {
java.lang.Thread.sleep(200)
map.put(id, map.get(id) + num )
}
finally {
$reentrantlock.writeLock().unlock()
}
}
}
@WithReadLock
和 @WithWriteLock
都支援指定替代鎖定物件。在這種情況下,參考的欄位必須由使用者宣告,如下列替代程式碼所示
import groovy.transform.WithReadLock
import groovy.transform.WithWriteLock
import java.util.concurrent.locks.ReentrantReadWriteLock
class Counters {
public final Map<String,Integer> map = [:].withDefault { 0 }
private final ReentrantReadWriteLock customLock = new ReentrantReadWriteLock()
@WithReadLock('customLock')
int get(String id) {
map.get(id)
}
@WithWriteLock('customLock')
void add(String id, int num) {
Thread.sleep(200) // emulate long computation
map.put(id, map.get(id)+num)
}
}
詳細資訊
-
請參閱 groovy.transform.WithReadLock 的 Javadoc
-
請參閱 groovy.transform.WithWriteLock 的 Javadoc
2.1.5. 更容易複製和外化
Groovy 提供兩個註解,旨在簡化 Cloneable
和 Externalizable
介面的實作,分別命名為 @AutoClone
和 @AutoExternalize
。
@groovy.transform.AutoClone
@AutoClone
註解旨在使用各種策略來實作 @java.lang.Cloneable
介面,這要感謝 style
參數
-
預設的
AutoCloneStyle.CLONE
策略會先呼叫super.clone()
,然後對每個可複製的屬性呼叫clone()
-
AutoCloneStyle.SIMPLE
策略使用常規建構函呼叫,並將屬性從來源複製到複製項 -
AutoCloneStyle.COPY_CONSTRUCTOR
策略會建立並使用複製建構函 -
AutoCloneStyle.SERIALIZATION
策略使用序列化(或外化)來複製物件
這些策略各有優缺點,已在 groovy.transform.AutoClone 和 groovy.transform.AutoCloneStyle 的 Javadoc 中討論。
例如,下列範例
import groovy.transform.AutoClone
@AutoClone
class Book {
String isbn
String title
List<String> authors
Date publicationDate
}
等於下列程式碼
class Book implements Cloneable {
String isbn
String title
List<String> authors
Date publicationDate
public Book clone() throws CloneNotSupportedException {
Book result = super.clone()
result.authors = authors instanceof Cloneable ? (List) authors.clone() : authors
result.publicationDate = publicationDate.clone()
result
}
}
請注意,字串屬性並未明確處理,因為字串是不可變的,而 Object
的 clone()
方法會複製字串參考。這也適用於原始欄位和 java.lang.Number
的大多數具體子類別。
除了複製樣式之外,@AutoClone
還支援多個選項
屬性 | 預設值 | 說明 | 範例 |
---|---|---|---|
excludes |
空清單 |
需要從複製中排除的屬性或欄位名稱清單。也允許使用由逗號分隔的欄位/屬性名稱組成的字串。有關詳細資訊,請參閱 groovy.transform.AutoClone#excludes |
|
includeFields |
false |
預設情況下,只會複製屬性。將此旗標設定為 true 也會複製欄位。 |
|
@groovy.transform.AutoExternalize
@AutoExternalize
AST 轉換將協助建立 java.io.Externalizable
類別。它會自動將介面新增到類別中,並產生 writeExternal
和 readExternal
方法。例如,這段程式碼
import groovy.transform.AutoExternalize
@AutoExternalize
class Book {
String isbn
String title
float price
}
將轉換為
class Book implements java.io.Externalizable {
String isbn
String title
float price
void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(isbn)
out.writeObject(title)
out.writeFloat( price )
}
public void readExternal(ObjectInput oin) {
isbn = (String) oin.readObject()
title = (String) oin.readObject()
price = oin.readFloat()
}
}
@AutoExternalize
注解支援兩個參數,讓您能稍微自訂它的行為
屬性 | 預設值 | 說明 | 範例 |
---|---|---|---|
excludes |
空清單 |
需要從外部化中排除的屬性或欄位名稱清單。也允許使用由逗號分隔的欄位/屬性名稱組成的字串。請參閱 groovy.transform.AutoExternalize#excludes 以取得詳細資料 |
|
includeFields |
false |
預設情況下,只有屬性會被外部化。將此旗標設定為 true 也會複製欄位。 |
|
2.1.6. 更安全的指令碼編寫
Groovy 語言讓您能輕鬆在執行階段執行使用者指令碼(例如使用 groovy.lang.GroovyShell),但您如何確保指令碼不會耗盡所有 CPU(無限迴圈)或並行的指令碼不會慢慢消耗執行緒池中的所有可用執行緒?Groovy 提供了幾個註解,這些註解旨在讓指令碼編寫更安全,產生例如允許您自動中斷執行的程式碼。
@groovy.transform.ThreadInterrupt
在 JVM 世界中,一個複雜的情況是當執行緒無法停止時。Thread#stop
方法存在,但已標示為過時(且不可靠),因此您唯一的機會在於 Thread#interrupt
。呼叫後者會在執行緒上設定 interrupt
旗標,但它不會停止執行緒的執行。這很麻煩,因為執行緒中執行的程式碼有責任檢查中斷旗標並適當地退出。當您身為開發人員時,這很有道理,因為您知道自己執行的程式碼是要在獨立執行緒中執行,但一般來說,您並不知道。對於可能甚至不知道執行緒是什麼的使用者指令碼來說,情況更糟(想想 DSL)。
@ThreadInterrupt
透過在程式碼中的關鍵位置新增執行緒中斷檢查來簡化這一點
-
迴圈(for、while)
-
方法的第一個指令
-
封閉主體的第一個指令
想像一下以下使用者指令碼
while (true) {
i++
}
這是一個明顯的無限迴圈。如果此程式碼在其自己的執行緒中執行,中斷不會有幫助:如果您在執行緒上join
,則呼叫程式碼將能夠繼續,但執行緒仍會處於執行狀態,在背景中執行,而您無法停止它,緩慢導致執行緒飢餓。
解決此問題的一種可能性是這樣設定您的 shell
def config = new CompilerConfiguration()
config.addCompilationCustomizers(
new ASTTransformationCustomizer(ThreadInterrupt)
)
def binding = new Binding(i:0)
def shell = new GroovyShell(binding,config)
然後將 shell 設定為對所有指令碼自動套用 @ThreadInterrupt
AST 轉換。這允許您以這種方式執行使用者指令碼
def t = Thread.start {
shell.evaluate(userCode)
}
t.join(1000) // give at most 1000ms for the script to complete
if (t.alive) {
t.interrupt()
}
轉換自動修改使用者程式碼,如下所示
while (true) {
if (Thread.currentThread().interrupted) {
throw new InterruptedException('The current thread has been interrupted.')
}
i++
}
在迴圈中引入的檢查保證,如果在目前執行緒上設定 interrupt
旗標,則會擲回例外,中斷執行緒的執行。
@ThreadInterrupt
支援多個選項,可讓您進一步自訂轉換的行為
屬性 | 預設值 | 說明 | 範例 |
---|---|---|---|
擲回 |
|
指定如果執行緒中斷時擲回的例外類型。 |
|
checkOnMethodStart |
true |
是否應在每個方法主體的開頭插入中斷檢查。請參閱 groovy.transform.ThreadInterrupt 以取得詳細資訊。 |
|
applyToAllClasses |
true |
是否應對同一個來源單元(在同一個來源檔案中)的所有類別套用轉換。請參閱 groovy.transform.ThreadInterrupt 以取得詳細資訊。 |
|
applyToAllMembers |
true |
是否應對類別的所有成員套用轉換。請參閱 groovy.transform.ThreadInterrupt 以取得詳細資訊。 |
|
@groovy.transform.TimedInterrupt
@TimedInterrupt
AST 轉換嘗試解決與 @groovy.transform.ThreadInterrupt
略有不同的問題:它不會檢查執行緒的 interrupt
旗標,而是如果執行緒執行時間過長,它會自動擲回例外。
此註解不會產生監控執行緒。相反地,它以類似於 @ThreadInterrupt 的方式運作,在程式碼中的適當位置放置檢查。這表示如果您有一個被 I/O 阻擋的執行緒,它不會中斷。
|
想像以下使用者程式碼
def fib(int n) { n<2?n:fib(n-1)+fib(n-2) }
result = fib(600)
此處著名的費氏數列運算實作遠未最佳化。如果呼叫時使用較高的 n
值,可能需要數分鐘才能得到答案。使用 @TimedInterrupt
,您可以選擇腳本允許執行的時間長度。下列設定程式碼將允許使用者腳本最多執行 1 秒
def config = new CompilerConfiguration()
config.addCompilationCustomizers(
new ASTTransformationCustomizer(value:1, TimedInterrupt)
)
def binding = new Binding(result:0)
def shell = new GroovyShell(this.class.classLoader, binding,config)
這段程式碼等於使用 @TimedInterrupt
註解類別,如下所示
@TimedInterrupt(value=1, unit=TimeUnit.SECONDS)
class MyClass {
def fib(int n) {
n<2?n:fib(n-1)+fib(n-2)
}
}
@TimedInterrupt
支援多個選項,可讓您進一步自訂轉換行為
屬性 | 預設值 | 說明 | 範例 |
---|---|---|---|
值 |
Long.MAX_VALUE |
與 |
|
單位 |
TimeUnit.SECONDS |
與 |
|
擲回 |
|
指定逾時時擲回的例外狀況類型。 |
|
checkOnMethodStart |
true |
是否應在每個方法主體的開頭插入中斷檢查。有關詳細資訊,請參閱 groovy.transform.TimedInterrupt。 |
|
applyToAllClasses |
true |
是否應將轉換套用至同一個來源單元的全部類別(在同一個來源檔案中)。有關詳細資訊,請參閱 groovy.transform.TimedInterrupt。 |
|
applyToAllMembers |
true |
是否應將轉換套用至類別的所有成員。有關詳細資訊,請參閱 groovy.transform.TimedInterrupt。 |
|
@TimedInterrupt 目前與靜態方法不相容!
|
@groovy.transform.ConditionalInterrupt
最後一個用於更安全腳本編寫的註解是您想要使用自訂策略中斷腳本時的基礎註解。特別是,如果您想要使用資源管理(限制對 API 的呼叫次數,…),這是您選擇的註解。在以下範例中,使用者程式碼使用無限迴圈,但是 @ConditionalInterrupt
將允許我們檢查配額管理員並自動中斷腳本
@ConditionalInterrupt({Quotas.disallow('user')})
class UserCode {
void doSomething() {
int i=0
while (true) {
println "Consuming resources ${++i}"
}
}
}
此處的配額檢查非常基本,但它可以是任何程式碼
class Quotas {
static def quotas = [:].withDefault { 10 }
static boolean disallow(String userName) {
println "Checking quota for $userName"
(quotas[userName]--)<0
}
}
我們可以使用這個測試程式碼來確保 @ConditionalInterrupt
正常運作
assert Quotas.quotas['user'] == 10
def t = Thread.start {
new UserCode().doSomething()
}
t.join(5000)
assert !t.alive
assert Quotas.quotas['user'] < 0
當然,在實際應用中,不太可能手動將 @ConditionalInterrupt
新增至使用者程式碼。它可以採用類似於 ThreadInterrupt 區段中顯示的範例的方式注入,使用 org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
def config = new CompilerConfiguration()
def checkExpression = new ClosureExpression(
Parameter.EMPTY_ARRAY,
new ExpressionStatement(
new MethodCallExpression(new ClassExpression(ClassHelper.make(Quotas)), 'disallow', new ConstantExpression('user'))
)
)
config.addCompilationCustomizers(
new ASTTransformationCustomizer(value: checkExpression, ConditionalInterrupt)
)
def shell = new GroovyShell(this.class.classLoader,new Binding(),config)
def userCode = """
int i=0
while (true) {
println "Consuming resources \\${++i}"
}
"""
assert Quotas.quotas['user'] == 10
def t = Thread.start {
shell.evaluate(userCode)
}
t.join(5000)
assert !t.alive
assert Quotas.quotas['user'] < 0
@ConditionalInterrupt
支援多個選項,可讓您進一步自訂轉換行為
屬性 | 預設值 | 說明 | 範例 |
---|---|---|---|
值 |
將呼叫的閉包用於檢查是否允許執行。如果閉包傳回 false,則允許執行。如果傳回 true,則會擲回例外狀況。 |
|
|
擲回 |
|
指定應中斷執行時擲回的例外狀況類型。 |
|
checkOnMethodStart |
true |
是否應在每個方法主體的開頭插入中斷檢查。有關詳細資訊,請參閱 groovy.transform.ConditionalInterrupt。 |
|
applyToAllClasses |
true |
是否應將轉換套用於同一個來源單元(在同一個來源檔案中)的所有類別。有關詳細資料,請參閱 groovy.transform.ConditionalInterrupt。 |
|
applyToAllMembers |
true |
是否應將轉換套用於類別的所有成員。有關詳細資料,請參閱 groovy.transform.ConditionalInterrupt。 |
|
2.1.7. 編譯器指令
此類別的 AST 轉換會將註解分組,這些註解會直接影響程式碼的語意,而不是專注於程式碼產生。因此,它們可以視為編譯器指令,這些指令會在編譯時間或執行時間變更程式的行為。
@groovy.transform.Field
@Field
註解僅在指令碼的內容中才有意義,其目標是解決指令碼常見的範圍錯誤。例如,下列範例會在執行時間失敗
def x
String line() {
"="*x
}
x=3
assert "===" == line()
x=5
assert "=====" == line()
所引發的錯誤可能難以理解:groovy.lang.MissingPropertyException:沒有此屬性:x。原因在於指令碼會編譯成類別,而指令碼主體本身會編譯成單一的 run() 方法。在指令碼中定義的方法是獨立的,因此上述程式碼等同於下列程式碼
class MyScript extends Script {
String line() {
"="*x
}
public def run() {
def x
x=3
assert "===" == line()
x=5
assert "=====" == line()
}
}
因此,def x
會有效地詮釋為一個區域變數,超出 line
方法的範圍。@Field
AST 轉換的目標是透過將變數的範圍變更為封閉指令碼的欄位來修正此問題
@Field def x
String line() {
"="*x
}
x=3
assert "===" == line()
x=5
assert "=====" == line()
現在,結果等效的程式碼為
class MyScript extends Script {
def x
String line() {
"="*x
}
public def run() {
x=3
assert "===" == line()
x=5
assert "=====" == line()
}
}
@groovy.transform.PackageScope
預設情況下,Groovy 可見性規則暗示,如果您建立一個欄位而未指定修飾詞,則該欄位會詮釋為一個屬性
class Person {
String name // this is a property
}
如果您想要建立一個封裝私有欄位,而不是屬性(私有欄位+getter/setter),請使用 @PackageScope
註解您的欄位
class Person {
@PackageScope String name // not a property anymore
}
@PackageScope
註解也可以用於類別、方法和建構函式。此外,透過在類別層級指定 PackageScopeTarget
值清單作為註解屬性,該類別中所有沒有明確修飾詞且符合所提供的 PackageScopeTarget
的成員都會維持封裝保護。例如,若要套用至類別中的欄位,請使用下列註解
import static groovy.transform.PackageScopeTarget.FIELDS
@PackageScope(FIELDS)
class Person {
String name // not a property, package protected
Date dob // not a property, package protected
private int age // explicit modifier, so won't be touched
}
@PackageScope
註解很少用於一般 Groovy 約定,但有時對於應在封裝中內部可見的工廠方法,或用於測試目的的方法或建構函式,或是與需要此類可見性約定的第三方程式庫整合時很有用。
@groovy.transform.Final
@Final
本質上是 final
修飾詞的別名。用意是您幾乎不會直接使用 @Final
註解(只要使用 final
即可)。但是,在建立應將 final 修飾詞套用至要註解的節點的元註解時,您可以混合使用 @Final
,例如。
@AnnotationCollector([Singleton,Final]) @interface MySingleton {}
@MySingleton
class GreetingService {
String greeting(String name) { "Hello, $name!" }
}
assert GreetingService.instance.greeting('Bob') == 'Hello, Bob!'
assert Modifier.isFinal(GreetingService.modifiers)
@groovy.transform.AutoFinal
@AutoFinal
註解指示編譯器在註解節點中的許多地方自動插入 final 修飾詞。如果套用至方法(或建構函式),該方法(或建構函式)的參數會標示為 final。如果套用至類別定義,所有宣告的方法和建構函式也會套用相同的處理方式。
通常會認為重新指派方法或建構函式的參數給其主體是不好的做法。透過將 final 修飾詞新增至所有參數宣告,您可以完全避免這種做法。有些程式設計師認為在所有地方新增 final 會增加樣板程式碼的數量,並讓方法簽章有點雜亂。另一種替代方法可能是使用程式碼檢閱程序,或套用 codenarc 規則,以在觀察到這種做法時發出警告,但這些替代方法可能會導致品質檢查期間的回饋延遲,而不是在 IDE 或編譯期間。@AutoFinal
註解的目標是最大化編譯器/IDE 回饋,同時保留簡潔的程式碼,並將樣板雜訊降至最低。
以下範例說明如何在類別層級套用註解
import groovy.transform.AutoFinal
@AutoFinal
class Person {
private String first, last
Person(String first, String last) {
this.first = first
this.last = last
}
String fullName(String separator) {
"$first$separator$last"
}
String greeting(String salutation) {
"$salutation, $first"
}
}
在此範例中,建構函式的兩個參數和 fullname
與 greeting
方法的單一參數會是 final。編譯器會標示出嘗試在建構函式或方法主體中修改這些參數的行為。
以下範例說明如何在方法層級套用註解
class Calc {
@AutoFinal
int add(int a, int b) { a + b }
int mult(int a, int b) { a * b }
}
在此,add
方法會有 final 參數,但 mult
方法會保持不變。
@groovy.transform.AnnotationCollector
@AnnotationCollector
允許建立元註解,在 專屬區段 中有說明。
@groovy.transform.TypeChecked
@TypeChecked
會在 Groovy 程式碼中啟用編譯時期類型檢查。有關詳細資訊,請參閱 類型檢查區段。
@groovy.transform.CompileStatic
@CompileStatic
會在 Groovy 程式碼中啟用靜態編譯。有關詳細資訊,請參閱 類型檢查區段。
@groovy.transform.CompileDynamic
@CompileDynamic
會停用 Groovy 程式碼部分的靜態編譯。有關詳細資訊,請參閱 類型檢查區段。
@groovy.transform.SelfType
@SelfType
並非 AST 轉換,而是一個用於特質的標記介面。有關進一步的詳細資訊,請參閱 特質文件。
2.1.8. Swing 模式
@groovy.beans.Bindable
@Bindable
是一個 AST 轉換,它會將一般屬性轉換為繫結屬性(根據 JavaBeans 規範)。@Bindable
註解可以放在屬性或類別上。若要將類別的所有屬性轉換為繫結屬性,可以在類別上加上註解,如下面的範例所示
import groovy.beans.Bindable
@Bindable
class Person {
String name
int age
}
這等同於撰寫以下內容
import java.beans.PropertyChangeListener
import java.beans.PropertyChangeSupport
class Person {
final private PropertyChangeSupport this$propertyChangeSupport
String name
int age
public void addPropertyChangeListener(PropertyChangeListener listener) {
this$propertyChangeSupport.addPropertyChangeListener(listener)
}
public void addPropertyChangeListener(String name, PropertyChangeListener listener) {
this$propertyChangeSupport.addPropertyChangeListener(name, listener)
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
this$propertyChangeSupport.removePropertyChangeListener(listener)
}
public void removePropertyChangeListener(String name, PropertyChangeListener listener) {
this$propertyChangeSupport.removePropertyChangeListener(name, listener)
}
public void firePropertyChange(String name, Object oldValue, Object newValue) {
this$propertyChangeSupport.firePropertyChange(name, oldValue, newValue)
}
public PropertyChangeListener[] getPropertyChangeListeners() {
return this$propertyChangeSupport.getPropertyChangeListeners()
}
public PropertyChangeListener[] getPropertyChangeListeners(String name) {
return this$propertyChangeSupport.getPropertyChangeListeners(name)
}
}
因此,@Bindable
會移除類別中許多樣板程式碼,大幅提高可讀性。如果註解放在單一屬性上,則只會繫結該屬性
import groovy.beans.Bindable
class Person {
String name
@Bindable int age
}
@groovy.beans.ListenerList
@ListenerList
AST 轉換會產生程式碼,用於新增、移除和取得類別的監聽器清單,只要註解集合屬性即可
import java.awt.event.ActionListener
import groovy.beans.ListenerList
class Component {
@ListenerList
List<ActionListener> listeners;
}
轉換會根據清單的泛型類型產生適當的 add/remove 方法。此外,它還會根據類別中宣告的公開方法建立 fireXXX
方法
import java.awt.event.ActionEvent
import java.awt.event.ActionListener as ActionListener
import groovy.beans.ListenerList as ListenerList
public class Component {
@ListenerList
private List<ActionListener> listeners
public void addActionListener(ActionListener listener) {
if ( listener == null) {
return
}
if ( listeners == null) {
listeners = []
}
listeners.add(listener)
}
public void removeActionListener(ActionListener listener) {
if ( listener == null) {
return
}
if ( listeners == null) {
listeners = []
}
listeners.remove(listener)
}
public ActionListener[] getActionListeners() {
Object __result = []
if ( listeners != null) {
__result.addAll(listeners)
}
return (( __result ) as ActionListener[])
}
public void fireActionPerformed(ActionEvent param0) {
if ( listeners != null) {
ArrayList<ActionListener> __list = new ArrayList<ActionListener>(listeners)
for (def listener : __list ) {
listener.actionPerformed(param0)
}
}
}
}
@Bindable
支援多個選項,可讓您進一步自訂轉換的行為
屬性 | 預設值 | 說明 | 範例 |
---|---|---|---|
name |
泛型類型名稱 |
預設情況下,附加到 add/remove/… 方法的字尾是清單泛型類型的簡單類別名稱。 |
|
synchronize |
false |
如果設定為 true,產生的方法將會同步 |
|
@groovy.beans.Vetoable
@Vetoable
註解的工作方式類似於 @Bindable
,但會根據 JavaBeans 規格產生受約束屬性,而非繫結屬性。此註解可以放在類別上,表示所有屬性都將轉換為受約束屬性,或放在單一屬性上。例如,使用 @Vetoable
註解此類別
import groovy.beans.Vetoable
import java.beans.PropertyVetoException
import java.beans.VetoableChangeListener
@Vetoable
class Person {
String name
int age
}
等同於撰寫此內容
public class Person {
private String name
private int age
final private java.beans.VetoableChangeSupport this$vetoableChangeSupport
public void addVetoableChangeListener(VetoableChangeListener listener) {
this$vetoableChangeSupport.addVetoableChangeListener(listener)
}
public void addVetoableChangeListener(String name, VetoableChangeListener listener) {
this$vetoableChangeSupport.addVetoableChangeListener(name, listener)
}
public void removeVetoableChangeListener(VetoableChangeListener listener) {
this$vetoableChangeSupport.removeVetoableChangeListener(listener)
}
public void removeVetoableChangeListener(String name, VetoableChangeListener listener) {
this$vetoableChangeSupport.removeVetoableChangeListener(name, listener)
}
public void fireVetoableChange(String name, Object oldValue, Object newValue) throws PropertyVetoException {
this$vetoableChangeSupport.fireVetoableChange(name, oldValue, newValue)
}
public VetoableChangeListener[] getVetoableChangeListeners() {
return this$vetoableChangeSupport.getVetoableChangeListeners()
}
public VetoableChangeListener[] getVetoableChangeListeners(String name) {
return this$vetoableChangeSupport.getVetoableChangeListeners(name)
}
public void setName(String value) throws PropertyVetoException {
this.fireVetoableChange('name', name, value)
name = value
}
public void setAge(int value) throws PropertyVetoException {
this.fireVetoableChange('age', age, value)
age = value
}
}
如果註解放在單一屬性上,只有該屬性會變成可否決的
import groovy.beans.Vetoable
class Person {
String name
@Vetoable int age
}
2.1.9. 測試協助
@groovy.test.NotYetImplemented
@NotYetImplemented
用於反轉 JUnit 3/4 測試案例的結果。如果某個功能尚未實作,但測試已經實作,這個功能特別有用。在這種情況下,預期測試會失敗。使用 @NotYetImplemented
標記它會反轉測試結果,如下面的範例所示
import groovy.test.GroovyTestCase
import groovy.test.NotYetImplemented
class Maths {
static int fib(int n) {
// todo: implement later
}
}
class MathsTest extends GroovyTestCase {
@NotYetImplemented
void testFib() {
def dataTable = [
1:1,
2:1,
3:2,
4:3,
5:5,
6:8,
7:13
]
dataTable.each { i, r ->
assert Maths.fib(i) == r
}
}
}
使用此技術的另一個優點是,您可以在知道如何修復錯誤之前,為錯誤撰寫測試案例。如果將來某個時候,程式碼中的修改間接修正了錯誤,您會收到通知,因為預期會失敗的測試通過了。
@groovy.transform.ASTTest
@ASTTest
是一個特殊的 AST 轉換,用於協助除錯其他 AST 轉換或 Groovy 編譯器本身。它會讓開發人員在編譯期間「探索」AST,並對 AST 執行斷言,而非對編譯結果執行斷言。這表示此 AST 轉換會在產生位元組碼之前存取 AST。@ASTTest
可以放在任何可註解的節點上,並需要兩個參數
-
phase:設定
@ASTTest
會觸發的階段。測試程式碼會在這個階段結束時處理 AST 樹。 -
value:當階段到達時,會在註解節點上執行的程式碼
編譯階段必須從 org.codehaus.groovy.control.CompilePhase 中選擇一個。但是,由於無法使用相同的註解註解節點兩次,因此您無法在兩個不同的編譯階段對同一個節點使用 @ASTTest 。
|
value
是閉包表示式,可以存取對應於註解節點的特殊變數 node
,以及一個輔助方法 lookup
,將在 這裡 討論。例如,您可以像這樣註解類別節點
import groovy.transform.ASTTest
import org.codehaus.groovy.ast.ClassNode
@ASTTest(phase=CONVERSION, value={ (1)
assert node instanceof ClassNode (2)
assert node.name == 'Person' (3)
})
class Person {
}
1 | 我們在轉換階段後檢查抽象語法樹的狀態 |
2 | 節點是指由 @ASTTest 加註的 AST 節點 |
3 | 它可以在編譯時用於執行斷言 |
@ASTTest
的一個有趣功能是,如果斷言失敗,則編譯將會失敗。現在想像一下,我們想要在編譯時檢查 AST 轉換的行為。我們將在此處採用 @PackageScope
,並且我們將想要驗證使用 @PackageScope
加註的屬性會變成套件私有欄位。為此,我們必須知道轉換在何階段執行,這可以在 org.codehaus.groovy.transform.PackageScopeASTTransformation 中找到:語意分析。然後可以像這樣撰寫測試
import groovy.transform.ASTTest
import groovy.transform.PackageScope
@ASTTest(phase=SEMANTIC_ANALYSIS, value={
def nameNode = node.properties.find { it.name == 'name' }
def ageNode = node.properties.find { it.name == 'age' }
assert nameNode
assert ageNode == null // shouldn't be a property anymore
def ageField = node.getDeclaredField 'age'
assert ageField.modifiers == 0
})
class Person {
String name
@PackageScope int age
}
@ASTTest
加註只能放置在語法允許的任何地方。有時,您可能想要測試未加註的 AST 節點的內容。在這種情況下,@ASTTest
提供一個方便的 lookup
方法,它會在 AST 中搜尋標記有特殊代碼的節點
def list = lookup('anchor') (1)
Statement stmt = list[0] (2)
1 | 傳回標籤為 'anchor' 的 AST 節點清單 |
2 | 由於查詢總是會傳回清單,因此必須選擇要處理的元素 |
例如,想像一下您想要測試 for 迴圈變數的宣告類型。然後您可以像這樣做
import groovy.transform.ASTTest
import groovy.transform.PackageScope
import org.codehaus.groovy.ast.ClassHelper
import org.codehaus.groovy.ast.expr.DeclarationExpression
import org.codehaus.groovy.ast.stmt.ForStatement
class Something {
@ASTTest(phase=SEMANTIC_ANALYSIS, value={
def forLoop = lookup('anchor')[0]
assert forLoop instanceof ForStatement
def decl = forLoop.collectionExpression.expressions[0]
assert decl instanceof DeclarationExpression
assert decl.variableExpression.name == 'i'
assert decl.variableExpression.originType == ClassHelper.int_TYPE
})
void someMethod() {
int x = 1;
int y = 10;
anchor: for (int i=0; i<x+y; i++) {
println "$i"
}
}
}
@ASTTest
也會在測試封閉中公開這些變數
-
node
對應於加註的節點,和往常一樣 -
compilationUnit
可存取目前的org.codehaus.groovy.control.CompilationUnit
-
compilePhase
傳回目前的編譯階段 (org.codehaus.groovy.control.CompilePhase
)
如果您未指定 phase
屬性,後者會很有用。在這種情況下,封閉會在每個編譯階段後 (包括 SEMANTIC_ANALYSIS
) 執行。轉換的內容會在每個階段後保留,讓您有機會檢查兩個階段之間的變更。
舉例來說,以下是您可以在類別節點上傾印註冊的 AST 轉換清單的方式
import groovy.transform.ASTTest
import groovy.transform.CompileStatic
import groovy.transform.Immutable
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.control.CompilePhase
@ASTTest(value={
System.err.println "Compile phase: $compilePhase"
ClassNode cn = node
System.err.println "Global AST xforms: ${compilationUnit?.ASTTransformationsContext?.globalTransformNames}"
CompilePhase.values().each {
def transforms = cn.getTransforms(it)
if (transforms) {
System.err.println "Ast xforms for phase $it:"
transforms.each { map ->
System.err.println(map)
}
}
}
})
@CompileStatic
@Immutable
class Foo {
}
以下是您如何在兩個階段之間記憶變數進行測試的方式
import groovy.transform.ASTTest
import groovy.transform.ToString
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.control.CompilePhase
@ASTTest(value={
if (compilePhase == CompilePhase.INSTRUCTION_SELECTION) { (1)
println "toString() was added at phase: ${added}"
assert added == CompilePhase.CANONICALIZATION (2)
} else {
if (node.getDeclaredMethods('toString') && added == null) { (3)
added = compilePhase (4)
}
}
})
@ToString
class Foo {
String name
}
1 | 如果目前的編譯階段為指令選擇 |
2 | 那麼我們要確保在 CANONICALIZATION 中已新增 toString |
3 | 否則,如果 toString 存在,且來自內容的變數 added 為空值 |
4 | 那麼這表示這個編譯階段就是新增 toString 的階段 |
2.1.10. Grape 處理
@groovy.lang.Grapes
Grape
是嵌入在 Groovy 中的相依性管理引擎,它仰賴數個註解,這些註解在 本指南章節 中有詳細說明。
2.2. 開發 AST 轉換
轉換有兩種:全域轉換和區域轉換。
-
全域轉換 由編譯器套用在正在編譯的程式碼上,無論轉換套用在哪裡。實作全域轉換的已編譯類別會放在 JAR 中,並新增到編譯器的類別路徑中,其中包含服務定位器檔案
META-INF/services/org.codehaus.groovy.transform.ASTTransformation
,以及一行轉換類別名稱。轉換類別必須具備無引數建構函式,並實作org.codehaus.groovy.transform.ASTTransformation
介面。它將針對 編譯中的每個來源執行,因此請務必不要建立會以廣泛且耗時的方式掃描所有 AST 的轉換,以保持編譯器快速。 -
區域轉換 是透過註解您要轉換的程式碼元素來套用在區域的轉換。為此,我們會重複使用註解符號,而這些註解應該實作
org.codehaus.groovy.transform.ASTTransformation
。編譯器會找出它們,並對這些程式碼元素套用轉換。
2.2.1. 編譯階段指南
Groovy AST 轉換必須在九個已定義的編譯階段之一執行 (org.codehaus.groovy.control.CompilePhase)。
全域轉換可以在任何階段套用,但區域轉換只能在語意分析階段或之後套用。簡而言之,編譯階段為
-
初始化:開啟原始檔和設定環境
-
剖析:語法用於產生代表原始碼的代幣樹
-
轉換:從代幣樹建立抽象語法樹 (AST)。
-
語意分析:執行語法無法檢查的一致性和有效性檢查,並解析類別。
-
規範化:完成 AST 建立
-
指令選取:選擇指令集,例如 Java 6 或 Java 7 位元組碼層級
-
類別產生:在記憶體中建立類別的位元組碼
-
輸出:將二進位輸出寫入檔案系統
-
完成:執行任何最後的清理
一般來說,在後面的階段會有更多型別資訊可用。如果您的轉換涉及讀取 AST,則資訊較豐富的後續階段可能是個不錯的選擇。如果您的轉換涉及寫入 AST,則樹狀結構較稀疏的早期階段可能會更方便。
2.2.2. 區域轉換
區域 AST 轉換相對於套用它們的內容。在多數情況下,內容由註解定義,註解將定義轉換的範圍。例如,註解一個欄位表示轉換套用於該欄位,而註解類別表示轉換套用於整個類別。
作為一個天真且簡單的範例,考慮撰寫一個 @WithLogging
轉換,它會在方法呼叫的開始和結束處新增主控台訊息。因此,以下「Hello World」範例實際上會列印「Hello World」以及開始和停止訊息
@WithLogging
def greet() {
println "Hello World"
}
greet()
區域 AST 轉換是一種執行此操作的簡單方法。它需要兩件事
-
@WithLogging
註解的定義 -
org.codehaus.groovy.transform.ASTTransformation
的實作,它會將記錄運算式新增至方法
ASTTransformation
是一個回呼,讓您可以存取 org.codehaus.groovy.control.SourceUnit,透過它您可以取得 org.codehaus.groovy.ast.ModuleNode (AST) 的參考。
AST (抽象語法樹) 是一個樹狀結構,主要由 org.codehaus.groovy.ast.expr.Expression (表達式) 或 org.codehaus.groovy.ast.expr.Statement (陳述式) 組成。要了解 AST 的一個簡單方法是在偵錯器中探索它。一旦您有了 AST,就可以分析它以找出有關程式碼的資訊,或重新撰寫它以新增新功能。
本機轉換註解是最簡單的部分。以下是 @WithLogging
import org.codehaus.groovy.transform.GroovyASTTransformationClass
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.METHOD])
@GroovyASTTransformationClass(["gep.WithLoggingASTTransformation"])
public @interface WithLogging {
}
註解保留可以是 SOURCE
,因為您不需要該註解。這裡的元素類型是 METHOD
,@WithLogging
,因為該註解適用於方法。
但最重要的部分是 @GroovyASTTransformationClass
註解。這會將 @WithLogging
註解連結到您將撰寫的 ASTTransformation
類別。gep.WithLoggingASTTransformation
是我們將撰寫的 ASTTransformation
的完全限定類別名稱。這條線將註解連接到轉換。
有了這個,Groovy 編譯器將在原始碼單元中找到 @WithLogging
時呼叫 gep.WithLoggingASTTransformation
。現在,在執行範例指令碼時,在 IDE 中設定的任何中斷點都將在 LoggingASTTransformation
中觸發。
ASTTransformation
類別稍微複雜一些。以下是為 @WithLogging
新增方法開始和停止訊息的非常簡單且非常天真的轉換
@CompileStatic (1)
@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS) (2)
class WithLoggingASTTransformation implements ASTTransformation { (3)
@Override
void visit(ASTNode[] nodes, SourceUnit sourceUnit) { (4)
MethodNode method = (MethodNode) nodes[1] (5)
def startMessage = createPrintlnAst("Starting $method.name") (6)
def endMessage = createPrintlnAst("Ending $method.name") (7)
def existingStatements = ((BlockStatement)method.code).statements (8)
existingStatements.add(0, startMessage) (9)
existingStatements.add(endMessage) (10)
}
private static Statement createPrintlnAst(String message) { (11)
new ExpressionStatement(
new MethodCallExpression(
new VariableExpression("this"),
new ConstantExpression("println"),
new ArgumentListExpression(
new ConstantExpression(message)
)
)
)
}
}
1 | 即使不是強制性的,如果您使用 Groovy 撰寫 AST 轉換,強烈建議使用 CompileStatic ,因為它會提升編譯器的效能。 |
2 | 使用 org.codehaus.groovy.transform.GroovyASTTransformation 註解,以告知轉換需要在編譯的哪個階段執行。這裡是在語意分析階段。 |
3 | 實作 ASTTransformation 介面 |
4 | 它只有一個 visit 方法 |
5 | nodes 參數是一個 2 AST 節點陣列,其中第一個是註解節點 (@WithLogging ),第二個是註解節點 (方法節點) |
6 | 建立一個陳述式,當我們進入方法時會印出訊息 |
7 | 建立一個陳述式,當我們離開方法時會印出訊息 |
8 | 取得方法主體,在本例中是一個 BlockStatement |
9 | 在現有程式碼的第一個陳述式之前加入進入方法訊息 |
10 | 在現有程式碼的最後一個陳述式之後附加離開方法訊息 |
11 | 建立一個 ExpressionStatement ,包裝一個 MethodCallExpression ,對應於 this.println("message") |
請注意,為了簡化這個範例,我們沒有進行必要的檢查,例如檢查附註節點是否真的是一個 MethodNode
,或是方法主體是否是一個 BlockStatement
的實例。這項練習留給讀者。
請注意在 createPrintlnAst(String)
方法中建立新的 println 陳述式。建立程式碼的 AST 並不總是簡單。在本例中,我們需要建構一個新的方法呼叫,傳入接收器/變數、方法名稱和一個引數清單。在建立 AST 時,寫下您嘗試在 Groovy 檔案中建立的程式碼,然後在除錯器中檢查該程式碼的 AST,以瞭解要建立什麼,可能會有所幫助。然後使用您透過除錯器學到的知識,撰寫一個像 createPrintlnAst
這樣的函式。
最後
@WithLogging
def greet() {
println "Hello World"
}
greet()
產生
Starting greet Hello World Ending greet
請注意,AST 轉換會直接參與編譯過程。初學者常見的錯誤是將 AST 轉換程式碼放在與使用轉換的類別相同的原始碼樹中。一般來說,在相同的原始碼樹中表示它們會在同一時間編譯。由於轉換本身會分階段編譯,而且每個編譯階段會在進入下一個階段之前處理相同原始碼單元的全部檔案,因此會產生一個直接的後果:轉換不會在使用它的類別之前編譯!結論是,在使用 AST 轉換之前需要先編譯它們。一般來說,這就像將它們放在一個獨立的原始碼樹中一樣簡單。 |
2.2.3. 全域轉換
全域 AST 轉換類似於區域轉換,但有一個主要差異:它們不需要附註,表示它們會全域套用,也就是套用在每個正在編譯的類別上。因此,非常重要的是將它們的使用限制在最後的資源,因為它可能會對編譯器效能產生重大影響。
遵循局部 AST 轉換的範例,想像我們想要追蹤所有方法,而不僅僅是註解為 @WithLogging
的方法。基本上,我們需要這段程式碼的行為與先前註解為 @WithLogging
的程式碼相同
def greet() {
println "Hello World"
}
greet()
要讓這段程式碼運作,有兩個步驟
-
在
META-INF/services
目錄中建立org.codehaus.groovy.transform.ASTTransformation
描述符 -
建立
ASTTransformation
實作
需要描述符檔案,而且必須在類別路徑中找到。它將包含單一行
gep.WithLoggingASTTransformation
轉換的程式碼看起來類似於局部案例,但我們需要使用 SourceUnit
,而不是使用 ASTNode[]
參數
@CompileStatic (1)
@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS) (2)
class WithLoggingASTTransformation implements ASTTransformation { (3)
@Override
void visit(ASTNode[] nodes, SourceUnit sourceUnit) { (4)
def methods = sourceUnit.AST.methods (5)
methods.each { method -> (6)
def startMessage = createPrintlnAst("Starting $method.name") (7)
def endMessage = createPrintlnAst("Ending $method.name") (8)
def existingStatements = ((BlockStatement)method.code).statements (9)
existingStatements.add(0, startMessage) (10)
existingStatements.add(endMessage) (11)
}
}
private static Statement createPrintlnAst(String message) { (12)
new ExpressionStatement(
new MethodCallExpression(
new VariableExpression("this"),
new ConstantExpression("println"),
new ArgumentListExpression(
new ConstantExpression(message)
)
)
)
}
}
1 | 即使不是強制性的,如果您使用 Groovy 撰寫 AST 轉換,強烈建議使用 CompileStatic ,因為它會提升編譯器的效能。 |
2 | 使用 org.codehaus.groovy.transform.GroovyASTTransformation 註解,以告知轉換需要在編譯的哪個階段執行。這裡是在語意分析階段。 |
3 | 實作 ASTTransformation 介面 |
4 | 它只有一個 visit 方法 |
5 | sourceUnit 參數可存取正在編譯的來源,因此我們會取得目前來源的 AST,並從這個檔案中擷取方法清單 |
6 | 我們會逐一反覆運算來源檔案中的每個方法 |
7 | 建立一個陳述式,當我們進入方法時會印出訊息 |
8 | 建立一個陳述式,當我們離開方法時會印出訊息 |
9 | 取得方法主體,在本例中是一個 BlockStatement |
10 | 在現有程式碼的第一個陳述式之前加入進入方法訊息 |
11 | 在現有程式碼的最後一個陳述式之後附加離開方法訊息 |
12 | 建立一個 ExpressionStatement ,包裝一個 MethodCallExpression ,對應於 this.println("message") |
2.2.4. AST API 指南
AbstractASTTransformation
雖然您已經看到您可以直接實作 ASTTransformation
介面,但在幾乎所有情況下,您不會這麼做,而是延伸 org.codehaus.groovy.transform.AbstractASTTransformation 類別。這個類別提供多種實用方法,讓 AST 轉換更容易撰寫。Groovy 中包含的幾乎所有 AST 轉換都會延伸這個類別。
ClassCodeExpressionTransformer
能夠將表達式轉換為另一個表達式是很常見的用例。Groovy 提供了一個類別,讓您可以很輕鬆地做到這一點:org.codehaus.groovy.ast.ClassCodeExpressionTransformer
為了說明這一點,我們來建立一個 @Shout
轉換,它會將方法呼叫引數中的所有 String
常數轉換為它們的大寫版本。例如
@Shout
def greet() {
println "Hello World"
}
greet()
應該列印
HELLO WORLD
然後轉換的程式碼可以使用 ClassCodeExpressionTransformer
讓這變得更容易
@CompileStatic
@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS)
class ShoutASTTransformation implements ASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
ClassCodeExpressionTransformer trn = new ClassCodeExpressionTransformer() { (1)
private boolean inArgList = false
@Override
protected SourceUnit getSourceUnit() {
sourceUnit (2)
}
@Override
Expression transform(final Expression exp) {
if (exp instanceof ArgumentListExpression) {
inArgList = true
} else if (inArgList &&
exp instanceof ConstantExpression && exp.value instanceof String) {
return new ConstantExpression(exp.value.toUpperCase()) (3)
}
def trn = super.transform(exp)
inArgList = false
trn
}
}
trn.visitMethod((MethodNode)nodes[1]) (4)
}
}
1 | 轉換在內部會建立一個 ClassCodeExpressionTransformer |
2 | 轉換器需要傳回來源單元 |
3 | 如果在引數清單內偵測到字串類型的常數表達式,則將其轉換成大寫版本 |
4 | 對正在註解的方法呼叫轉換器 |
AST 節點
撰寫 AST 轉換需要深入了解 Groovy API 內部。特別是需要了解 AST 類別。由於這些類別是內部的,因此 API 將來有機會會變更,這表示您的轉換可能會中斷。儘管有此警告,但 AST 隨著時間的推移一直非常穩定,而且這種情況很少發生。 |
抽象語法樹的類別屬於 org.codehaus.groovy.ast
套件。建議讀者使用 Groovy 主控台,特別是 AST 瀏覽器工具,以了解這些類別。另一個學習資源是 AST Builder 測試套件。
2.2.5. 巨集
簡介
在 2.5.0 版本之前,開發人員在開發 AST 轉換時,應深入了解編譯器如何建置 AST (抽象語法樹),才能知道如何在編譯期間新增新的表達式或陳述式。
儘管使用 org.codehaus.groovy.ast.tool.GeneralUtils
靜態方法可以減輕建立表達式和陳述式的負擔,但這仍然是直接撰寫這些 AST 節點的低階方式。我們需要一些東西來抽象化直接撰寫 AST 的方式,而這正是 Groovy 巨集的用途。它們允許您在編譯期間直接新增程式碼,而無需將您腦海中的程式碼轉譯成 org.codehaus.groovy.ast.*
節點相關類別。
陳述式和表達式
讓我們看一個範例,建立一個本地的 AST 轉換:@AddMessageMethod
。當套用於給定的類別時,它會新增一個名為 getMessage
的新方法到該類別。此方法會傳回「42」。此註解相當直接
@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.TYPE])
@GroovyASTTransformationClass(["metaprogramming.AddMethodASTTransformation"])
@interface AddMethod { }
不使用巨集時,AST 轉換會是什麼樣子?類似這樣
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class AddMethodASTTransformation extends AbstractASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
ClassNode classNode = (ClassNode) nodes[1]
ReturnStatement code =
new ReturnStatement( (1)
new ConstantExpression("42")) (2)
MethodNode methodNode =
new MethodNode(
"getMessage",
ACC_PUBLIC,
ClassHelper.make(String),
[] as Parameter[],
[] as ClassNode[],
code) (3)
classNode.addMethod(methodNode) (4)
}
}
1 | 建立一個回傳陳述式 |
2 | 建立一個常數表達式「42」 |
3 | 將程式碼新增到新方法 |
4 | 將新方法新增到已註解的類別 |
如果您不習慣 AST API,這絕對不像您心目中的程式碼。現在看看前述程式碼如何透過使用巨集簡化。
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class AddMethodWithMacrosASTTransformation extends AbstractASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
ClassNode classNode = (ClassNode) nodes[1]
ReturnStatement simplestCode = macro { return "42" } (1)
MethodNode methodNode =
new MethodNode(
"getMessage",
ACC_PUBLIC,
ClassHelper.make(String),
[] as Parameter[],
[] as ClassNode[],
simplestCode) (2)
classNode.addMethod(methodNode) (3)
}
}
1 | 簡單多了。您想要新增一個傳回「42」的回傳陳述式,而這正是您可以在 macro 實用程式方法中讀到的。您的純文字程式碼會轉譯為 org.codehaus.groovy.ast.stmt.ReturnStatement |
2 | 將回傳陳述式新增到新方法 |
3 | 將新程式碼新增到已註解的類別 |
雖然在此範例中使用 macro
方法建立一個陳述式,但 macro
方法也可以用來建立表達式,這取決於您使用哪一個 macro
簽章
-
macro(Closure)
:使用封閉區塊中的程式碼建立一個給定的陳述式。 -
macro(Boolean,Closure)
:如果為true,則將封閉區塊中的表達式包裝在一個陳述式中;如果為false,則傳回一個表達式 -
macro(CompilePhase, Closure)
:在特定編譯階段使用封閉區塊中的程式碼建立一個給定的陳述式 -
macro(CompilePhase, Boolean, Closure)
:在特定編譯階段建立一個陳述式或表達式(true == 陳述式,false == 表達式)。
所有這些簽章都可以在 org.codehaus.groovy.macro.runtime.MacroGroovyMethods 中找到
|
有時我們可能只對建立一個給定的表達式感興趣,而不是整個陳述式,為此我們應該使用任何帶有布林參數的 macro
呼叫
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class AddGetTwoASTTransformation extends AbstractASTTransformation {
BinaryExpression onePlusOne() {
return macro(false) { 1 + 1 } (1)
}
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
ClassNode classNode = nodes[1]
BinaryExpression expression = onePlusOne() (2)
ReturnStatement returnStatement = GeneralUtils.returnS(expression) (3)
MethodNode methodNode =
new MethodNode("getTwo",
ACC_PUBLIC,
ClassHelper.Integer_TYPE,
[] as Parameter[],
[] as ClassNode[],
returnStatement (4)
)
classNode.addMethod(methodNode) (5)
}
}
1 | 我們告訴巨集不要將表達式包裝在陳述式中,我們只對表達式感興趣 |
2 | 指定表達式 |
3 | 使用 GeneralUtils 的方法建立 ReturnStatement 並傳回表達式 |
4 | 將程式碼新增到新方法 |
5 | 將方法新增至類別 |
變數替換
巨集很棒,但如果我們的巨集無法接收參數或解析周遭變數,我們就無法建立任何有用或可重複使用的東西。
在以下範例中,我們建立 AST 轉換 @MD5
,當套用至給定的字串欄位時,將新增一個傳回該欄位 MD5 值的方法。
@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.FIELD])
@GroovyASTTransformationClass(["metaprogramming.MD5ASTTransformation"])
@interface MD5 { }
以及轉換
@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
class MD5ASTTransformation extends AbstractASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
FieldNode fieldNode = nodes[1]
ClassNode classNode = fieldNode.declaringClass
String capitalizedName = fieldNode.name.capitalize()
MethodNode methodNode = new MethodNode(
"get${capitalizedName}MD5",
ACC_PUBLIC,
ClassHelper.STRING_TYPE,
[] as Parameter[],
[] as ClassNode[],
buildMD5MethodCode(fieldNode))
classNode.addMethod(methodNode)
}
BlockStatement buildMD5MethodCode(FieldNode fieldNode) {
VariableExpression fieldVar = GeneralUtils.varX(fieldNode.name) (1)
return macro(CompilePhase.SEMANTIC_ANALYSIS, true) { (2)
return java.security.MessageDigest
.getInstance('MD5')
.digest($v { fieldVar }.getBytes()) (3)
.encodeHex()
.toString()
}
}
}
1 | 我們需要變數表達式的參考 |
2 | 如果使用標準套件以外的類別,我們應該新增任何需要的匯入,或使用限定名稱。當使用給定靜態方法的限定名稱時,您需要確定它在適當的編譯階段中解析。在此特定案例中,我們指示巨集在 SEMANTIC_ANALYSIS 階段解析它,這是第一個具有類型資訊的編譯階段。 |
3 | 為了替換巨集中任何 expression ,我們需要使用 $v 方法。$v 接收一個閉包作為引數,而閉包僅允許替換表達式,表示繼承 org.codehaus.groovy.ast.expr.Expression 的類別。 |
MacroClass
正如我們先前提到的,macro
方法僅能產生 statements
和 expressions
。但如果我們想要產生其他類型的節點,例如方法、欄位等,該怎麼辦?
org.codehaus.groovy.macro.transform.MacroClass
可用於在我們的轉換中建立 類別(ClassNode 執行個體),就像我們先前使用 macro
方法建立陳述式和表達式一樣。
下一個範例是區域轉換 @Statistics
。當套用至給定類別時,它將新增兩個方法 getMethodCount() 和 getFieldCount(),分別傳回類別中的方法和欄位數量。以下是標記註解。
@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.TYPE])
@GroovyASTTransformationClass(["metaprogramming.StatisticsASTTransformation"])
@interface Statistics {}
以及 AST 轉換
@CompileStatic
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class StatisticsASTTransformation extends AbstractASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
ClassNode classNode = (ClassNode) nodes[1]
ClassNode templateClass = buildTemplateClass(classNode) (1)
templateClass.methods.each { MethodNode node -> (2)
classNode.addMethod(node)
}
}
@CompileDynamic
ClassNode buildTemplateClass(ClassNode reference) { (3)
def methodCount = constX(reference.methods.size()) (4)
def fieldCount = constX(reference.fields.size()) (5)
return new MacroClass() {
class Statistics {
java.lang.Integer getMethodCount() { (6)
return $v { methodCount }
}
java.lang.Integer getFieldCount() { (7)
return $v { fieldCount }
}
}
}
}
}
1 | 建立範本類別 |
2 | 將範本類別方法新增至註解類別 |
3 | 傳遞參考類別 |
4 | 擷取參考類別方法計數值表達式 |
5 | 擷取參考類別欄位計數值表達式 |
6 | 使用參考的方法計數值表達式建立 getMethodCount() 方法 |
7 | 使用參考的欄位計數值表達式建立 getFieldCount() 方法 |
基本上我們已建立 Statistics 類別作為範本,以避免撰寫低階 AST API,然後將範本類別中建立的方法複製到其最終目的地。
MacroClass 實作中的類型應在內部解析,這就是我們必須撰寫 java.lang.Integer 而不是僅撰寫 Integer 的原因。
|
請注意,我們正在使用 @CompileDynamic 。這是因為我們使用 MacroClass 的方式就像我們實際上正在實作它一樣。因此,如果您使用 @CompileStatic ,它會抱怨,因為抽象類別的實作不能是另一個不同的類別。
|
@Macro 方法
您已經看到,透過使用 macro
,您可以省下許多工作,但您可能會想知道該方法從何而來。您沒有宣告它或靜態匯入它。您可以將它視為一個特殊全域方法(或如果您喜歡,則為每個 Object
上的方法)。這很像 println
擴充方法的定義方式。但與 println
(它成為在編譯過程中稍後選取執行的方法)不同,macro
展開是在編譯過程的早期完成。將 macro
宣告為此早期展開的可用方法之一,是透過使用 @Macro
註解註解 macro
方法定義,並使用與擴充模組類似的機制提供該方法。此類方法稱為巨集方法,好消息是您可以定義自己的方法。
若要定義您自己的巨集方法,請以類似於擴充模組的方式建立一個類別,並新增一個方法,例如
public class ExampleMacroMethods {
@Macro
public static Expression safe(MacroContext macroContext, MethodCallExpression callExpression) {
return ternaryX(
notNullX(callExpression.getObjectExpression()),
callExpression,
constX(null)
);
}
...
}
現在,您會使用 META-INF/groovy
目錄中的 org.codehaus.groovy.runtime.ExtensionModule
檔案將此註冊為擴充模組。
現在,假設類別和元資料檔案在您的類別路徑中,您可以使用巨集方法,如下所示
def nullObject = null
assert null == safe(safe(nullObject.hashcode()).toString())
2.2.6. 測試 AST 轉換
分離來源樹
本節說明有關測試 AST 轉換的良好實務。前幾節強調了一個事實,即要能夠執行 AST 轉換,必須先編譯它。這聽起來可能很明顯,但很多人會因此而陷入困境,嘗試在定義 AST 轉換的相同來源樹中使用它。
因此,測試 AST 轉換的第一個提示是將測試來源與轉換來源分開。同樣地,這只不過是最佳實務,但您必須確保您的建置確實會將它們分開編譯。使用 Apache Maven 和 Gradle 時,預設情況下會這樣做。
除錯 AST 轉換
在 AST 轉換中設定中斷點非常方便,這樣你可以在 IDE 中除錯你的程式碼。不過,你可能會驚訝地發現你的 IDE 沒有在中斷點處停止。原因其實很簡單:如果你的 IDE 使用 Groovy 編譯器編譯 AST 轉換的單元測試,那麼編譯是由 IDE 觸發的,但編譯檔案的程序沒有除錯選項。只有在執行測試案例時,才會在虛擬機器上設定除錯選項。簡而言之:為時已晚,類別已經編譯完畢,你的轉換也已經套用。
一個非常簡單的解決方法是使用提供 assertScript
方法的 GroovyTestCase
類別。這表示在測試案例中撰寫此內容時
static class Subject {
@MyTransformToDebug
void methodToBeTested() {}
}
void testMyTransform() {
def c = new Subject()
c.methodToBeTested()
}
你應該撰寫
void testMyTransformWithBreakpoint() {
assertScript '''
import metaprogramming.MyTransformToDebug
class Subject {
@MyTransformToDebug
void methodToBeTested() {}
}
def c = new Subject()
c.methodToBeTested()
'''
}
不同之處在於,當你使用 assertScript
時,assertScript
區塊中的程式碼會在執行單元測試時編譯。也就是說,這次 Subject
類別會在除錯功能啟用的情況下編譯,並且會觸發中斷點。
ASTMatcher
有時候你可能想要對 AST 節點進行宣告;可能是為了篩選節點,或確保給定的轉換已建立預期的 AST 節點。
篩選節點
例如,如果你只想對特定的一組 AST 節點套用給定的轉換,你可以使用 ASTMatcher 來篩選這些節點。以下範例顯示如何將給定的表達式轉換為另一個表達式。使用 ASTMatcher 時,它會尋找特定的表達式 1 + 1
,並將其轉換為 3
。這就是我們稱它為 @Joking
範例的原因。
首先,我們建立只能套用於方法的 @Joking
註解
@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.METHOD])
@GroovyASTTransformationClass(["metaprogramming.JokingASTTransformation"])
@interface Joking { }
然後進行轉換,這只會將 org.codehaus.groovy.ast.ClassCodeExpressionTransformer
的一個執行個體套用於方法程式碼區塊中的所有表達式。
@CompileStatic
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class JokingASTTransformation extends AbstractASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
MethodNode methodNode = (MethodNode) nodes[1]
methodNode
.getCode()
.visit(new ConvertOnePlusOneToThree(source)) (1)
}
}
1 | 取得方法的程式碼陳述式,並套用表達式轉換器 |
而這是使用 ASTMatcher 將轉換只套用於符合表達式 1 + 1
的那些表達式的時候。
class ConvertOnePlusOneToThree extends ClassCodeExpressionTransformer {
SourceUnit sourceUnit
ConvertOnePlusOneToThree(SourceUnit sourceUnit) {
this.sourceUnit = sourceUnit
}
@Override
Expression transform(Expression exp) {
Expression ref = macro { 1 + 1 } (1)
if (ASTMatcher.matches(ref, exp)) { (2)
return macro { 3 } (3)
}
return super.transform(exp)
}
}
1 | 建立用作參考模式的表達式 |
2 | 檢查評估的目前表達式是否符合參考表達式 |
3 | 如果符合,則以 macro 建立的表達式取代目前的表達式 |
然後,您可以按以下方式測試實作
package metaprogramming
class Something {
@Joking
Integer getResult() {
return 1 + 1
}
}
assert new Something().result == 3
單元測試 AST 轉換
我們通常只檢查 AST 轉換的最後使用方式是否符合預期,即可測試 AST 轉換。但如果我們能輕易檢查轉換所新增的節點是否符合我們從一開始的預期,那就太好了。
以下轉換會將新的方法 giveMeTwo
新增到有註解的類別。
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class TwiceASTTransformation extends AbstractASTTransformation {
static final String VAR_X = 'x'
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
ClassNode classNode = (ClassNode) nodes[1]
MethodNode giveMeTwo = getTemplateClass(sumExpression)
.getDeclaredMethods('giveMeTwo')
.first()
classNode.addMethod(giveMeTwo) (1)
}
BinaryExpression getSumExpression() { (2)
return macro {
$v{ varX(VAR_X) } +
$v{ varX(VAR_X) }
}
}
ClassNode getTemplateClass(Expression expression) { (3)
return new MacroClass() {
class Template {
java.lang.Integer giveMeTwo(java.lang.Integer x) {
return $v { expression }
}
}
}
}
}
1 | 將方法新增到有註解的類別 |
2 | 建立二元運算式。二元運算式在 + 符號的兩側使用相同的變數運算式(在 org.codehaus.groovy.ast.tool.GeneralUtils 檢查 varX 方法)。 |
3 | 建立新的 ClassNode,其中包含稱為 giveMeTwo 的方法,此方法會傳回作為參數傳遞的運算式的結果。 |
現在,我不想建立測試來執行特定範例程式碼的轉換。我想檢查二元運算式的建置是否正確無誤
void testTestingSumExpression() {
use(ASTMatcher) { (1)
TwiceASTTransformation sample = new TwiceASTTransformation()
Expression referenceNode = macro {
a + a (2)
}.withConstraints { (3)
placeholder 'a' (4)
}
assert sample
.sumExpression
.matches(referenceNode) (5)
}
}
1 | 將 ASTMatcher 用作類別 |
2 | 建立範本節點 |
3 | 對該範本節點套用一些限制 |
4 | 告訴編譯器 a 是佔位符。 |
5 | 宣告參考節點和目前節點相等 |
當然,您隨時都可以/應該檢查實際執行狀況
void testASTBehavior() {
assertScript '''
package metaprogramming
@Twice
class AAA {
}
assert new AAA().giveMeTwo(1) == 2
'''
}
ASTTest
最後但並非最不重要的一點是,測試 AST 轉換也是在測試編譯期間的 AST 狀態。Groovy 提供一個名為 @ASTTest
的工具,用於執行這項工作:它是一個註解,可讓您在抽象語法樹中新增宣告。請查看 ASTTest 文件,以取得更多詳細資訊。
2.2.7. 外部參考
如果您有興趣逐步學習撰寫 AST 轉換,可以追蹤 這個工作坊。