运行时和编译时元编程
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 对象,它实现了 groovy.lang.GroovyInterceptable 接口,并具有方法拦截功能,这将在 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
定义,它接收一个额外的值参数
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. 静态 methodMissing
可以通过 ExpandoMetaClass 添加 methodMissing
方法的静态变体,也可以在类级别使用 $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. 静态 propertyMissing
可以通过 ExpandoMetaClass 添加 propertyMissing
方法的静态变体,也可以在类级别使用 $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 运行时的 method dispatcher 机制进行拦截。
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()
。这种方法适用于 POJO 和 POGO,如以下示例所示
class InterceptionThroughMetaClassTest extends GroovyTestCase {
void testPOJOMetaClassInterception() {
String invoking = 'ha'
invoking.metaClass.invokeMethod = { String name, Object args ->
'invoked'
}
assert invoking.length() == 'invoked'
assert invoking.someMethod() == 'invoked'
}
void testPOGOMetaClassInterception() {
Entity entity = new Entity('Hello')
entity.metaClass.invokeMethod = { String name, Object args ->
'invoked'
}
assert entity.build(new Object()) == 'invoked'
assert entity.someMethod() == 'invoked'
}
}
有关 MetaClass 的更多信息,请参见 MetaClasses 部分。 |
1.8. 类别
在某些情况下,如果一个不在控制范围内的类具有额外的
类别是使用所谓的*类别类*来实现的。类别类很特殊,因为它需要满足某些预定义的规则才能定义扩展方法。
系统中包含了一些类别,用于向类添加功能,使它们在 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. 元类
如前所述,元类在方法解析中起着核心作用。对于来自 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
。它很特殊,因为它允许使用简洁的闭包语法动态添加或更改方法、构造函数、属性甚至静态方法。
在模拟或存根场景中应用这些修改特别有用,如 测试指南 中所示。
每个 java.lang.Class
都由 Groovy 提供一个特殊的 metaClass
属性,该属性将为您提供一个指向 ExpandoMetaClass
实例的引用。然后可以使用此实例添加方法或更改已存在方法的行为。
默认情况下,ExpandoMetaClass 不执行继承。要启用此功能,您必须在应用程序启动之前(例如在 main 方法或 servlet 引导程序中)调用 ExpandoMetaClass#enableGlobally() 。 |
以下部分详细介绍了如何在各种场景中使用 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()
Closure
代码的第一步是查找给定名称和参数的 MetaMethod
。如果找到该方法,一切正常,将委托给它。如果没有找到,将返回一个虚拟值。
MetaMethod 是已知存在于 MetaClass 上的方法,无论是在运行时还是在编译时添加。 |
相同的逻辑可用于覆盖 setProperty
或 getProperty
。
class Person {
String name = "Fred"
}
Person.metaClass.getProperty = { String name ->
def metaProperty = Person.metaClass.getMetaProperty(name)
def result
if(metaProperty) result = metaProperty.getProperty(delegate)
else {
result = "Flintstone"
}
result
}
def p = new Person()
assert "Fred" == p.name
assert "Flintstone" == p.other
这里需要注意的重要一点是,不是查找 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')
getText
方法在 File
类中不存在。但是,Groovy 知道它,因为它是在一个特殊的类 ResourceGroovyMethods
中定义的
public static String getText(File file, String charset) throws IOException {
return IOGroovyMethods.getText(newReader(file, charset));
}
您可能会注意到,扩展方法是在辅助类(其中定义了各种扩展方法)中使用静态方法定义的。getText
方法的第一个参数对应于接收方,而其他参数对应于扩展方法的参数。因此,在这里,我们正在 File
类上定义一个名为 getText 的方法(因为第一个参数是 File
类型),该方法接受一个参数作为参数(编码 String
)。
创建扩展模块的过程很简单
-
编写如上所示的扩展类
-
编写模块描述符文件
然后,您需要让 Groovy 能够看到扩展模块,这与将扩展模块类和描述符放在类路径上一样简单。这意味着您可以选择
-
直接在类路径上提供类和模块描述符
-
或者将扩展模块捆绑到 jar 文件中以实现可重用性
扩展模块可以向类添加两种方法
-
实例方法(在类的实例上调用)
-
静态方法(在类本身中调用)
1.10.2. 实例方法
要向现有类添加实例方法,您需要创建一个扩展类。例如,假设您想在 Integer
上添加一个 maxRetries
方法,该方法接受一个闭包,并在没有抛出异常的情况下最多执行 n 次。为此,您只需要编写以下代码
class MaxRetriesExtension { (1)
static void maxRetries(Integer self, Closure code) { (2)
assert self >= 0
int retries = self
Throwable e = null
while (retries > 0) {
try {
code.call()
break
} catch (Throwable err) {
e = err
retries--
}
}
if (retries == 0 && e) {
throw e
}
}
}
1 | 扩展类 |
2 | 静态方法的第一个参数对应于消息的接收方,即扩展的实例 |
然后,在声明了扩展类之后,您可以按照以下方式调用它
int i=0
5.maxRetries {
i++
}
assert i == 1
i=0
try {
5.maxRetries {
i++
throw new RuntimeException("oops")
}
} catch (RuntimeException e) {
assert i == 5
}
1.10.3. 静态方法
也可以向类添加静态方法。在这种情况下,需要在**单独**的文件中定义静态方法。静态扩展方法和实例扩展方法**不能**存在于同一个类中。
class StaticStringExtension { (1)
static String greeting(String self) { (2)
'Hello, world!'
}
}
1 | 静态扩展类 |
2 | 静态方法的第一个参数对应于要扩展的类,并且**未使用** |
在这种情况下,您可以直接在 String
类上调用它
assert String.greeting() == 'Hello, world!'
1.10.4. 模块描述符
为了让 Groovy 能够加载扩展方法,您必须声明扩展辅助类。您必须在 META-INF/groovy
目录中创建一个名为 org.codehaus.groovy.runtime.ExtensionModule
的文件
moduleName=Test module for specifications moduleVersion=1.0-test extensionClasses=support.MaxRetriesExtension staticExtensionClasses=support.StaticStringExtension
模块描述符需要 4 个键
-
moduleName:模块的名称
-
moduleVersion:模块的版本。请注意,版本号仅用于检查是否在两个不同版本中加载了同一个模块。
-
extensionClasses:实例方法的扩展辅助类列表。您可以提供多个类,只要它们用逗号分隔即可。
-
staticExtensionClasses:静态方法的扩展辅助类列表。您可以提供多个类,只要它们用逗号分隔即可。
请注意,模块不需要同时定义静态辅助程序和实例辅助程序,并且您可以在单个模块中添加多个类。您还可以在单个模块中扩展不同的类,没有任何问题。甚至可以在单个扩展类中使用不同的类,但建议按功能集将扩展方法分组到类中。
1.10.5. 扩展模块和类路径
值得注意的是,您无法使用与使用它的代码同时编译的扩展。这意味着要使用扩展,它**必须**作为编译后的类在类路径上可用,然后才能编译使用它的代码。通常,这意味着您不能将测试类与扩展类本身放在同一个源代码单元中。由于通常将测试源代码与普通源代码分开,并在构建过程中的另一个步骤中执行,因此这不是问题。
1.10.6. 与类型检查的兼容性
与类别不同,扩展模块与类型检查兼容:如果在类路径上找到它们,那么类型检查器会知道扩展方法,并且在您调用它们时不会发出警告。它还与静态编译兼容。
2. 编译时元编程
Groovy 中的编译时元编程允许在编译时生成代码。这些转换会改变程序的抽象语法树 (AST),这就是为什么在 Groovy 中我们称之为 AST 转换的原因。AST 转换允许您挂接到编译过程,修改 AST 并继续编译过程以生成常规的字节码。与运行时元编程相比,它的优点是使这些更改在类文件本身(即字节码)中可见。在字节码中使其可见非常重要,例如,如果需要将转换作为类契约的一部分(实现接口、扩展抽象类等),或者如果您需要从 Java(或其他 JVM 语言)调用您的类。例如,AST 转换可以向类添加方法。如果使用运行时元编程,则只有从 Groovy 才能看到新方法。如果使用编译时元编程执行相同的操作,则该方法也可以从 Java 中看到。最后但并非最不重要的是,编译时元编程的性能可能更高(因为不需要初始化阶段)。
在本节中,我们将首先解释与 Groovy 发行版捆绑在一起的各种编译时转换。在下一节中,我们将描述如何实现您自己的 AST 转换以及此技术的缺点。
2.1. 可用 AST 转换
Groovy 附带了各种 AST 转换,涵盖了不同的需求:减少样板代码(代码生成)、实现设计模式(委托等)、日志记录、声明式并发、克隆、更安全的脚本编写、调整编译、实现 Swing 模式、测试,以及最终管理依赖项。如果这些 AST 转换都没有满足您的需求,您仍然可以实现您自己的转换,如开发您自己的 AST 转换一节中所示。
AST 转换可以分为两类
-
全局 AST 转换会在找到它们时透明地、全局地应用于编译类路径
-
局部 AST 转换是通过用标记注释源代码来应用的。与全局 AST 转换不同,局部 AST 转换可能支持参数。
Groovy 没有附带任何全局 AST 转换,但是您可以在此处找到可用于代码中的局部 AST 转换列表
2.1.1. 代码生成转换
此类转换包括有助于消除样板代码的 AST 转换。这通常是您必须编写的代码,但它不包含任何有用的信息。通过自动生成这些样板代码,您需要编写的代码将变得简洁干净,并且由于样板代码不正确而导致引入错误的机会也会降低。
@groovy.transform.ToString
@ToString
AST 转换会生成类的可读 toString
表示形式。例如,像下面这样注释 Person
类将自动为您生成 toString
方法
import groovy.transform.ToString
@ToString
class Person {
String firstName
String lastName
}
使用此定义,则以下断言通过,这意味着已生成了一个从类中获取字段值并打印它们的 toString
方法
def p = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p.toString() == 'Person(Jack, Nicholson)'
@ToString
注释接受几个参数,这些参数在以下表格中总结
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
excludes |
空列表 |
要从 toString 中排除的属性列表 |
|
includes |
未定义的标记列表(表示所有字段) |
要包含在 toString 中的字段列表 |
|
includeSuper |
False |
是否应在 toString 中包含超类 |
|
includeNames |
false |
是否在生成的 toString 中包含属性的名称。 |
|
includeFields |
False |
是否应在 toString 中包含字段,以及属性 |
|
includeSuperProperties |
False |
是否应在 toString 中包含超属性 |
|
includeSuperFields |
False |
是否应在 toString 中包含可见的超字段 |
|
ignoreNulls |
False |
是否应显示具有空值的属性/字段 |
|
includePackage |
True |
在 toString 中使用完全限定的类名,而不是简单名称 |
|
allProperties |
True |
在 toString 中包含所有 JavaBean 属性 |
|
cache |
False |
缓存 toString 字符串。只有当类是不可变时才应将其设置为 true。 |
|
allNames |
False |
是否应在生成的 toString 中包含具有内部名称的字段和/或属性 |
|
@groovy.transform.EqualsAndHashCode
@EqualsAndHashCode
AST 转换旨在为您生成 equals
和 hashCode
方法。生成的哈希码遵循Josh Bloch 的Effective Java 中描述的最佳实践
import groovy.transform.EqualsAndHashCode
@EqualsAndHashCode
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
def p2 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1==p2
assert p1.hashCode() == p2.hashCode()
有几个选项可用于调整 @EqualsAndHashCode
的行为
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
excludes |
空列表 |
要从 equals/hashCode 中排除的属性列表 |
|
includes |
未定义的标记列表(表示所有字段) |
要包含在 equals/hashCode 中的字段列表 |
|
cache |
False |
缓存 hashCode 计算。只有当类是不可变时才应将其设置为 true。 |
|
callSuper |
False |
是否应在 equals 和 hashCode 计算中包含 super |
|
includeFields |
False |
除了属性之外,字段是否应该包含在equals/hashCode中 |
|
useCanEqual |
True |
equals是否应该调用canEqual帮助方法。 |
|
allProperties |
False |
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')
第一个构造函数是一个无参数构造函数,只要您没有最终属性,它就可以允许传统的 map 样式构造。Groovy 在幕后调用无参数构造函数,然后调用相关的 setter。值得注意的是,如果第一个属性(或字段)的类型是 LinkedHashMap,或者存在单个 Map、AbstractMap 或 HashMap 属性(或字段),那么将无法使用 map 样式命名参数。
其他构造函数是通过按定义顺序获取属性生成的。Groovy 将生成与属性(或字段,取决于选项)数量相同的构造函数。
将defaults
属性(参见可用的配置选项表)设置为false
,将禁用正常的默认值行为,这意味着
-
将生成一个构造函数
-
尝试使用初始值将产生错误
-
将无法使用 map 样式命名参数
此属性通常仅在另一个 Java 框架期望只有一个构造函数的情况下使用,例如注入框架或 JUnit 参数化运行程序。
不可变性支持
如果@PropertyOptions
注解也出现在具有@TupleConstructor
注解的类上,那么生成的构造函数可能包含自定义属性处理逻辑。例如,@PropertyOptions
注解上的propertyHandler
属性可以设置为ImmutablePropertyHandler
,这将导致添加不可变类所需的逻辑(防御性复制、克隆等)。这通常会在使用@Immutable
元注解时自动发生在幕后。某些注解属性可能不受所有属性处理程序的支持。
自定义选项
@TupleConstructor
AST 转换接受几个注解属性
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
excludes |
空列表 |
要从元组构造函数生成中排除的属性列表 |
|
includes |
未定义的列表(表示所有字段) |
要包含在元组构造函数生成中的字段列表 |
|
includeProperties |
True |
属性是否应该包含在元组构造函数生成中 |
|
includeFields |
False |
除了属性之外,字段是否应该包含在元组构造函数生成中 |
|
includeSuperProperties |
True |
父类的属性是否应该包含在元组构造函数生成中 |
|
includeSuperFields |
False |
父类的字段是否应该包含在元组构造函数生成中 |
|
callSuper |
False |
超级属性是否应该在调用父构造函数时调用,而不是设置为属性 |
|
force |
False |
默认情况下,如果已定义构造函数,转换将不做任何操作。将此属性设置为 true,将生成构造函数,您有责任确保没有定义重复的构造函数。 |
|
defaults |
True |
表示构造函数参数启用了默认值处理。设置为 false 以获得一个构造函数,但禁用初始值支持和命名参数。 |
|
useSetters |
False |
默认情况下,转换将直接从其相应的构造函数参数设置每个属性的支持字段。将此属性设置为 true,构造函数将改为调用 setter(如果存在)。通常,在构造函数中调用可以被覆盖的 setter 被认为是不好的风格。您有责任避免这种不好的风格。 |
|
allNames |
False |
内部名称的字段和/或属性是否应该包含在构造函数中 |
|
allProperties |
False |
JavaBean 属性是否应该包含在构造函数中 |
|
pre |
empty |
包含要插入到生成的构造函数开头语句的闭包 |
|
post |
empty |
包含要插入到生成的构造函数末尾语句的闭包 |
|
将defaults
注解属性设置为false
并将force
注解属性设置为true
允许通过对不同情况使用不同的自定义选项来创建多个元组构造函数(前提是每个情况都有不同的类型签名),如以下示例所示
class Named {
String name
}
@ToString(includeSuperProperties=true, ignoreNulls=true, includeNames=true, includeFields=true)
@TupleConstructor(force=true, defaults=false)
@TupleConstructor(force=true, defaults=false, includeFields=true)
@TupleConstructor(force=true, defaults=false, includeSuperProperties=true)
class Book extends Named {
Integer published
private Boolean fiction
Book() {}
}
assert new Book("Regina", 2015).toString() == 'Book(published:2015, name:Regina)'
assert new Book(2015, false).toString() == 'Book(published:2015, fiction:false)'
assert new Book(2015).toString() == 'Book(published:2015)'
assert new Book().toString() == 'Book()'
assert Book.constructors.size() == 4
类似地,以下是一个使用includes
的不同选项的另一个示例
@ToString(includeSuperProperties=true, ignoreNulls=true, includeNames=true, includeFields=true)
@TupleConstructor(force=true, defaults=false, includes='name,year')
@TupleConstructor(force=true, defaults=false, includes='year,fiction')
@TupleConstructor(force=true, defaults=false, includes='name,fiction')
class Book {
String name
Integer year
Boolean fiction
}
assert new Book("Regina", 2015).toString() == 'Book(name:Regina, year:2015)'
assert new Book(2015, false).toString() == 'Book(year:2015, fiction:false)'
assert new Book("Regina", false).toString() == 'Book(name:Regina, fiction:false)'
assert Book.constructors.size() == 3
@groovy.transform.MapConstructor
@MapConstructor
注解旨在通过为您生成地图构造函数来消除样板代码。创建的地图构造函数使得类中的每个属性都根据提供的映射中具有属性名称的键的值进行设置。用法如以下示例所示
import groovy.transform.*
@ToString
@MapConstructor
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1.toString() == 'Person(Jack, Nicholson)'
生成的构造函数将大致如下
public Person(Map args) {
if (args.containsKey('firstName')) {
this.firstName = args.get('firstName')
}
if (args.containsKey('lastName')) {
this.lastName = args.get('lastName')
}
}
@groovy.transform.Canonical
@Canonical
元注解组合了 @ToString、@EqualsAndHashCode 和 @TupleConstructor 注解
import groovy.transform.Canonical
@Canonical
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1.toString() == 'Person(Jack, Nicholson)' // Effect of @ToString
def p2 = new Person('Jack','Nicholson') // Effect of @TupleConstructor
assert p2.toString() == 'Person(Jack, Nicholson)'
assert p1==p2 // Effect of @EqualsAndHashCode
assert p1.hashCode()==p2.hashCode() // Effect of @EqualsAndHashCode
可以使用 @Immutable 元注解来生成类似的不可变类。@Canonical
元注解支持它聚合的注解中的配置选项。有关详细信息,请参见这些注解。
import groovy.transform.Canonical
@Canonical(excludes=['lastName'])
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1.toString() == 'Person(Jack)' // Effect of @ToString(excludes=['lastName'])
def p2 = new Person('Jack') // Effect of @TupleConstructor(excludes=['lastName'])
assert p2.toString() == 'Person(Jack)'
assert p1==p2 // Effect of @EqualsAndHashCode(excludes=['lastName'])
assert p1.hashCode()==p2.hashCode() // Effect of @EqualsAndHashCode(excludes=['lastName'])
@Canonical
元注解可以与显式使用一个或多个其组件注解一起使用,如下所示
import groovy.transform.Canonical
@Canonical(excludes=['lastName'])
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1.toString() == 'Person(Jack)' // Effect of @ToString(excludes=['lastName'])
def p2 = new Person('Jack') // Effect of @TupleConstructor(excludes=['lastName'])
assert p2.toString() == 'Person(Jack)'
assert p1==p2 // Effect of @EqualsAndHashCode(excludes=['lastName'])
assert p1.hashCode()==p2.hashCode() // Effect of @EqualsAndHashCode(excludes=['lastName'])
来自@Canonical
的任何适用注解属性都将传递到显式注解,但显式注解中已存在的属性优先。
@groovy.transform.InheritConstructors
@InheritConstructor
AST 转换旨在为您生成与超类构造函数匹配的构造函数。这在覆盖异常类时特别有用
import groovy.transform.InheritConstructors
@InheritConstructors
class CustomException extends Exception {}
// all those are generated constructors
new CustomException()
new CustomException("A custom message")
new CustomException("A custom message", new RuntimeException())
new CustomException(new RuntimeException())
// Java 7 only
// new CustomException("A custom message", new RuntimeException(), false, true)
@InheritConstructor
AST 转换支持以下配置选项
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
constructorAnnotations |
False |
在复制过程中是否将注解从构造函数中转存 |
|
parameterAnnotations |
False |
在复制构造函数时,是否将注解从构造函数参数中转存 |
|
@groovy.lang.Category
@Category
AST 转换简化了 Groovy 类别的创建。从历史上看,Groovy 类别是这样编写的
class TripleCategory {
public static Integer triple(Integer self) {
3*self
}
}
use (TripleCategory) {
assert 9 == 3.triple()
}
@Category
转换让您可以使用实例样式类来编写相同的内容,而不是静态类样式。这消除了对每个方法的第一个参数都是接收器的需求。类别可以这样编写
@Category(Integer)
class TripleCategory {
public Integer triple() { 3*this }
}
use (TripleCategory) {
assert 9 == 3.triple()
}
请注意,混合的类可以使用this
来引用。还值得注意的是,在类别类中使用实例字段本质上是不安全的:类别不是有状态的(如特征)。
@groovy.transform.IndexedProperty
@IndexedProperty
注解旨在为列表/数组类型属性生成索引 getter/setter。如果您想从 Java 使用 Groovy 类,这特别有用。虽然 Groovy 支持 GPath 来访问属性,但这在 Java 中不可用。@IndexedProperty
注解将生成以下形式的索引属性
class SomeBean {
@IndexedProperty String[] someArray = new String[2]
@IndexedProperty List someList = []
}
def bean = new SomeBean()
bean.setSomeArray(0, 'value')
bean.setSomeList(0, 123)
assert bean.someArray[0] == 'value'
assert bean.someList == [123]
@groovy.lang.Lazy
@Lazy
AST 转换实现了字段的延迟初始化。例如,以下代码
class SomeBean {
@Lazy LinkedList myField
}
将生成以下代码
List $myField
List getMyField() {
if ($myField!=null) { return $myField }
else {
$myField = new LinkedList()
return $myField
}
}
用于初始化字段的默认值是声明类型的默认构造函数。可以通过在属性赋值的右侧使用闭包来定义默认值,如以下示例所示
class SomeBean { @Lazy LinkedList myField = { ['a','b','c']}() }
在这种情况下,生成的代码如下所示
List $myField List getMyField() { if ($myField!=null) { return $myField } else { $myField = { ['a','b','c']}() return $myField } }
如果字段被声明为 volatile,那么初始化将使用双重检查锁定 模式进行同步。
使用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 |
prefix |
includes/excludes |
includeSuperProperties |
allNames |
|
链式 setter |
n/a |
n/a |
n/a |
是,默认值为“set” |
是 |
n/a |
是,默认值为 |
|
显式构建器类,构建的类保持不变 |
n/a |
n/a |
是,默认值为“build” |
是,默认值为“” |
是 |
是,默认值为 |
是,默认值为 |
|
创建嵌套的辅助类 |
是,默认值为 <TypeName>Builder |
是,默认值为“builder” |
是,默认值为“build” |
是,默认值为“” |
是 |
是,默认值为 |
是,默认值为 |
|
创建嵌套的辅助类,提供类型安全的流畅创建 |
是,默认值为 <TypeName>Initializer |
是,默认值为“createInitializer” |
是,默认值为“create”,但通常仅在内部使用 |
是,默认值为“” |
是 |
是,默认值为 |
是,默认值为 |
要使用 SimpleStrategy
,请使用 @Builder
注释您的 Groovy 类,并指定策略,如本例所示
import groovy.transform.builder.*
@Builder(builderStrategy=SimpleStrategy)
class Person {
String first
String last
Integer born
}
然后,只需按链式方式调用 setter,如下所示
def p1 = new Person().setFirst('Johnny').setLast('Depp').setBorn(1963)
assert "$p1.first $p1.last" == 'Johnny Depp'
对于每个属性,都会创建一个生成的 setter,如下所示
public Person setFirst(java.lang.String first) {
this.first = first
return this
}
您可以在本例中指定前缀,如下所示
import groovy.transform.builder.*
@Builder(builderStrategy=SimpleStrategy, prefix="")
class Person {
String first
String last
Integer born
}
调用链式 setter 的方式如下所示
def p = new Person().first('Johnny').last('Depp').born(1963)
assert "$p.first $p.last" == 'Johnny Depp'
您可以将 SimpleStrategy
与 @TupleConstructor
结合使用。如果您的 @Builder
注释没有显式的 includes
或 excludes
注释属性,但您的 @TupleConstructor
注释有,则来自 @TupleConstructor
的属性将被重新用于 @Builder
。对于结合 @TupleConstructor
的任何注释别名(例如 @Canonical
)也是如此。
如果有一个您希望在构建过程中调用的 setter,则可以使用注释属性 useSetters
。有关详细信息,请参阅 JavaDoc。
注释属性 builderClassName
、buildMethodName
、builderMethodName
、forClass
和 includeSuperProperties
不支持此策略。
Groovy 已经内置了构建机制。如果内置机制满足您的需求,请不要急于使用 @Builder 。以下是一些示例 |
def p2 = new Person(first: 'Keira', last: 'Knightley', born: 1985)
def p3 = new Person().with {
first = 'Geoffrey'
last = 'Rush'
born = 1951
}
要使用 ExternalStrategy
,请创建并使用 @Builder
注释您的 Groovy 构建器类,使用 forClass
指定构建器所针对的类,并指示使用 ExternalStrategy
。假设您有以下类,您希望为它创建一个构建器
class Person {
String first
String last
int born
}
您可以显式创建并使用您的构建器类,如下所示
import groovy.transform.builder.*
@Builder(builderStrategy=ExternalStrategy, forClass=Person)
class PersonBuilder { }
def p = new PersonBuilder().first('Johnny').last('Depp').born(1963).build()
assert "$p.first $p.last" == 'Johnny Depp'
请注意,您提供的(通常为空的)构建器类将填充适当的 setter 和构建方法。生成的构建方法将类似于
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 方法和具有返回类型的 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
注释,实现了一个接口,并且具有注释属性,以指示应为提供的任何方法抛出 UnsupportedOperationException
,并带有消息 Not supported by MyIterator
。以下是类定义
@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 变换为构造函数和方法添加了空检查保护语句,这些语句会导致这些方法在提供 null 参数时尽早失败。可以将其视为防御性编程的一种形式。注释可以添加到单个方法或构造函数,或者添加到类,在这种情况下它将应用于所有方法/构造函数。
@NullCheck
String longerOf(String first, String second) {
first.size() >= second.size() ? first : second
}
assert longerOf('cat', 'canary') == 'canary'
def ex = shouldFail(IllegalArgumentException) {
longerOf('cat', null)
}
assert ex.message == 'second cannot be null'
2.1.2. 类设计注释
此类注释旨在通过使用声明性风格来简化众所周知的模式(委托、单例等)的实现。
@groovy.transform.BaseScript
@BaseScript
在脚本中使用,以指示脚本应扩展自自定义脚本基类,而不是 groovy.lang.Script
。有关更多详细信息,请参阅 特定领域语言 的文档。
@groovy.lang.Delegate
@Delegate
AST 变换旨在实现委托设计模式。在以下类中
class Event {
@Delegate Date when
String title
}
when
属性使用 @Delegate
注释,这意味着 Event
类将委托对 Date
方法的调用到 when
属性。在这种情况下,生成的代码如下所示
class Event {
Date when
String title
boolean before(Date other) {
when.before(other)
}
// ...
}
然后,您可以直接在 Event
类上调用 before
方法,例如
def ev = new Event(title:'Groovy keynote', when: Date.parse('yyyy/MM/dd', '2013/09/10'))
def now = new Date()
assert ev.before(now)
除了对属性(或字段)进行注解外,您也可以对方法进行注解。在这种情况下,该方法可以被认为是委托的 getter 或工厂方法。例如,以下是一个类,它(非常不寻常地)有一个委托池,这些委托以循环方式访问
class Test {
private int robinCount = 0
private List<List> items = [[0], [1], [2]]
@Delegate
List getRoundRobinList() {
items[robinCount++ % items.size()]
}
void checkItems(List<List> testValue) {
assert items == testValue
}
}
以下是该类的示例用法
def t = new Test()
t << 'fee'
t << 'fi'
t << 'fo'
t << 'fum'
t.checkItems([[0, 'fee', 'fum'], [1, 'fi'], [2, 'fo']])
以这种循环方式使用标准列表会违反列表的许多预期属性,因此不要期望上述类在此琐碎示例之外做任何有用的事情。
@Delegate
AST 变换的行为可以使用以下参数进行更改
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
接口 |
True |
字段实现的接口是否也应该由类实现 |
|
已弃用 |
false |
如果为 true,则还将委托使用 @Deprecated 注解的方法 |
|
方法注解 |
False |
是否将委托方法的注解传递到您的委托方法。 |
|
parameterAnnotations |
False |
是否将委托方法参数的注解传递到您的委托方法。 |
|
excludes |
空数组 |
要从委托中排除的方法列表。有关更细粒度的控制,请参见 |
|
includes |
未定义的标记数组(表示所有方法) |
要包含在委托中的方法列表。有关更细粒度的控制,请参见 |
|
排除类型 |
空数组 |
包含要从委托中排除的方法签名的接口列表 |
|
包含类型 |
未定义的标记数组(表示默认情况下没有列表) |
包含要包含在委托中的方法签名的接口列表 |
|
allNames |
False |
委托模式是否也应用于具有内部名称的方法 |
|
@groovy.transform.Immutable
@Immutable
元注解结合了以下注解
@Immutable
元注解简化了不可变类的创建。不可变类很有用,因为它们通常更容易推理,并且本质上是线程安全的。参见 Effective Java,最小化可变性,了解有关如何在 Java 中实现不可变类的所有详细信息。@Immutable
元注解自动为您完成了《Effective Java》中描述的大部分内容。要使用元注解,您只需像以下示例一样对类进行注解
import groovy.transform.Immutable
@Immutable
class Point {
int x
int y
}
不可变类的一个要求是,没有办法修改类中的任何状态信息。实现这一点的一个要求是为每个属性使用不可变类,或者为构造函数和属性 getter 中的任何可变属性执行特殊的编码,例如防御性复制输入和防御性复制输出。在 @ImmutableBase
、@MapConstructor
和 @TupleConstructor
之间,属性要么被标识为不可变的,要么为许多已知情况自动处理特殊的编码。提供了各种机制供您扩展允许的已处理属性类型。有关详细信息,请参见 @ImmutableOptions
和 @KnownImmutable
。
将 @Immutable
应用于类的结果与将 @Canonical 元注解应用于类的结果非常相似,但生成的类将具有额外的逻辑来处理不可变性。例如,您会观察到这一点,当您尝试修改一个属性时,将会抛出 ReadOnlyPropertyException
,因为该属性的底层字段将被自动设为 final。
@Immutable
元注解支持在其聚合的注解中找到的配置选项。有关详细信息,请参见这些注解。
@groovy.transform.ImmutableBase
使用 @ImmutableBase
生成的不可变类会自动设置为 final。此外,会检查每个属性的类型,并对类进行各种检查,例如,目前不允许使用公共实例字段。它还会根据需要生成一个 copyWith
构造函数。
支持以下注解属性
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
copyWith |
false |
一个布尔值,表示是否生成一个 |
|
@groovy.transform.PropertyOptions
此注解允许您指定一个自定义属性处理程序,在类构造期间由转换使用。它被 Groovy 主编译器忽略,但被其他转换(如 @TupleConstructor
、@MapConstructor
和 @ImmutableBase
)引用。它经常被 @Immutable
元注解在幕后使用。
@groovy.transform.VisibilityOptions
此注解允许您为另一个转换生成的构造指定一个自定义可见性。它被 Groovy 主编译器忽略,但被其他转换(如 @TupleConstructor
、@MapConstructor
和 @NamedVariant
)引用。
@groovy.transform.ImmutableOptions
Groovy 的不可变性支持依赖于一个预定义的已知不可变类列表(如 java.net.URI
或 java.lang.String
),如果使用该列表中没有的类型,则会失败,您可以使用 @ImmutableOptions
注解的以下注解属性将已知不可变类型添加到该列表中
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
knownImmutableClasses |
空列表 |
被认为是不可变的类列表。 |
|
knownImmutables |
空列表 |
被认为是不可变的属性名称列表。 |
|
如果您认为一个类型是不可变的,但它不是自动处理的类型之一,那么您有责任正确编码该类以确保不可变性。
@groovy.transform.KnownImmutable
@KnownImmutable
注解实际上不是一个触发任何 AST 变换的注解。它只是一个标记注解。您可以使用该注解对您的类进行注解(包括 Java 类),它们将被识别为不可变类中的成员的有效类型。这样可以避免您显式使用 @ImmutableOptions
中的 knownImmutables
或 knownImmutableClasses
注解属性。
@groovy.transform.Memoized
@Memoized
AST 变换简化了缓存的实现,通过使用 @Memoized
对方法进行注解,就可以允许缓存方法调用的结果。让我们想象以下方法
long longComputation(int seed) {
// slow computation
Thread.sleep(100*seed)
System.nanoTime()
}
这模拟了基于方法实际参数的长时间计算。如果没有 @Memoized
,每次方法调用都需要几秒钟,并且它会返回一个随机结果
def x = longComputation(1)
def y = longComputation(1)
assert x!=y
添加 @Memoized
通过添加基于参数的缓存来改变方法的语义
@Memoized
long longComputation(int seed) {
// slow computation
Thread.sleep(100*seed)
System.nanoTime()
}
def x = longComputation(1) // returns after 100 milliseconds
def y = longComputation(1) // returns immediately
def z = longComputation(2) // returns after 200 milliseconds
assert x==y
assert x!=z
可以使用两个可选参数配置缓存的大小
-
protectedCacheSize:保证在垃圾回收后不会被清除的结果数量
-
maxCacheSize:可以保存在内存中的最大结果数量
默认情况下,缓存的大小是无限的,并且没有缓存结果受到垃圾回收的保护。设置 protectedCacheSize>0 将创建一个无限的缓存,其中一些结果受到保护。设置 maxCacheSize>0 将创建一个有限的缓存,但没有任何保护免受垃圾保护。同时设置两者将创建一个有限的、受保护的缓存。
@groovy.transform.TailRecursive
@TailRecursive
注解可用于将方法末尾的递归调用自动转换为相同代码的等效迭代版本。这避免了由于太多递归调用导致的堆栈溢出。以下是计算阶乘时使用该注解的示例
import groovy.transform.CompileStatic
import groovy.transform.TailRecursive
@CompileStatic
class Factorial {
@TailRecursive
static BigInteger factorial( BigInteger i, BigInteger product = 1) {
if( i == 1) {
return product
}
return factorial(i-1, product*i)
}
}
assert Factorial.factorial(1) == 1
assert Factorial.factorial(3) == 6
assert Factorial.factorial(5) == 120
assert Factorial.factorial(50000).toString().size() == 213237 // Big number and no Stack Overflow
目前,该注解仅适用于自递归方法调用,即对完全相同方法的单个递归调用。如果您遇到简单的相互递归场景,请考虑使用闭包和 trampoline()
。还要注意,目前仅处理非 void 方法(void 调用将导致编译错误)。
目前,某些形式的方法重载可能会欺骗编译器,并且一些非尾递归调用被错误地视为尾递归调用。 |
@groovy.lang.Singleton
@Singleton
注解可用于在类上实现单例设计模式。单例实例默认情况下使用类初始化进行提前定义,或者延迟定义,在这种情况下,字段使用双重检查锁定进行初始化。
@Singleton
class GreetingService {
String greeting(String name) { "Hello, $name!" }
}
assert GreetingService.instance.greeting('Bob') == 'Hello, Bob!'
默认情况下,单例在类初始化时被提前创建,并通过 instance
属性提供。可以使用 property
参数更改单例的名称
@Singleton(property='theOne')
class GreetingService {
String greeting(String name) { "Hello, $name!" }
}
assert GreetingService.theOne.greeting('Bob') == 'Hello, Bob!'
还可以使用 lazy
参数使初始化延迟
class Collaborator {
public static boolean init = false
}
@Singleton(lazy=true,strict=false)
class GreetingService {
static void init() {}
GreetingService() {
Collaborator.init = true
}
String greeting(String name) { "Hello, $name!" }
}
GreetingService.init() // make sure class is initialized
assert Collaborator.init == false
GreetingService.instance
assert Collaborator.init == true
assert GreetingService.instance.greeting('Bob') == 'Hello, Bob!'
在此示例中,我们还将 strict
参数设置为 false,这允许我们定义自己的构造函数。
@groovy.lang.Mixin
已弃用。考虑使用特征代替。
2.1.3. 日志记录改进
Groovy 提供了一系列 AST 变换,这些变换有助于与最广泛使用的日志记录框架集成。每个常见的框架都有一个转换和相关的注解。这些转换提供了一种简化的声明式方法来使用日志记录框架。在每种情况下,转换将
-
在带注解的类中添加一个静态 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 添加到您的类路径中。
@groovy.util.logging.Log4j
Groovy 使用 @Log4j
注解支持 Apache Log4j 1.x 框架。编写
@groovy.util.logging.Log4j
class Greeter {
void greet() {
log.debug 'Called greeter'
println 'Hello, world!'
}
}
等效于编写
import org.apache.log4j.Logger
class Greeter {
private static final Logger log = Logger.getLogger(Greeter)
void greet() {
if (log.isDebugEnabled()) {
log.debug 'Called greeter'
}
println 'Hello, world!'
}
}
您仍然需要将适当的 log4j jar 添加到您的类路径中。此注解也可以与兼容的 reload4j log4j 替代品一起使用,只需使用该项目中的 jar 而不是 log4j jar。
@groovy.util.logging.Log4j2
Groovy 使用 @Log4j2
注解支持 Apache Log4j 2.x 框架。编写
@groovy.util.logging.Log4j2
class Greeter {
void greet() {
log.debug 'Called greeter'
println 'Hello, world!'
}
}
等效于编写
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
class Greeter {
private static final Logger log = LogManager.getLogger(Greeter)
void greet() {
if (log.isDebugEnabled()) {
log.debug 'Called greeter'
}
println 'Hello, world!'
}
}
您仍然需要将适当的 log4j2 jar 添加到您的类路径中。
@groovy.util.logging.Slf4j
Groovy 使用 @Slf4j
注解支持 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 添加到您的类路径中。
@groovy.util.logging.PlatformLog
Groovy 使用 @PlatformLog
注解支持 Java Platform Logging API and Service 框架。编写
@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
)的字段,但您可以通过指定 value 属性让它使用您想要的任何字段,如下例所示:
import groovy.transform.Synchronized
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
class Counter {
int cpt
private final Object myLock = new Object()
@Synchronized('myLock')
int incrementAndGet() {
cpt++
}
int get() {
cpt
}
}
@groovy.transform.WithReadLock
和 @groovy.transform.WithWriteLock
@WithReadLock
AST 变换与 @WithWriteLock
变换配合使用,以使用 JDK 提供的 ReentrantReadWriteLock
机制提供读/写同步。该注解可以添加到方法或静态方法中。它将透明地创建一个 $reentrantLock
最终字段(或静态方法的 $REENTRANTLOCK
),并将添加适当的同步代码。例如,以下代码:
import groovy.transform.WithReadLock
import groovy.transform.WithWriteLock
class Counters {
public final Map<String,Integer> map = [:].withDefault { 0 }
@WithReadLock
int get(String id) {
map.get(id)
}
@WithWriteLock
void add(String id, int num) {
Thread.sleep(200) // emulate long computation
map.put(id, map.get(id)+num)
}
}
等效于:
import groovy.transform.WithReadLock as WithReadLock
import groovy.transform.WithWriteLock as WithWriteLock
public class Counters {
private final Map<String, Integer> map
private final java.util.concurrent.locks.ReentrantReadWriteLock $reentrantlock
public int get(java.lang.String id) {
$reentrantlock.readLock().lock()
try {
map.get(id)
}
finally {
$reentrantlock.readLock().unlock()
}
}
public void add(java.lang.String id, int num) {
$reentrantlock.writeLock().lock()
try {
java.lang.Thread.sleep(200)
map.put(id, map.get(id) + num )
}
finally {
$reentrantlock.writeLock().unlock()
}
}
}
@WithReadLock
和 @WithWriteLock
都支持指定备用锁定对象。在这种情况下,用户必须声明引用的字段,如下面的替代方案所示:
import groovy.transform.WithReadLock
import groovy.transform.WithWriteLock
import java.util.concurrent.locks.ReentrantReadWriteLock
class Counters {
public final Map<String,Integer> map = [:].withDefault { 0 }
private final ReentrantReadWriteLock customLock = new ReentrantReadWriteLock()
@WithReadLock('customLock')
int get(String id) {
map.get(id)
}
@WithWriteLock('customLock')
void add(String id, int num) {
Thread.sleep(200) // emulate long computation
map.put(id, map.get(id)+num)
}
}
有关详细信息,请参阅:
-
查看 groovy.transform.WithReadLock 的 Javadoc 文档。
-
查看 groovy.transform.WithWriteLock 的 Javadoc 文档。
2.1.5. 更轻松的克隆和外部化
Groovy 提供了两个注解,旨在分别简化 Cloneable
和 Externalizable
接口的实现,它们分别名为 @AutoClone
和 @AutoExternalize
。
@groovy.transform.AutoClone
@AutoClone
注解旨在使用各种策略实现 @java.lang.Cloneable
接口,这得益于 style
参数。
-
默认的
AutoCloneStyle.CLONE
策略首先调用super.clone()
,然后对每个可克隆属性调用clone()
。 -
AutoCloneStyle.SIMPLE
策略使用常规的构造函数调用,并将属性从源复制到克隆。 -
AutoCloneStyle.COPY_CONSTRUCTOR
策略创建并使用一个复制构造函数。 -
AutoCloneStyle.SERIALIZATION
策略使用序列化(或外部化)来克隆对象。
这些策略中的每一种都有优缺点,这些优缺点在 groovy.transform.AutoClone 和 groovy.transform.AutoCloneStyle 的 Javadoc 文档中进行了讨论。
例如,以下示例:
import groovy.transform.AutoClone
@AutoClone
class Book {
String isbn
String title
List<String> authors
Date publicationDate
}
等效于:
class Book implements Cloneable {
String isbn
String title
List<String> authors
Date publicationDate
public Book clone() throws CloneNotSupportedException {
Book result = super.clone()
result.authors = authors instanceof Cloneable ? (List) authors.clone() : authors
result.publicationDate = publicationDate.clone()
result
}
}
请注意,String 属性没有被显式处理,因为 String 是不可变的,Object
的 clone()
方法将复制 String 引用。同样的事情也适用于原始字段和 java.lang.Number
的大多数具体子类。
除了克隆风格之外,@AutoClone
还支持多个选项:
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
excludes |
空列表 |
需要从克隆中排除的属性或字段名称列表。也可以使用由逗号分隔的字段/属性名称组成的字符串。有关详细信息,请参阅 groovy.transform.AutoClone#excludes。 |
|
includeFields |
false |
默认情况下,只克隆属性。将此标志设置为 true 将还会克隆字段。 |
|
@groovy.transform.AutoExternalize
@AutoExternalize
AST 变换将有助于创建 java.io.Externalizable
类。它将自动将接口添加到类中,并生成 writeExternal
和 readExternal
方法。例如,这段代码:
import groovy.transform.AutoExternalize
@AutoExternalize
class Book {
String isbn
String title
float price
}
将被转换为:
class Book implements java.io.Externalizable {
String isbn
String title
float price
void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(isbn)
out.writeObject(title)
out.writeFloat( price )
}
public void readExternal(ObjectInput oin) {
isbn = (String) oin.readObject()
title = (String) oin.readObject()
price = oin.readFloat()
}
}
@AutoExternalize
注解支持两个参数,使您可以稍微自定义其行为。
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
excludes |
空列表 |
需要从外部化中排除的属性或字段名称列表。也可以使用由逗号分隔的字段/属性名称组成的字符串。有关详细信息,请参阅 groovy.transform.AutoExternalize#excludes。 |
|
includeFields |
false |
默认情况下,只外部化属性。将此标志设置为 true 将还会克隆字段。 |
|
2.1.6. 更安全的脚本编写
Groovy 语言使在运行时执行用户脚本变得很容易(例如,使用 groovy.lang.GroovyShell),但是您如何确保脚本不会占用所有 CPU(无限循环),或者并发脚本不会慢慢消耗线程池中所有可用的线程呢?Groovy 提供了一些注解,旨在实现更安全的脚本编写,生成能够例如允许您自动中断执行的代码。
@groovy.transform.ThreadInterrupt
JVM 世界中一个复杂的情况是,线程无法停止。Thread#stop
方法存在,但已弃用(并且不可靠),因此您唯一的机会在于 Thread#interrupt
。调用后者将设置线程上的 interrupt
标志,但它不会停止线程的执行。这是有问题的,因为线程中执行的代码有责任检查中断标志并适当地退出。当您作为开发人员知道要执行的代码旨在在独立线程中运行时,这很有意义,但通常情况下您并不知道这一点。对于用户脚本来说,情况更糟,他们甚至可能不知道线程是什么(想想 DSL)。
@ThreadInterrupt
通过在代码的关键位置添加线程中断检查来简化此过程:
-
循环(for,while)
-
方法的第一个指令
-
闭包体中的第一个指令
让我们想象一下以下用户脚本:
while (true) {
i++
}
这是一个明显的无限循环。如果这段代码在其自己的线程中执行,中断将无济于事:如果您在该线程上调用 join
,那么调用代码将能够继续执行,但该线程仍将处于活动状态,在后台运行,您无法停止它,从而慢慢导致线程饥饿。
解决此问题的一种方法是,以这种方式设置您的 shell:
def config = new CompilerConfiguration()
config.addCompilationCustomizers(
new ASTTransformationCustomizer(ThreadInterrupt)
)
def binding = new Binding(i:0)
def shell = new GroovyShell(binding,config)
然后,shell 被配置为自动对所有脚本应用 @ThreadInterrupt
AST 变换。这使您能够以这种方式执行用户脚本:
def t = Thread.start {
shell.evaluate(userCode)
}
t.join(1000) // give at most 1000ms for the script to complete
if (t.alive) {
t.interrupt()
}
变换会自动修改用户代码,如下所示:
while (true) {
if (Thread.currentThread().interrupted) {
throw new InterruptedException('The current thread has been interrupted.')
}
i++
}
在循环中引入的检查保证,如果在当前线程上设置了 interrupt
标志,则将抛出一个异常,中断线程的执行。
@ThreadInterrupt
支持多个选项,使您可以进一步自定义变换的行为:
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
thrown |
|
指定如果线程被中断,则抛出的异常类型。 |
|
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
支持多个选项,使您可以进一步自定义变换的行为:
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
value |
Long.MAX_VALUE |
与 |
|
unit |
TimeUnit.SECONDS |
与 |
|
thrown |
|
指定如果达到超时时间,则抛出的异常类型。 |
|
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
支持多个选项,可以进一步定制转换的行为。
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
value |
将被调用的闭包,用于检查是否允许执行。如果闭包返回 false,则允许执行。如果它返回 true,则会抛出异常。 |
|
|
thrown |
|
指定如果应中止执行将抛出的异常类型。 |
|
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
,例如。
@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 会增加样板代码的数量,并使方法签名有些杂乱。另一种方法可能是使用代码审查流程或应用 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
方法的单个参数将是最终的。尝试在构造函数或方法主体中修改这些参数将被编译器标记。
以下示例说明了在方法级别应用注释
class Calc {
@AutoFinal
int add(int a, int b) { a + b }
int mult(int a, int b) { a * b }
}
这里,add
方法将具有最终参数,但 mult
方法将保持不变。
@groovy.transform.AnnotationCollector
@AnnotationCollector
允许创建元注释,这些注释在 专门部分 中进行了描述。
@groovy.transform.TypeChecked
@TypeChecked
在你的 Groovy 代码上激活编译时类型检查。有关详细信息,请参阅 关于类型检查的部分。
@groovy.transform.CompileStatic
@CompileStatic
在你的 Groovy 代码上激活静态编译。有关详细信息,请参阅 关于类型检查的部分。
@groovy.transform.CompileDynamic
@CompileDynamic
在你的 Groovy 代码的部分禁用静态编译。有关详细信息,请参阅 关于类型检查的部分。
@groovy.transform.SelfType
@SelfType
不是 AST 转换,而是一个与特征一起使用的标记接口。有关详细信息,请参阅 特征文档。
2.1.8. Swing 模式
@groovy.beans.Bindable
@Bindable
是一个 AST 转换,它将常规属性转换为绑定属性(根据 JavaBeans 规范)。@Bindable
注释可以放在属性或类上。要将类的所有属性转换为绑定属性,可以在类上添加注释,例如在此示例中
import groovy.beans.Bindable
@Bindable
class Person {
String name
int age
}
这等同于编写以下代码
import java.beans.PropertyChangeListener
import java.beans.PropertyChangeSupport
class Person {
final private PropertyChangeSupport this$propertyChangeSupport
String name
int age
public void addPropertyChangeListener(PropertyChangeListener listener) {
this$propertyChangeSupport.addPropertyChangeListener(listener)
}
public void addPropertyChangeListener(String name, PropertyChangeListener listener) {
this$propertyChangeSupport.addPropertyChangeListener(name, listener)
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
this$propertyChangeSupport.removePropertyChangeListener(listener)
}
public void removePropertyChangeListener(String name, PropertyChangeListener listener) {
this$propertyChangeSupport.removePropertyChangeListener(name, listener)
}
public void firePropertyChange(String name, Object oldValue, Object newValue) {
this$propertyChangeSupport.firePropertyChange(name, oldValue, newValue)
}
public PropertyChangeListener[] getPropertyChangeListeners() {
return this$propertyChangeSupport.getPropertyChangeListeners()
}
public PropertyChangeListener[] getPropertyChangeListeners(String name) {
return this$propertyChangeSupport.getPropertyChangeListeners(name)
}
}
因此,@Bindable
从你的类中删除了大量样板代码,极大地提高了可读性。如果注释放在单个属性上,则只有该属性被绑定
import groovy.beans.Bindable
class Person {
String name
@Bindable int age
}
@groovy.beans.ListenerList
@ListenerList
AST 转换通过注释集合属性,为类生成用于添加、删除和获取监听器列表的代码
import java.awt.event.ActionListener
import groovy.beans.ListenerList
class Component {
@ListenerList
List<ActionListener> listeners;
}
转换将根据列表的泛型类型生成适当的添加/删除方法。此外,它还将根据类上声明的公共方法创建 fireXXX
方法
import java.awt.event.ActionEvent
import java.awt.event.ActionListener as ActionListener
import groovy.beans.ListenerList as ListenerList
public class Component {
@ListenerList
private List<ActionListener> listeners
public void addActionListener(ActionListener listener) {
if ( listener == null) {
return
}
if ( listeners == null) {
listeners = []
}
listeners.add(listener)
}
public void removeActionListener(ActionListener listener) {
if ( listener == null) {
return
}
if ( listeners == null) {
listeners = []
}
listeners.remove(listener)
}
public ActionListener[] getActionListeners() {
Object __result = []
if ( listeners != null) {
__result.addAll(listeners)
}
return (( __result ) as ActionListener[])
}
public void fireActionPerformed(ActionEvent param0) {
if ( listeners != null) {
ArrayList<ActionListener> __list = new ArrayList<ActionListener>(listeners)
for (def listener : __list ) {
listener.actionPerformed(param0)
}
}
}
}
@Bindable
支持多个选项,可以进一步定制转换的行为。
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
name |
泛型类型名称 |
默认情况下,将附加到添加/删除/… 方法的后缀是列表的泛型类型的简单类名。 |
|
synchronize |
false |
如果设置为 true,生成的将同步 |
|
@groovy.beans.Vetoable
@Vetoable
注释的工作方式类似于 @Bindable
,但根据 JavaBeans 规范生成受约束的属性,而不是绑定属性。该注释可以放在类上,这意味着所有属性都将转换为受约束的属性,或者放在单个属性上。例如,使用 @Vetoable
注释此类
import groovy.beans.Vetoable
import java.beans.PropertyVetoException
import java.beans.VetoableChangeListener
@Vetoable
class Person {
String name
int age
}
等同于编写以下代码
public class Person {
private String name
private int age
final private java.beans.VetoableChangeSupport this$vetoableChangeSupport
public void addVetoableChangeListener(VetoableChangeListener listener) {
this$vetoableChangeSupport.addVetoableChangeListener(listener)
}
public void addVetoableChangeListener(String name, VetoableChangeListener listener) {
this$vetoableChangeSupport.addVetoableChangeListener(name, listener)
}
public void removeVetoableChangeListener(VetoableChangeListener listener) {
this$vetoableChangeSupport.removeVetoableChangeListener(listener)
}
public void removeVetoableChangeListener(String name, VetoableChangeListener listener) {
this$vetoableChangeSupport.removeVetoableChangeListener(name, listener)
}
public void fireVetoableChange(String name, Object oldValue, Object newValue) throws PropertyVetoException {
this$vetoableChangeSupport.fireVetoableChange(name, oldValue, newValue)
}
public VetoableChangeListener[] getVetoableChangeListeners() {
return this$vetoableChangeSupport.getVetoableChangeListeners()
}
public VetoableChangeListener[] getVetoableChangeListeners(String name) {
return this$vetoableChangeSupport.getVetoableChangeListeners(name)
}
public void setName(String value) throws PropertyVetoException {
this.fireVetoableChange('name', name, value)
name = value
}
public void setAge(int value) throws PropertyVetoException {
this.fireVetoableChange('age', age, value)
age = value
}
}
如果注释放在单个属性上,则只有该属性被设置为可否决的
import groovy.beans.Vetoable
class Person {
String name
@Vetoable int age
}
2.1.9. 测试辅助
@groovy.test.NotYetImplemented
@NotYetImplemented
用于反转 JUnit 3/4 测试用例的结果。它特别有用,如果一个特性尚未实现,但测试已经实现了。在这种情况下,预计测试会失败。用 @NotYetImplemented
标记它将反转测试的结果,例如在此示例中
import groovy.test.GroovyTestCase
import groovy.test.NotYetImplemented
class Maths {
static int fib(int n) {
// todo: implement later
}
}
class MathsTest extends GroovyTestCase {
@NotYetImplemented
void testFib() {
def dataTable = [
1:1,
2:1,
3:2,
4:3,
5:5,
6:8,
7:13
]
dataTable.each { i, r ->
assert Maths.fib(i) == r
}
}
}
使用此技术的另一个优点是,你可以在不知道如何修复错误之前编写错误的测试用例。如果将来代码中的某个修改通过副作用修复了错误,你将收到通知,因为一个应该失败的测试通过了。
@groovy.transform.ASTTest
@ASTTest
是一种特殊的 AST 转换,旨在帮助调试其他 AST 转换或 Groovy 编译器本身。它将允许开发人员在编译期间“探索”AST 并对 AST 进行断言,而不是对编译的结果进行断言。这意味着这种 AST 转换在生成字节码之前提供了对 AST 的访问。@ASTTest
可以放在任何可注释的节点上,需要两个参数
-
phase:设置
@ASTTest
将被触发的阶段。测试代码将在该阶段结束时在 AST 树上运行。 -
value:注释的节点在到达该阶段时将执行的代码
编译阶段必须从 org.codehaus.groovy.control.CompilePhase 中选择一个。但是,由于无法用相同的注释两次注释一个节点,因此你将无法在同一个节点上的两个不同编译阶段使用 @ASTTest 。 |
value
是一个闭包表达式,它可以访问一个特殊的变量 node
,该变量对应于注释的节点,以及一个辅助 lookup
方法,将在 此处 讨论。例如,你可以像这样注释一个类节点
import groovy.transform.ASTTest
import org.codehaus.groovy.ast.ClassNode
@ASTTest(phase=CONVERSION, value={ (1)
assert node instanceof ClassNode (2)
assert node.name == 'Person' (3)
})
class Person {
}
1 | 我们在 CONVERSION 阶段后检查抽象语法树 (AST) 的状态。 |
2 | 节点是指由 @ASTTest 注解的 AST 节点。 |
3 | 它可以用于在编译时执行断言。 |
@ASTTest
的一个有趣特性是,如果断言失败,则 **编译也会失败**。现在想象一下,我们想要在编译时检查 AST 转换的行为。我们将以 @PackageScope
为例,并希望验证用 @PackageScope
注解的属性会变成包私有字段。为此,我们必须知道转换在哪个阶段运行,这可以在 org.codehaus.groovy.transform.PackageScopeASTTransformation 中找到:语义分析。然后可以编写这样的测试
import groovy.transform.ASTTest
import groovy.transform.PackageScope
@ASTTest(phase=SEMANTIC_ANALYSIS, value={
def nameNode = node.properties.find { it.name == 'name' }
def ageNode = node.properties.find { it.name == 'age' }
assert nameNode
assert ageNode == null // shouldn't be a property anymore
def ageField = node.getDeclaredField 'age'
assert ageField.modifiers == 0
})
class Person {
String name
@PackageScope int age
}
@ASTTest
注解只能放置在语法允许的位置。有时,你可能想要测试一个不可注解的 AST 节点的内容。在这种情况下,@ASTTest
提供了一个方便的 lookup
方法,它将在 AST 中搜索用特殊标记标记的节点。
def list = lookup('anchor') (1)
Statement stmt = list[0] (2)
1 | 返回标签为“anchor”的 AST 节点列表。 |
2 | 由于 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 | 那么我们希望确保在 CANONICALIZATION 阶段添加了 toString 。 |
3 | 否则,如果 toString 存在且上下文中的变量 added 为空。 |
4 | 那么这意味着这个编译阶段就是添加 toString 的阶段。 |
2.1.10. Grape 处理
@groovy.lang.Grapes
Grape
是一个嵌入到 Groovy 中的依赖项管理引擎,它依赖于几个注解,这些注解在指南的这一部分中有详细描述。
2.2. 开发 AST 转换
转换有两种类型:全局转换和局部转换。
-
全局转换 由编译器应用于正在编译的代码,无论转换应用于何处。实现全局转换的已编译类位于添加到编译器类路径中的 JAR 文件中,并包含服务定位器文件
META-INF/services/org.codehaus.groovy.transform.ASTTransformation
,其中包含一行包含转换类名称。转换类必须具有无参数构造函数并实现org.codehaus.groovy.transform.ASTTransformation
接口。它将针对 **编译中的所有源代码** 运行,因此请确保不要创建以扩展和耗时的方式扫描整个 AST 的转换,以保持编译器的速度。 -
局部转换 是通过注解你想要转换的代码元素来应用的局部转换。为此,我们重用注解符号,这些注解应该实现
org.codehaus.groovy.transform.ASTTransformation
。编译器将发现它们并将转换应用于这些代码元素。
2.2.1. 编译阶段指南
Groovy AST 转换必须在九个定义的编译阶段之一中执行(org.codehaus.groovy.control.CompilePhase)。
全局转换可以在任何阶段应用,但局部转换只能在语义分析阶段或之后应用。简而言之,编译阶段是
-
初始化:打开源文件并配置环境。
-
解析:使用语法生成表示源代码的令牌树。
-
转换:从令牌树创建抽象语法树 (AST)。
-
语义分析:执行语法无法检查的的一致性和有效性检查,并解析类。
-
规范化:完成 AST 的构建。
-
指令选择:选择指令集,例如 Java 6 或 Java 7 字节码级别。
-
类生成:在内存中创建类的字节码。
-
输出:将二进制输出写入文件系统。
-
完成:执行任何最后清理。
一般来说,在后面的阶段会有更多类型信息可用。如果你的转换与读取 AST 有关,那么信息更丰富的后期阶段可能是一个不错的选择。如果你的转换与写入 AST 有关,那么树更稀疏的早期阶段可能更方便。
2.2.2. 局部转换
局部 AST 转换相对于它们应用的上下文而言。在大多数情况下,上下文是由注解定义的,该注解将定义转换的范围。例如,注解一个字段意味着转换 *应用于* 该字段,而注解类意味着转换 *应用于* 整个类。
作为一个简单的例子,考虑想要编写一个 @WithLogging
转换,它会在方法调用开始和结束时添加控制台消息。因此,以下“Hello World”示例实际上会打印“Hello World”以及开始和停止消息。
@WithLogging
def greet() {
println "Hello World"
}
greet()
局部 AST 转换是一种简单的实现方式。它需要两件事
-
@WithLogging
注解的定义。 -
org.codehaus.groovy.transform.ASTTransformation 的实现,该实现将日志记录表达式添加到方法中。
ASTTransformation
是一个回调,它使你能够访问 org.codehaus.groovy.control.SourceUnit,通过它可以获取 org.codehaus.groovy.ast.ModuleNode(AST)的引用。
AST(抽象语法树)是一种树形结构,主要由 org.codehaus.groovy.ast.expr.Expression(表达式)或 org.codehaus.groovy.ast.expr.Statement(语句)组成。了解 AST 的一种简单方法是在调试器中探索它。获得 AST 后,你可以分析它以找出有关代码的信息或重写它以添加新功能。
局部转换注解是最简单的部分。以下是 @WithLogging
注解。
import org.codehaus.groovy.transform.GroovyASTTransformationClass
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.METHOD])
@GroovyASTTransformationClass(["gep.WithLoggingASTTransformation"])
public @interface WithLogging {
}
注解保留可以是 SOURCE
,因为你不需要在之后保留注解。这里元素类型是 METHOD
,@WithLogging
因为注解应用于方法。
但最重要的部分是 @GroovyASTTransformationClass
注解。它将 @WithLogging
注解链接到你将编写的 ASTTransformation
类。gep.WithLoggingASTTransformation
是我们将编写的 ASTTransformation
的完全限定类名。这一行将注解连接到转换。
有了它,Groovy 编译器将在源单元中找到 @WithLogging
时,始终调用 gep.WithLoggingASTTransformation
。在运行示例脚本时,在 IDE 中设置的 LoggingASTTransformation
内部的任何断点现在都会被命中。
ASTTransformation
类稍微复杂一些。以下是一个非常简单且非常幼稚的转换,它为 @WithLogging
添加方法开始和停止消息。
@CompileStatic (1)
@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS) (2)
class WithLoggingASTTransformation implements ASTTransformation { (3)
@Override
void visit(ASTNode[] nodes, SourceUnit sourceUnit) { (4)
MethodNode method = (MethodNode) nodes[1] (5)
def startMessage = createPrintlnAst("Starting $method.name") (6)
def endMessage = createPrintlnAst("Ending $method.name") (7)
def existingStatements = ((BlockStatement)method.code).statements (8)
existingStatements.add(0, startMessage) (9)
existingStatements.add(endMessage) (10)
}
private static Statement createPrintlnAst(String message) { (11)
new ExpressionStatement(
new MethodCallExpression(
new VariableExpression("this"),
new ConstantExpression("println"),
new ArgumentListExpression(
new ConstantExpression(message)
)
)
)
}
}
1 | 即使不是必需的,如果你用 Groovy 编写 AST 转换,也强烈建议使用 CompileStatic ,因为它会提高编译器的性能。 |
2 | 用 org.codehaus.groovy.transform.GroovyASTTransformation 进行注解,以告知转换需要在哪个编译阶段运行。这里是在 *语义分析* 阶段。 |
3 | 实现 ASTTransformation 接口。 |
4 | 它只有一个 visit 方法。 |
5 | nodes 参数是一个包含 2 个 AST 节点的数组,其中第一个是注解节点 (@WithLogging ),第二个是带注解的节点(方法节点)。 |
6 | 创建一个语句,当我们进入方法时打印一条消息。 |
7 | 创建一个语句,当我们退出方法时打印一条消息。 |
8 | 获取方法体,在本例中为 BlockStatement 。 |
9 | 在现有代码的第一个语句之前添加进入方法消息。 |
10 | 在现有代码的最后一个语句之后追加退出方法消息。 |
11 | 创建一个 ExpressionStatement ,它包装一个 MethodCallExpression ,对应于 this.println("message") 。 |
需要注意的是,为了示例简洁,我们没有进行必要的检查,例如检查带注解的节点是否真的是 MethodNode
,或者方法体是否是 BlockStatement
的实例。这个练习留给读者完成。
注意在 createPrintlnAst(String)
方法中创建新的 println 语句。为代码创建 AST 并不总是简单。在本例中,我们需要构造一个新的方法调用,传入接收者/变量、方法名称和参数列表。创建 AST 时,将你试图创建的代码写入 Groovy 文件,然后在调试器中检查该代码的 AST 以了解要创建什么,可能会有所帮助。然后使用你在调试器中了解到的知识编写类似于 createPrintlnAst
的函数。
最后
@WithLogging
def greet() {
println "Hello World"
}
greet()
产生
Starting greet Hello World Ending greet
需要注意的是,AST 转换直接参与编译过程。初学者常犯的一个错误是将 AST 转换代码与使用该转换的类放在同一个源代码树中。通常情况下,在同一个源代码树中意味着它们同时编译。由于转换本身将在阶段中编译,并且每个编译阶段都会在进入下一个阶段之前处理同一个源单元的所有文件,因此有一个直接的结果:转换将不会在使用它的类之前编译!总之,在你可以使用 AST 转换之前,需要先对它们进行预编译。通常,将它们放在单独的源代码树中就足够了。 |
2.2.3. 全局转换
全局 AST 变换与局部变换类似,但有一个主要区别:它们不需要注解,这意味着它们被全局应用,也就是说,它们应用于被编译的每个类。因此,将它们的使用限制在最后的手段非常重要,因为它们会对编译器性能产生重大影响。
以局部 AST 变换为例,想象一下我们想跟踪所有方法,而不仅仅是那些用 @WithLogging
注解的方法。基本上,我们需要这段代码的行为与之前用 @WithLogging
注解的代码相同。
def greet() {
println "Hello World"
}
greet()
要实现这一点,需要两个步骤。
-
在
META-INF/services
目录中创建org.codehaus.groovy.transform.ASTTransformation
描述符。 -
创建
ASTTransformation
实现。
描述符文件是必需的,并且必须在类路径中找到。它将包含一行代码。
gep.WithLoggingASTTransformation
变换代码与局部情况类似,但不需要使用 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 | 对被注解的方法调用变换。 |
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 | 创建 return 语句。 |
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 | 将 return 语句添加到新方法中。 |
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
方法只能生成 语句
和 表达式
。但是,如果我们想生成其他类型的节点,比如方法、字段等等呢?
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 | 构建一个名为 giveMeTwo 的新 **ClassNode**,它返回作为参数传递的表达式的结果。 |
现在,与其创建一个测试,在给定的示例代码上执行转换,我更想检查二元表达式的构建是否正确。
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 转换的逐步教程,可以参考 此研讨会。