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'
只有当 Groovy 运行时找不到给定属性的 getter 方法时,才会调用 propertyMissing(String)
方法。
对于 setter 方法,可以添加第二个 propertyMissing
定义,它接受一个额外的 value 参数:
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'
}
}
我们不能使用 println 等默认 Groovy 方法,因为这些方法被注入到所有 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 的更多信息,请参阅 元类 部分。 |
1.8. 类别 (Categories)
在某些情况下,如果一个不受控制的类有额外的功能方法,那将非常有用。为了实现这种能力,Groovy 实现了一种从 Objective-C 借鉴来的功能,称为“类别 (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
内部,可以访问类别方法。如上例所示,即使像 java.lang.Integer
或 java.util.Date
这样的 JDK 类也可以通过用户定义的方法进行丰富。
类别不需要直接暴露给用户代码,以下代码也可以:
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. 元类 (Metaclasses)
如前所述,元类在方法解析中起着核心作用。对于每次来自 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
groovy.lang.MetaClass 的自定义实现。通常,您会希望扩展现有的元类之一,例如 MetaClassImpl
、DelegatingMetaClass
、ExpandoMetaClass
或 ProxyMetaClass
;否则,您将需要实现完整的方法查找逻辑。在使用新的元类实例之前,您应该调用 groovy.lang.MetaClass#initialize(),否则元类可能不会按预期运行。
委托元类
如果您只需要装饰现有的元类,DelegatingMetaClass
可以简化该用例。旧的元类实现仍然可以通过 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
魔法包
可以通过给元类一个特殊构造(魔法)的类名和包名,在启动时更改元类。为了更改 java.lang.Integer
的元类,只需在类路径中放置一个 groovy.runtime.metaclass.java.lang.IntegerMetaClass
类即可。这很有用,例如,在与框架一起工作时,如果您想在代码被框架执行之前进行元类更改。魔法包的通用形式是 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
。下面的示例将使用这个新的元类:
// File testInteger.groovy
def i = 10
assert i.isBiggerThan5()
assert !i.isBiggerThan15()
println i.isBiggerThan5()
通过使用 groovy -cp . testInteger.groovy
运行该文件,IntegerMetaClass
将位于类路径中,因此它将成为 java.lang.Integer
的元类,拦截对 isBiggerThan*()
方法的调用。
1.9.3. 每实例元类
您可以单独更改单个对象的元类,因此可以拥有同一类的多个对象,但具有不同的元类。
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
。它之所以特殊,是因为它允许使用简洁的闭包语法动态添加或更改方法、构造函数、属性甚至静态方法。
在模拟或存根场景中,应用这些修改特别有用,如 测试指南 中所示。
Groovy 为每个 java.lang.Class
提供了一个特殊的 metaClass
属性,它将为您提供一个 ExpandoMetaClass
实例的引用。然后可以使用此实例添加方法或更改现有方法的行为。
默认情况下,ExpandoMetaClass 不支持继承。要启用此功能,您必须在应用程序启动之前调用 ExpandoMetaClass#enableGlobally() ,例如在 main 方法或 servlet 引导中。 |
以下各节将详细介绍如何在各种场景中使用 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 Web 应用程序框架中找到。“动态编解码器”的概念就是通过使用动态方法名来实现的。
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()
闭包代码中的第一步是查找给定名称和参数的 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
这里需要注意的重要一点是,除了 MetaMethod
之外,还会查找 MetaProperty
实例。如果存在,则调用 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')
File
类上不存在 getText
方法。但是,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 转换可能支持参数。
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
注解接受几个参数,这些参数总结在下表中
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
排除项 |
空列表 |
要从 toString 中排除的属性列表 |
|
包含项 |
未定义标记列表(表示所有字段) |
要包含在 toString 中的字段列表 |
|
includeSuper |
False |
是否应将超类包含在 toString 中 |
|
includeNames |
false |
是否在生成的 toString 中包含属性名称。 |
|
includeFields |
False |
除了属性之外,字段是否应包含在 toString 中 |
|
includeSuperProperties |
False |
超类属性是否应包含在 toString 中 |
|
includeSuperFields |
False |
可见的超类字段是否应包含在 toString 中 |
|
ignoreNulls |
False |
是否显示值为 null 的属性/字段 |
|
includePackage |
True |
在 toString 中使用完全限定类名而不是简单名称 |
|
allProperties |
True |
将所有 JavaBean 属性包含在 toString 中 |
|
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
的行为
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
排除项 |
空列表 |
要从 equals/hashCode 中排除的属性列表 |
|
包含项 |
未定义标记列表(表示所有字段) |
要包含在 equals/hashCode 中的字段列表 |
|
cache |
False |
缓存 hashCode 计算。仅当类是不可变时才应设置为 true。 |
|
callSuper |
False |
是否将 super 包含在 equals 和 hashCode 计算中 |
|
includeFields |
False |
除了属性之外,字段是否应包含在 equals/hashCode 中 |
|
useCanEqual |
True |
equals 是否应调用 canEqual 辅助方法。 |
|
allProperties |
False |
JavaBean 属性是否应包含在 equals 和 hashCode 计算中 |
|
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 在底层调用无参数构造函数,然后调用相关的 setter。值得注意的是,如果第一个属性(或字段)的类型为 LinkedHashMap,或者只有一个 Map、AbstractMap 或 HashMap 属性(或字段),则 map 样式的命名参数将不可用。
其他构造函数是按照它们定义的顺序获取属性生成的。Groovy 将生成与属性数量(或字段数量,取决于选项)相同的构造函数。
将 defaults
属性(参见可用配置选项表)设置为 false
,将禁用正常的默认值行为,这意味着
-
将只生成一个构造函数
-
尝试使用初始值将导致错误
-
映射式命名参数将不可用
此属性通常仅在另一个 Java 框架期望只有一个构造函数的情况下使用,例如注入框架或 JUnit 参数化运行器。
不变性支持
如果 @PropertyOptions
注解也出现在带有 @TupleConstructor
注解的类上,那么生成的构造函数可能包含自定义属性处理逻辑。例如,@PropertyOptions
注解上的 propertyHandler
属性可以设置为 ImmutablePropertyHandler
,这将导致添加不变类所需的逻辑(防御性拷贝,克隆等)。这通常在使用 @Immutable
元注解时会自动在幕后发生。某些注解属性可能不受所有属性处理程序支持。
自定义选项
@TupleConstructor
AST 转换接受几个注解属性
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
排除项 |
空列表 |
要从元组构造函数生成中排除的属性列表 |
|
包含项 |
未定义列表(表示所有字段) |
要包含在元组构造函数生成中的字段列表 |
|
includeProperties |
True |
是否应将属性包含在元组构造函数生成中 |
|
includeFields |
False |
除了属性之外,字段是否应包含在元组构造函数生成中 |
|
includeSuperProperties |
True |
超类中的属性是否应包含在元组构造函数生成中 |
|
includeSuperFields |
False |
超类中的字段是否应包含在元组构造函数生成中 |
|
callSuper |
False |
是否应在调用父构造函数时调用超类属性,而不是将其设置为属性 |
|
强制 |
False |
默认情况下,如果已定义构造函数,则转换将不执行任何操作。将此属性设置为 true,将生成构造函数,并且你有责任确保没有定义重复的构造函数。 |
|
默认值 |
True |
指示为构造函数参数启用了默认值处理。设置为 false 以获得正好一个构造函数,但禁用初始值支持和命名参数。 |
|
useSetters |
False |
默认情况下,转换将直接从其相应的构造函数参数设置每个属性的后备字段。将此属性设置为 true,构造函数将改为调用 setter(如果存在)。从构造函数内部调用可以被覆盖的 setter 通常被认为是不好的风格。你有责任避免这种不好的风格。 |
|
allNames |
False |
内部名称的字段和/或属性是否应包含在构造函数中 |
|
allProperties |
False |
JavaBean 属性是否应包含在构造函数中 |
|
前置 |
空 |
一个闭包,包含要插入到生成构造函数开头的语句 |
|
后置 |
空 |
一个闭包,包含要插入到生成构造函数末尾的语句 |
|
将 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 转换支持以下配置选项
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
构造函数注解 |
False |
复制时是否保留构造函数中的注解 |
|
参数注解 |
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
来引用。还值得注意的是,在类别类中使用实例字段本质上是不安全的:类别不是有状态的(像 trait 一样)。
@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,则初始化将使用双重检查锁定模式进行同步。
使用 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'))) }
可以通过将 auto
标志设置为 false
来禁用 Ruby
版本。
@groovy.transform.Sortable
@Sortable
AST 转换用于帮助编写可比较且通常可通过多个属性轻松排序的类。它易于使用,如下面的示例所示,我们注释了 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 |
前缀 |
包含/排除 |
includeSuperProperties |
allNames |
|
链式设置器 |
不适用 |
不适用 |
不适用 |
是,默认为“set” |
是 |
不适用 |
是,默认为 |
|
显式构建器类,正在构建的类未触及 |
不适用 |
不适用 |
是,默认为“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
,创建一个并注释一个 Groovy 构建器类,使用 @Builder
注解,使用 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 和构建方法。生成的构建方法将类似于
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 转换提供了对超类或接口中找到的任何抽象方法的虚拟实现。虚拟实现对于所有找到的抽象方法都相同,可以是
-
基本为空(对于 void 方法和有返回类型的方法,返回该类型的默认值)
-
抛出指定异常(带可选消息)的语句
-
一些用户提供的代码
第一个例子说明了默认情况。我们的类用 @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()
}
同样值得研究等效的生成代码,其中提供了三个 void 方法,所有这些方法都抛出提供的异常
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
注解,实现了一个接口,并且具有注解属性,指示对于任何提供的方法,都应该抛出带有消息 Not supported by 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'
同样值得研究等效的生成代码,其中提供了三个 void 方法,所有这些方法都抛出提供的异常
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 转换向构造函数和方法添加空检查守护语句,当提供了空参数时,这些方法会提前失败。它可以被视为一种防御性编程。该注解可以添加到单个方法或构造函数,也可以添加到类中,在这种情况下它将应用于所有方法/构造函数。
@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 注解的方法 |
|
方法注解 |
False |
是否将委托方法上的注解带到委托方法上。 |
|
参数注解 |
False |
是否将委托方法参数上的注解带到委托方法上。 |
|
排除项 |
空数组 |
要从委托中排除的方法列表。要进行更细粒度的控制,请参阅 |
|
包含项 |
未定义的标记数组(表示所有方法) |
要包含在委托中的方法列表。要进行更细粒度的控制,请参阅 |
|
排除类型 |
空数组 |
包含要从委托中排除的方法签名的接口列表 |
|
包含类型 |
未定义的标记数组(表示默认情况下没有列表) |
包含要包含在委托中的方法签名的接口列表 |
|
allNames |
False |
委托模式是否也应应用于具有内部名称的方法 |
|
@groovy.transform.Immutable
@Immutable
元注解结合了以下注解
@Immutable
元注解简化了不可变类的创建。不可变类非常有用,因为它们通常更容易理解,并且本质上是线程安全的。有关如何在 Java 中实现不可变类的所有详细信息,请参阅Effective Java, Minimize Mutability。@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 类)注解您的类,它们将被识别为不可变类中成员的 acceptable 类型。这使您无需显式使用 @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
目前,该注解仅适用于自递归方法调用,即对完全相同的方法再次进行单个递归调用。如果您有涉及简单相互递归的场景,请考虑使用 Closures 和 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
已弃用。请考虑改用 trait。
2.1.3. 日志记录改进
Groovy 提供了一系列 AST 转换,有助于集成最广泛使用的日志框架。每个常用框架都有一个转换和关联的注解。这些转换提供了一种简化的声明性方法来使用日志框架。在每种情况下,转换都会
-
向注解类添加一个对应于记录器的静态 final
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 添加到您的 classpath 中。
@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 添加到您的 classpath 中。此注解也可以与兼容的 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 添加到您的 classpath 中。
@groovy.util.logging.Slf4j
Groovy 使用 @Slf4j
注解支持 Simple Logging Facade for 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(s) 添加到您的 classpath 中。
@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
final 字段(或静态方法的 $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
还支持多个选项
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
排除项 |
空列表 |
需要从克隆中排除的属性或字段名称列表。也可以使用逗号分隔的字段/属性名称字符串。有关详细信息,请参阅 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
注解支持两个参数,可让您稍微自定义其行为
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
排除项 |
空列表 |
需要从外部化中排除的属性或字段名称列表。也可以使用逗号分隔的字段/属性名称字符串。有关详细信息,请参阅 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: No such property: 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 转换,而是在 trait 中使用的标记接口。有关详细信息,请参阅trait 文档。
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;
}
转换将根据列表的泛型类型生成相应的添加/删除方法。此外,它还将根据类上声明的公共方法创建 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
支持多个选项,可让您进一步自定义转换的行为
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
名称 |
泛型类型名称 |
默认情况下,将添加到添加/删除/...方法后面的后缀是列表泛型类型的简单类名。 |
|
同步 |
false |
如果设置为 true,生成的 method 将同步 |
|
@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 | 我们正在检查 CONVERSION 阶段后的抽象语法树的状态 |
2 | node 指的是被 @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 | 由于 lookup 总是返回一个列表,因此始终需要选择要处理的元素 |
例如,假设您要测试 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 | 那么我们想确保 toString 在 CANONICALIZATION 中添加 |
3 | 否则,如果 toString 存在并且上下文中的变量 added 为 null |
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 参数是一个包含两个 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
转换的代码看起来与局部情况类似,但我们不再使用 ASTNode[]
参数,而是需要使用 SourceUnit
@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”的 return 语句,这正是您可以在 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
,当应用于给定 String 字段时,它将添加一个返回该字段 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
不同的是,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 触发的,但是将编译文件的进程没有调试选项。只有当测试用例执行时,调试选项才会在虚拟机上设置。简而言之:为时已晚,类已经编译,您的转换也已经应用。
一个非常简单的解决方法是使用 GroovyTestCase
类,它提供了一个 assertScript
方法。这意味着,您不必在测试用例中这样写
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转换。但是,如果我们能有一种简单的方法来检查,例如,转换添加的节点是否与我们最初预期的相同,那将是很棒的。
以下转换将一个新方法 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转换的分步教程感兴趣,可以关注 这个工作坊。