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 | 将请求转发给所有属性的 getter,除了 field3 。 |
可以通过重写 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
静态变体 methodMissing
方法可以通过 ExpandoMetaClass 添加,也可以在类级别使用 $static_methodMissing
方法实现。
class Foo {
static def $static_methodMissing(String name, Object args) {
return "Missing static method name is $name"
}
}
assert Foo.bar() == 'Missing static method name is bar'
1.6. 静态 propertyMissing
静态变体 propertyMissing
方法可以通过 ExpandoMetaClass 添加,也可以在类级别使用 $static_propertyMissing
方法实现。
class Foo {
static def $static_propertyMissing(String name) {
return "Missing static property name is $name"
}
}
assert Foo.foobar == 'Missing static property name is foobar'
1.7. GroovyInterceptable
groovy.lang.GroovyInterceptable 接口是标记接口,它扩展了 GroovyObject
,用于通知 Groovy 运行时所有方法都应通过 Groovy 运行时的方法调度器机制进行拦截。
package groovy.lang;
public interface GroovyInterceptable extends GroovyObject {
}
当 Groovy 对象实现 GroovyInterceptable
接口时,其 invokeMethod()
将被调用以执行任何方法调用。下面您可以看到此类对象的简单示例
class Interception implements GroovyInterceptable {
def definedMethod() { }
def invokeMethod(String name, Object args) {
'invokedMethod'
}
}
下一段代码是一个测试,它表明对现有方法和不存在方法的调用都将返回相同的值。
class InterceptableTest extends GroovyTestCase {
void testCheckInterception() {
def interception = new Interception()
assert interception.definedMethod() == 'invokedMethod'
assert interception.someMethod() == 'invokedMethod'
}
}
我们不能使用像 println 这样的默认 groovy 方法,因为这些方法被注入到所有 Groovy 对象中,因此它们也会被拦截。 |
如果我们想拦截所有方法调用,但不想实现 GroovyInterceptable
接口,我们可以实现对象 MetaClass
上的 invokeMethod()
。这种方法适用于 POGO 和 POJO,如以下示例所示
class InterceptionThroughMetaClassTest extends GroovyTestCase {
void testPOJOMetaClassInterception() {
String invoking = 'ha'
invoking.metaClass.invokeMethod = { String name, Object args ->
'invoked'
}
assert invoking.length() == 'invoked'
assert invoking.someMethod() == 'invoked'
}
void testPOGOMetaClassInterception() {
Entity entity = new Entity('Hello')
entity.metaClass.invokeMethod = { String name, Object args ->
'invoked'
}
assert entity.build(new Object()) == 'invoked'
assert entity.someMethod() == 'invoked'
}
}
有关 MetaClass 的更多信息,请参阅 元类 部分。 |
1.8. 类别
在某些情况下,如果一个不受控制的类拥有额外的方法,会非常有用。为了实现这种功能,Groovy 实现了一个借鉴自 Objective-C 的功能,称为“类别”。
类别通过所谓的“类别类”实现。类别类很特别,因为它需要满足某些预定义规则才能定义扩展方法。
系统中包含一些类别,用于为类添加功能,使其在 Groovy 环境中更易于使用。
类别类默认情况下不会启用。要使用类别类中定义的方法,需要应用 GDK 提供的范围内的 use 方法,该方法可从每个 Groovy 对象实例内部访问。
use(TimeCategory) {
println 1.minute.from.now (1)
println 10.hours.ago
def someDate = new Date() (2)
println someDate - 3.months
}
1 | TimeCategory 为 Integer 添加方法。 |
2 | TimeCategory 为 Date 添加方法。 |
use 方法将类别类作为第一个参数,将闭包代码块作为第二个参数。在 Closure 内部,可以访问类别方法。如上面的示例所示,即使是 JDK 类(如 java.lang.Integer 或 java.util.Date)也可以使用用户定义的方法进行扩展。
类别不需要直接暴露给用户代码,以下方法也可以实现。
class JPACategory{
// Let's enhance JPA EntityManager without getting into the JSR committee
static void persistAll(EntityManager em , Object[] entities) { //add an interface to save all
entities?.each { em.persist(it) }
}
}
def transactionContext = {
EntityManager em, Closure c ->
def tx = em.transaction
try {
tx.begin()
use(JPACategory) {
c()
}
tx.commit()
} catch (e) {
tx.rollback()
} finally {
//cleanup your resource here
}
}
// user code, they always forget to close resource in exception, some even forget to commit, let's not rely on them.
EntityManager em; //probably injected
transactionContext (em) {
em.persistAll(obj1, obj2, obj3)
// let's do some logics here to make the example sensible
em.persistAll(obj2, obj4, obj6)
}
当我们查看 groovy.time.TimeCategory 类时,我们会发现扩展方法都是声明为 static 方法的。实际上,这是类别类必须满足的条件之一,才能成功地将它的方法添加到 use 代码块中的类中。
public class TimeCategory {
public static Date plus(final Date date, final BaseDuration duration) {
return duration.plus(date);
}
public static Date minus(final Date date, final BaseDuration duration) {
final Calendar cal = Calendar.getInstance();
cal.setTime(date);
cal.add(Calendar.YEAR, -duration.getYears());
cal.add(Calendar.MONTH, -duration.getMonths());
cal.add(Calendar.DAY_OF_YEAR, -duration.getDays());
cal.add(Calendar.HOUR_OF_DAY, -duration.getHours());
cal.add(Calendar.MINUTE, -duration.getMinutes());
cal.add(Calendar.SECOND, -duration.getSeconds());
cal.add(Calendar.MILLISECOND, -duration.getMillis());
return cal.getTime();
}
// ...
另一个要求是,静态方法的第一个参数必须定义该方法在激活后所附加的类型。其他参数是该方法将作为参数接收的正常参数。
由于参数和静态方法的约定,类别方法定义可能比普通方法定义稍微不直观。作为替代,Groovy 带有 @Category 注解,该注解在编译时将带注解的类转换为类别类。
class Distance {
def number
String toString() { "${number}m" }
}
@Category(Number)
class NumberCategory {
Distance getMeters() {
new Distance(number: this)
}
}
use (NumberCategory) {
assert 42.meters.toString() == '42m'
}
应用 @Category 注解的优点是可以使用实例方法,而无需目标类型作为第一个参数。目标类型类作为参数传递给注解。
在 编译时元编程部分 中有一个关于 @Category 的单独部分。 |
1.9. 元类
如前所述,元类在方法解析中起着至关重要的作用。对于从 groovy 代码调用的每个方法,Groovy 将找到给定对象的 MetaClass,并将方法解析委托给元类,通过 groovy.lang.MetaClass#invokeMethod(java.lang.Class,java.lang.Object,java.lang.String,java.lang.Object,boolean,boolean),不要与 groovy.lang.GroovyObject#invokeMethod(java.lang.String,java.lang.Object) 混淆,后者恰好是元类最终可能调用的方法。
1.9.1. 默认元类 MetaClassImpl
默认情况下,对象会获得 MetaClassImpl 的实例,该实例实现默认方法查找。这种方法查找包括在对象类中查找方法(“常规”方法),但如果通过这种方式没有找到方法,它将诉诸于调用 methodMissing,并最终调用 groovy.lang.GroovyObject#invokeMethod(java.lang.String,java.lang.Object)
class Foo {}
def f = new Foo()
assert f.metaClass =~ /MetaClassImpl/
1.9.2. 自定义元类
您可以更改任何对象或类的元类,并将其替换为 MetaClass 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 不执行继承。要启用此功能,您必须在应用程序启动之前调用 ExpandoMetaClass#enableGlobally(),例如在 main 方法或 servlet 引导程序中。 |
以下部分将详细介绍如何在各种场景中使用 ExpandoMetaClass。
方法
通过调用 metaClass 属性访问 ExpandoMetaClass 后,可以使用左移 << 或 = 运算符添加方法。
请注意,左移运算符用于追加新方法。如果类或接口声明了具有相同名称和参数类型的公共方法,包括从超类和超接口继承的那些方法,但不包括在运行时添加到 metaClass 的那些方法,则将抛出异常。如果要替换类或接口声明的方法,可以使用 = 运算符。 |
这些运算符应用于 metaClass 的一个不存在的属性上,传递一个 Closure 代码块的实例。
class Book {
String title
}
Book.metaClass.titleInUpperCase << {-> title.toUpperCase() }
def b = new Book(title:"The Stand")
assert "THE STAND" == b.titleInUpperCase()
上面的示例展示了如何通过访问 metaClass 属性并使用 << 或 = 运算符为其分配一个 Closure 代码块来向类添加新方法。Closure 参数被解释为方法参数。可以使用 {→ …} 语法添加无参数方法。
属性
ExpandoMetaClass 支持两种添加或覆盖属性的机制。
首先,它支持通过简单地为 metaClass 的属性分配一个值来声明可变属性。
class Book {
String title
}
Book.metaClass.author = "Stephen King"
def b = new Book()
assert "Stephen King" == b.author
另一种方法是通过使用添加实例方法的标准机制来添加 getter 和/或 setter 方法。
class Book {
String title
}
Book.metaClass.getAuthor << {-> "Stephen King" }
def b = new Book()
assert "Stephen King" == b.author
在上面的源代码示例中,属性由闭包决定,是一个只读属性。可以添加一个等效的 setter 方法,但随后需要存储属性值以供以后使用。这可以通过以下示例所示的方式完成。
class Book {
String title
}
def properties = Collections.synchronizedMap([:])
Book.metaClass.setAuthor = { String value ->
properties[System.identityHashCode(delegate) + "author"] = value
}
Book.metaClass.getAuthor = {->
properties[System.identityHashCode(delegate) + "author"]
}
但这并不是唯一的技术。例如,在 servlet 容器中,一种方法可能是将值存储在当前执行的请求中,作为请求属性(如 Grails 在某些情况下所做的那样)。
构造函数
可以使用特殊的 constructor 属性添加构造函数。可以使用 << 或 = 运算符为其分配一个 Closure 代码块。当代码在运行时执行时,Closure 的参数将成为构造函数参数。
class Book {
String title
}
Book.metaClass.constructor << { String title -> new Book(title:title) }
def book = new Book('Groovy in Action - 2nd Edition')
assert book.title == 'Groovy in Action - 2nd Edition'
但是,在添加构造函数时要小心,因为很容易陷入堆栈溢出问题。 |
静态方法
可以使用与实例方法相同的技术添加静态方法,方法是在方法名称之前添加 static 限定符。
class Book {
String title
}
Book.metaClass.static.create << { String title -> new Book(title:title) }
def b = Book.create("The Stand")
借用方法
使用 ExpandoMetaClass,可以使用 Groovy 的方法指针语法从其他类借用方法。
class Person {
String name
}
class MortgageLender {
def borrowMoney() {
"buy house"
}
}
def lender = new MortgageLender()
Person.metaClass.buyHouse = lender.&borrowMoney
def p = new Person()
assert "buy house" == p.buyHouse()
动态方法名
由于 Groovy 允许您使用字符串作为属性名,因此这反过来允许您在运行时动态创建方法名和属性名。要使用动态名称创建方法,只需使用字符串作为引用属性名的语言功能即可。
class Person {
String name = "Fred"
}
def methodName = "Bob"
Person.metaClass."changeNameTo${methodName}" = {-> delegate.name = "Bob" }
def p = new Person()
assert "Fred" == p.name
p.changeNameToBob()
assert "Bob" == p.name
相同的概念可以应用于静态方法和属性。
动态方法名的一个应用可以在 Grails Web 应用程序框架中找到。使用动态方法名实现了“动态编解码器”的概念。
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
方法的第一个参数对应于接收方,而其他参数对应于扩展方法的参数。因此,这里定义了一个名为 getText 的方法,它在 File
类上(因为第一个参数是 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 |
是否应将 super 包含在 equals 和 hashCode 计算中 |
|
includeFields |
False |
除了属性之外,是否应在 equals/hashCode 中包含字段 |
|
useCanEqual |
True |
是否应使 equals 调用 canEqual 帮助程序方法。 |
|
allProperties |
False |
是否应将 JavaBean 属性包含在 equals 和 hashCode 计算中 |
|
allNames |
False |
是否应将具有内部名称的字段和/或属性包含在 equals 和 hashCode 计算中 |
|
@groovy.transform.TupleConstructor
@TupleConstructor
注解旨在通过为您生成构造函数来消除样板代码。 创建一个元组构造函数,它对每个属性(以及可能每个字段)都有一个参数。 每个参数都有一个默认值(使用属性的初始值,如果存在,否则根据属性类型使用 Java 的默认值)。
实现细节
通常您不需要了解生成的构造函数的实现细节; 您只需以通常的方式使用它们。 但是,如果您想添加多个构造函数,了解 Java 集成选项或满足某些依赖项注入框架的要求,那么了解一些细节很有用。
如前所述,生成的构造函数应用了默认值。 在随后的编译阶段,将应用 Groovy 编译器的标准默认值处理行为。 最终结果是将多个构造函数放在类的字节码中。 这提供了一个易于理解的语义,对于 Java 集成目的也很有用。 例如,以下代码将生成 3 个构造函数
import groovy.transform.TupleConstructor
@TupleConstructor
class Person {
String firstName
String lastName
}
// traditional map-style constructor
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
// generated tuple constructor
def p2 = new Person('Jack', 'Nicholson')
// generated tuple constructor with default value for second property
def p3 = new Person('Jack')
第一个构造函数是一个无参数构造函数,只要您没有最终属性,它就允许传统的 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
注解旨在通过为您生成 map 构造函数来消除样板代码。 创建一个 map 构造函数,以便设置类中的每个属性,其依据是提供的 map 中的值,该值的键与属性的名称相同。 用法如以下示例所示
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 变换用于帮助编写Comparable
类,并且通常可以通过众多属性轻松排序。它易于使用,如以下示例所示,我们在其中对Person
类进行注释
import groovy.transform.Sortable
@Sortable class Person {
String first
String last
Integer born
}
生成的类具有以下属性
-
它实现了
Comparable
接口 -
它包含一个
compareTo
方法,该方法的实现基于first
、last
和born
属性的自然排序 -
它有三个返回比较器的方法:
comparatorByFirst
、comparatorByLast
和comparatorByBorn
。
生成的compareTo
方法将如下所示
public int compareTo(java.lang.Object obj) {
if (this.is(obj)) {
return 0
}
if (!(obj instanceof Person)) {
return -1
}
java.lang.Integer value = this.first <=> obj.first
if (value != 0) {
return value
}
value = this.last <=> obj.last
if (value != 0) {
return value
}
value = this.born <=> obj.born
if (value != 0) {
return value
}
return 0
}
作为生成的比较器的示例,comparatorByFirst
比较器将具有一个compare
方法,该方法如下所示
public int compare(java.lang.Object arg0, java.lang.Object arg1) {
if (arg0 == arg1) {
return 0
}
if (arg0 != null && arg1 == null) {
return -1
}
if (arg0 == null && arg1 != null) {
return 1
}
return arg0.first <=> arg1.first
}
Person
类可以在任何需要Comparable
的地方使用,而生成的比较器可以在任何需要Comparator
的地方使用,如以下示例所示
def people = [
new Person(first: 'Johnny', last: 'Depp', born: 1963),
new Person(first: 'Keira', last: 'Knightley', born: 1985),
new Person(first: 'Geoffrey', last: 'Rush', born: 1951),
new Person(first: 'Orlando', last: 'Bloom', born: 1977)
]
assert people[0] > people[2]
assert people.sort()*.last == ['Rush', 'Depp', 'Knightley', 'Bloom']
assert people.sort(false, Person.comparatorByFirst())*.first == ['Geoffrey', 'Johnny', 'Keira', 'Orlando']
assert people.sort(false, Person.comparatorByLast())*.last == ['Bloom', 'Depp', 'Knightley', 'Rush']
assert people.sort(false, Person.comparatorByBorn())*.last == ['Rush', 'Depp', 'Bloom', 'Knightley']
通常,所有属性都在生成的compareTo
方法中按定义顺序使用。您可以通过在includes
或excludes
注释属性中提供属性名称列表,来包含或排除生成的compareTo
方法中的某些属性。如果使用includes
,则提供的属性名称的顺序将决定比较时属性的优先级。为了说明,请考虑以下Person
类定义
@Sortable(includes='first,born') class Person {
String last
int born
String first
}
它将有两个比较器方法comparatorByFirst
和comparatorByBorn
,生成的compareTo
方法将如下所示
public int compareTo(java.lang.Object obj) {
if (this.is(obj)) {
return 0
}
if (!(obj instanceof Person)) {
return -1
}
java.lang.Integer value = this.first <=> obj.first
if (value != 0) {
return value
}
value = this.born <=> obj.born
if (value != 0) {
return value
}
return 0
}
此Person
类可以按如下方式使用
def people = [
new Person(first: 'Ben', last: 'Affleck', born: 1972),
new Person(first: 'Ben', last: 'Stiller', born: 1965)
]
assert people.sort()*.last == ['Stiller', 'Affleck']
可以使用以下附加参数进一步更改@Sortable
AST 变换的行为
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
allProperties |
True |
是否应该使用 JavaBean 属性(在原生属性之后排序) |
|
allNames |
False |
是否应该使用具有“内部”名称的属性 |
|
includeSuperProperties |
False |
是否也应该使用超级属性(优先排序) |
|
@groovy.transform.builder.Builder
@Builder
AST 变换用于帮助编写可以使用流畅的API 调用创建的类。该变换支持多种构建策略以涵盖各种情况,并且有很多配置选项可以自定义构建过程。如果您是 AST 黑客,您还可以定义自己的策略类。下表列出了与 Groovy 捆绑在一起的可用策略以及每种策略支持的配置选项。
策略 |
描述 |
builderClassName |
builderMethodName |
buildMethodName |
prefix |
includes/excludes |
includeSuperProperties |
allNames |
|
链接的设置器 |
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
}
然后,只需像这里显示的那样以链接的方式调用设置器
def p1 = new Person().setFirst('Johnny').setLast('Depp').setBorn(1963)
assert "$p1.first $p1.last" == 'Johnny Depp'
对于每个属性,将创建一个生成的设置器,它看起来像这样
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
}
调用链接设置器将如下所示
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
)也是如此。
如果您有一个希望在构建过程中调用的设置器,可以使用注释属性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'
请注意,您提供的(通常为空)构建器类将填充有适当的设置器和构建方法。生成的构建方法将看起来类似于
public Person build() {
Person _thePerson = new Person()
_thePerson.first = first
_thePerson.last = last
_thePerson.born = born
return _thePerson
}
您要为其创建构建器的类可以是任何遵循正常 JavaBean 约定的 Java 或 Groovy 类,例如无参数构造函数和属性的设置器。以下是如何使用 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()
如果您有一个希望在构建过程中调用的设置器,可以使用注释属性useSetters
。有关详细信息,请参阅 JavaDoc。
此策略还支持注释静态方法和构造函数。在这种情况下,静态方法或构造函数参数将成为用于构建目的的属性,在静态方法的情况下,方法的返回类型将成为正在构建的目标类。如果您在一个类中使用了一个以上的@Builder
注释(在类、方法或构造函数位置),那么您有责任确保生成的辅助类和工厂方法具有唯一的名称(即最多只有一个可以使用默认名称值)。有关方法和构造函数用法的示例,但使用DefaultStrategy
策略,请参阅该策略的文档。
注释属性forClass
不适用于此策略。
@groovy.transform.AutoImplement
@AutoImplement
AST 变换为找到的任何来自超类或接口的抽象方法提供虚拟实现。虚拟实现对于找到的所有抽象方法都是一样的,并且可以是
-
本质上是空的(对于 void 方法和具有返回值的方法完全正确,返回该类型的默认值)
-
一个抛出指定异常的语句(带有可选消息)
-
一些用户提供的代码
第一个示例说明了默认情况。我们的类使用@AutoImplement
进行注释,具有一个超类和一个单个接口,如这里所示
import groovy.transform.AutoImplement
@AutoImplement
class MyNames extends AbstractList<String> implements Closeable { }
提供了一个来自Closeable
接口的void close()
方法,并且留空。还为来自超类的三个抽象方法提供了实现。get
、addAll
和size
方法的返回值类型分别是String
、boolean
和int
,其默认值分别是null
、false
和0
。我们可以使用我们的类(并检查其中一种方法的预期返回类型),使用以下代码
assert new MyNames().size() == 0
检查等效的生成代码也很有意义
class MyNames implements Closeable extends AbstractList<String> {
String get(int param0) {
return null
}
boolean addAll(Collection<? extends String> param0) {
return false
}
void close() throws Exception {
}
int size() {
return 0
}
}
第二个示例说明了最简单的异常情况。我们的类使用@AutoImplement
进行注释,具有一个超类,并且注释属性表明如果调用了任何“虚拟”方法,则应该抛出IOException
。以下是类的定义
@AutoImplement(exception=IOException)
class MyWriter extends Writer { }
我们可以使用该类(并检查其中一种方法是否抛出了预期异常),使用以下代码
import static groovy.test.GroovyAssert.shouldFail
shouldFail(IOException) {
new MyWriter().flush()
}
检查等效的生成代码也很有意义,其中提供了三个 void 方法,它们都抛出了提供的异常
class MyWriter extends Writer {
void flush() throws IOException {
throw new IOException()
}
void write(char[] param0, int param1, int param2) throws IOException {
throw new IOException()
}
void close() throws Exception {
throw new IOException()
}
}
第三个示例说明了带有提供的消息的异常情况。我们的类使用@AutoImplement
进行注释,实现了接口,并且具有注释属性以指示应为任何提供的 method 抛出带有Not supported by MyIterator
作为消息的UnsupportedOperationException
。以下是类的定义
@AutoImplement(exception=UnsupportedOperationException, message='Not supported by MyIterator')
class MyIterator implements Iterator<String> { }
我们可以使用该类(并检查预期异常是否抛出,以及是否具有其中一种方法的正确消息),使用以下代码
def ex = shouldFail(UnsupportedOperationException) {
new MyIterator().hasNext()
}
assert ex.message == 'Not supported by MyIterator'
检查等效的生成代码也很有意义,其中提供了三个 void 方法,它们都抛出了提供的异常
class MyIterator implements Iterator<String> {
boolean hasNext() {
throw new UnsupportedOperationException('Not supported by MyIterator')
}
String next() {
throw new UnsupportedOperationException('Not supported by MyIterator')
}
}
第四个示例说明了用户提供的代码的情况。我们的类使用@AutoImplement
进行注释,实现了接口,具有显式覆盖的hasNext
方法,以及包含为任何提供的 method 提供的代码的注释属性。以下是类的定义
@AutoImplement(code = { throw new UnsupportedOperationException('Should never be called but was called on ' + new Date()) })
class EmptyIterator implements Iterator<String> {
boolean hasNext() { false }
}
我们可以使用该类(并检查预期异常是否抛出,以及是否具有预期的形式的消息),使用以下代码
def ex = shouldFail(UnsupportedOperationException) {
new EmptyIterator().next()
}
assert ex.message.startsWith('Should never be called but was called on ')
检查等效的生成代码也很有意义,其中提供了next
方法
class EmptyIterator implements java.util.Iterator<String> {
boolean hasNext() {
false
}
String next() {
throw new UnsupportedOperationException('Should never be called but was called on ' + new Date())
}
}
@groovy.transform.NullCheck
@NullCheck
AST 变换会在构造函数和方法中添加空检查保护语句,这些语句会在提供空参数时导致方法提前失败。可以将其视为防御性编程的一种形式。该注解可以添加到单个方法或构造函数,也可以添加到类中,在这种情况下它将应用于所有方法/构造函数。
@NullCheck
String longerOf(String first, String second) {
first.size() >= second.size() ? first : second
}
assert longerOf('cat', 'canary') == 'canary'
def ex = shouldFail(IllegalArgumentException) {
longerOf('cat', null)
}
assert ex.message == 'second cannot be null'
2.1.2. 类设计注解
这类注解旨在通过使用声明性风格简化众所周知的设计模式(委托、单例等)的实现。
@groovy.transform.BaseScript
@BaseScript
用于脚本中,表示该脚本应扩展自自定义脚本基类,而不是 groovy.lang.Script
。有关详细信息,请参阅 领域特定语言 文档。
@groovy.lang.Delegate
@Delegate
AST 变换旨在实现委托设计模式。在以下类中
class Event {
@Delegate Date when
String title
}
when
属性使用 @Delegate
注释,这意味着 Event
类将把对 Date
方法的调用委托给 when
属性。在这种情况下,生成的代码如下所示
class Event {
Date when
String title
boolean before(Date other) {
when.before(other)
}
// ...
}
然后,您可以直接在 Event
类上调用 before
方法,例如
def ev = new Event(title:'Groovy keynote', when: Date.parse('yyyy/MM/dd', '2013/09/10'))
def now = new Date()
assert ev.before(now)
除了注释属性(或字段)外,还可以注释方法。在这种情况下,该方法可以被视为委托的 getter 或工厂方法。例如,以下类(很不寻常)有一个委托池,这些委托以循环方式访问
class Test {
private int robinCount = 0
private List<List> items = [[0], [1], [2]]
@Delegate
List getRoundRobinList() {
items[robinCount++ % items.size()]
}
void checkItems(List<List> testValue) {
assert items == testValue
}
}
以下是如何使用该类的示例
def t = new Test()
t << 'fee'
t << 'fi'
t << 'fo'
t << 'fum'
t.checkItems([[0, 'fee', 'fum'], [1, 'fi'], [2, 'fo']])
在循环方式中使用标准列表将违反列表的许多预期属性,因此不要期望上面的类除了这个微不足道的示例之外执行任何有用的操作。
@Delegate
AST 变换的行为可以使用以下参数更改
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
接口 |
True |
字段实现的接口是否也应该由类实现 |
|
已弃用 |
false |
如果为 true,则还委托使用 @Deprecated 注释的方法 |
|
methodAnnotations |
False |
是否将委托方法的注解传递到您的委托方法。 |
|
parameterAnnotations |
False |
是否将委托方法参数的注解传递到您的委托方法。 |
|
excludes |
空数组 |
要从委托中排除的方法列表。有关更细粒度的控制,另请参阅 |
|
includes |
未定义的标记数组(表示所有方法) |
要包含在委托中的方法列表。有关更细粒度的控制,另请参阅 |
|
excludeTypes |
空数组 |
包含要从委托中排除的方法签名的接口列表 |
|
includeTypes |
未定义的标记数组(表示默认情况下没有列表) |
包含要包含在委托中的方法签名的接口列表 |
|
allNames |
False |
委托模式是否也应该应用于具有内部名称的方法 |
|
@groovy.transform.Immutable
@Immutable
元注解组合了以下注解
@Immutable
元注解简化了不可变类的创建。不可变类很有用,因为它们通常更容易推理,并且本质上是线程安全的。有关如何在 Java 中实现不可变类的所有详细信息,请参阅 Effective Java,最小化可变性。@Immutable
元注解会自动为您完成 *Effective Java* 中描述的大多数事情。要使用该元注解,您需要做的就是像以下示例一样注释该类
import groovy.transform.Immutable
@Immutable
class Point {
int x
int y
}
不可变类的要求之一是无法修改类中的任何状态信息。实现此目的的一个要求是为每个属性使用不可变类,或者另外执行特殊编码,例如在构造函数和属性 getter 中对任何可变属性进行防御性复制和防御性复制输出。在 @ImmutableBase
、@MapConstructor
和 @TupleConstructor
之间,属性要么被识别为不可变的,要么会自动处理许多已知情况的特殊编码。提供各种机制供您扩展允许的已处理属性类型。有关详细信息,请参阅 @ImmutableOptions
和 @KnownImmutable
。
将 @Immutable
应用于类的结果与应用 @Canonical 元注解的结果非常相似,但生成的类将具有额外的逻辑来处理不可变性。例如,您可以通过尝试修改属性来观察这一点,这将导致抛出 ReadOnlyPropertyException
,因为属性的支持字段将自动变为最终的。
@Immutable
元注解支持它聚合的注解中的配置选项。有关更多详细信息,请参阅这些注解。
@groovy.transform.ImmutableBase
使用 @ImmutableBase
生成的不可变类会自动变为最终的。此外,会检查每个属性的类型,并在类上进行各种检查,例如,目前不允许公开实例字段。如果需要,它还会生成一个 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
已弃用。请考虑使用 traits 代替。
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
final 字段(或对于静态方法为 $REENTRANTLOCK
),并将添加适当的同步代码。例如,以下代码
import groovy.transform.WithReadLock
import groovy.transform.WithWriteLock
class Counters {
public final Map<String,Integer> map = [:].withDefault { 0 }
@WithReadLock
int get(String id) {
map.get(id)
}
@WithWriteLock
void add(String id, int num) {
Thread.sleep(200) // emulate long computation
map.put(id, map.get(id)+num)
}
}
等同于此
import groovy.transform.WithReadLock as WithReadLock
import groovy.transform.WithWriteLock as WithWriteLock
public class Counters {
private final Map<String, Integer> map
private final java.util.concurrent.locks.ReentrantReadWriteLock $reentrantlock
public int get(java.lang.String id) {
$reentrantlock.readLock().lock()
try {
map.get(id)
}
finally {
$reentrantlock.readLock().unlock()
}
}
public void add(java.lang.String id, int num) {
$reentrantlock.writeLock().lock()
try {
java.lang.Thread.sleep(200)
map.put(id, map.get(id) + num )
}
finally {
$reentrantlock.writeLock().unlock()
}
}
}
@WithReadLock
和 @WithWriteLock
都支持指定替代锁对象。在这种情况下,用户必须声明引用字段,如下面的替代方法所示
import groovy.transform.WithReadLock
import groovy.transform.WithWriteLock
import java.util.concurrent.locks.ReentrantReadWriteLock
class Counters {
public final Map<String,Integer> map = [:].withDefault { 0 }
private final ReentrantReadWriteLock customLock = new ReentrantReadWriteLock()
@WithReadLock('customLock')
int get(String id) {
map.get(id)
}
@WithWriteLock('customLock')
void add(String id, int num) {
Thread.sleep(200) // emulate long computation
map.put(id, map.get(id)+num)
}
}
有关详细信息,
-
请参阅 groovy.transform.WithReadLock 的 Javadoc
-
请参阅 groovy.transform.WithWriteLock 的 Javadoc
2.1.5. 更轻松的克隆和外部化
Groovy 提供了两个注释,旨在分别简化 Cloneable
和 Externalizable
接口的实现,分别命名为 @AutoClone
和 @AutoExternalize
。
@groovy.transform.AutoClone
@AutoClone
注释旨在使用各种策略实现 @java.lang.Cloneable
接口,这要归功于 style
参数
-
默认的
AutoCloneStyle.CLONE
策略首先调用super.clone()
,然后对每个可克隆的属性调用clone()
-
AutoCloneStyle.SIMPLE
策略使用常规的构造函数调用并将属性从源复制到克隆 -
AutoCloneStyle.COPY_CONSTRUCTOR
策略创建并使用一个复制构造函数 -
AutoCloneStyle.SERIALIZATION
策略使用序列化(或外部化)来克隆对象
这些策略各有优缺点,在 groovy.transform.AutoClone 和 groovy.transform.AutoCloneStyle 的 Javadoc 中有讨论。
例如,以下示例
import groovy.transform.AutoClone
@AutoClone
class Book {
String isbn
String title
List<String> authors
Date publicationDate
}
等同于此
class Book implements Cloneable {
String isbn
String title
List<String> authors
Date publicationDate
public Book clone() throws CloneNotSupportedException {
Book result = super.clone()
result.authors = authors instanceof Cloneable ? (List) authors.clone() : authors
result.publicationDate = publicationDate.clone()
result
}
}
请注意,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 修饰符应用于被注释节点的元注释时,您可以混合使用@Final
,例如。
@AnnotationCollector([Singleton,Final]) @interface MySingleton {}
@MySingleton
class GreetingService {
String greeting(String name) { "Hello, $name!" }
}
assert GreetingService.instance.greeting('Bob') == 'Hello, Bob!'
assert Modifier.isFinal(GreetingService.modifiers)
@groovy.transform.AutoFinal
@AutoFinal
注释指示编译器在被注释节点中的许多地方自动插入 final 修饰符。如果应用于方法(或构造函数),则该方法(或构造函数)的参数将被标记为 final。如果应用于类定义,则对该类中声明的所有方法和构造函数执行相同的处理。
通常认为在方法或构造函数的主体中重新分配方法或构造函数的参数是不好的做法。通过将 final 修饰符添加到所有参数声明,您可以完全避免这种做法。一些程序员认为在所有地方添加 final 会增加样板代码的数量,并使方法签名有些嘈杂。另一种方法可能是使用代码审查流程或应用codenarc 规则来给出警告,如果观察到这种做法,但这些替代方案可能会导致质量检查期间的延迟反馈,而不是在 IDE 中或编译期间。@AutoFinal
注释旨在最大限度地提高编译器/IDE 反馈,同时保留简洁的代码,最少的样板噪声。
以下示例说明了在类级别应用注释
import groovy.transform.AutoFinal
@AutoFinal
class Person {
private String first, last
Person(String first, String last) {
this.first = first
this.last = last
}
String fullName(String separator) {
"$first$separator$last"
}
String greeting(String salutation) {
"$salutation, $first"
}
}
在这个例子中,构造函数的两个参数以及fullname
和greeting
方法的单个参数将是 final。尝试在构造函数或方法主体中修改这些参数将被编译器标记。
以下示例说明了在方法级别应用注释
class Calc {
@AutoFinal
int add(int a, int b) { a + b }
int mult(int a, int b) { a * b }
}
这里,add
方法将具有 final 参数,但mult
方法将保持不变。
@groovy.transform.AnnotationCollector
@AnnotationCollector
允许创建元注释,这些元注释在专用部分中描述。
@groovy.transform.TypeChecked
@TypeChecked
在您的 Groovy 代码上激活编译时类型检查。有关详细信息,请参阅类型检查部分。
@groovy.transform.CompileStatic
@CompileStatic
在您的 Groovy 代码上激活静态编译。有关详细信息,请参阅类型检查部分。
@groovy.transform.CompileDynamic
@CompileDynamic
禁用 Groovy 代码部分的静态编译。有关详细信息,请参阅类型检查部分。
@groovy.transform.SelfType
@SelfType
不是 AST 转换,而是一个与特征一起使用的标记接口。有关更多详细信息,请参阅特征文档。
2.1.8. Swing 模式
@groovy.beans.Bindable
@Bindable
是一个 AST 转换,它将常规属性转换为绑定属性(根据JavaBeans 规范)。@Bindable
注释可以放在属性或类上。要将类的所有属性转换为绑定属性,可以像在以下示例中一样注释该类
import groovy.beans.Bindable
@Bindable
class Person {
String name
int age
}
这等效于编写以下内容
import java.beans.PropertyChangeListener
import java.beans.PropertyChangeSupport
class Person {
final private PropertyChangeSupport this$propertyChangeSupport
String name
int age
public void addPropertyChangeListener(PropertyChangeListener listener) {
this$propertyChangeSupport.addPropertyChangeListener(listener)
}
public void addPropertyChangeListener(String name, PropertyChangeListener listener) {
this$propertyChangeSupport.addPropertyChangeListener(name, listener)
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
this$propertyChangeSupport.removePropertyChangeListener(listener)
}
public void removePropertyChangeListener(String name, PropertyChangeListener listener) {
this$propertyChangeSupport.removePropertyChangeListener(name, listener)
}
public void firePropertyChange(String name, Object oldValue, Object newValue) {
this$propertyChangeSupport.firePropertyChange(name, oldValue, newValue)
}
public PropertyChangeListener[] getPropertyChangeListeners() {
return this$propertyChangeSupport.getPropertyChangeListeners()
}
public PropertyChangeListener[] getPropertyChangeListeners(String name) {
return this$propertyChangeSupport.getPropertyChangeListeners(name)
}
}
因此,@Bindable
从您的类中删除了许多样板代码,极大地提高了可读性。如果注释放在单个属性上,则仅绑定该属性
import groovy.beans.Bindable
class Person {
String name
@Bindable int age
}
@groovy.beans.ListenerList
@ListenerList
AST 转换生成用于向类添加、删除和获取侦听器列表的代码,只需注释集合属性即可
import java.awt.event.ActionListener
import groovy.beans.ListenerList
class Component {
@ListenerList
List<ActionListener> listeners;
}
转换将根据列表的泛型类型生成相应的添加/删除方法。此外,它还将根据类中声明的公共方法创建fireXXX
方法
import java.awt.event.ActionEvent
import java.awt.event.ActionListener as ActionListener
import groovy.beans.ListenerList as ListenerList
public class Component {
@ListenerList
private List<ActionListener> listeners
public void addActionListener(ActionListener listener) {
if ( listener == null) {
return
}
if ( listeners == null) {
listeners = []
}
listeners.add(listener)
}
public void removeActionListener(ActionListener listener) {
if ( listener == null) {
return
}
if ( listeners == null) {
listeners = []
}
listeners.remove(listener)
}
public ActionListener[] getActionListeners() {
Object __result = []
if ( listeners != null) {
__result.addAll(listeners)
}
return (( __result ) as ActionListener[])
}
public void fireActionPerformed(ActionEvent param0) {
if ( listeners != null) {
ArrayList<ActionListener> __list = new ArrayList<ActionListener>(listeners)
for (def listener : __list ) {
listener.actionPerformed(param0)
}
}
}
}
@Bindable
支持多个选项,这些选项将让您进一步自定义转换的行为
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
name |
泛型类型名称 |
默认情况下,将附加到 add/remove/…方法的后缀是列表泛型类型的简单类名。 |
|
synchronize |
false |
如果设置为 true,则生成的方法将被同步 |
|
@groovy.beans.Vetoable
@Vetoable
注释的工作方式类似于@Bindable
,但根据 JavaBeans 规范生成受约束的属性,而不是绑定属性。该注释可以放在类上,这意味着所有属性都将转换为受约束的属性,或者放在单个属性上。例如,使用@Vetoable
注释此类
import groovy.beans.Vetoable
import java.beans.PropertyVetoException
import java.beans.VetoableChangeListener
@Vetoable
class Person {
String name
int age
}
等效于编写以下内容
public class Person {
private String name
private int age
final private java.beans.VetoableChangeSupport this$vetoableChangeSupport
public void addVetoableChangeListener(VetoableChangeListener listener) {
this$vetoableChangeSupport.addVetoableChangeListener(listener)
}
public void addVetoableChangeListener(String name, VetoableChangeListener listener) {
this$vetoableChangeSupport.addVetoableChangeListener(name, listener)
}
public void removeVetoableChangeListener(VetoableChangeListener listener) {
this$vetoableChangeSupport.removeVetoableChangeListener(listener)
}
public void removeVetoableChangeListener(String name, VetoableChangeListener listener) {
this$vetoableChangeSupport.removeVetoableChangeListener(name, listener)
}
public void fireVetoableChange(String name, Object oldValue, Object newValue) throws PropertyVetoException {
this$vetoableChangeSupport.fireVetoableChange(name, oldValue, newValue)
}
public VetoableChangeListener[] getVetoableChangeListeners() {
return this$vetoableChangeSupport.getVetoableChangeListeners()
}
public VetoableChangeListener[] getVetoableChangeListeners(String name) {
return this$vetoableChangeSupport.getVetoableChangeListeners(name)
}
public void setName(String value) throws PropertyVetoException {
this.fireVetoableChange('name', name, value)
name = value
}
public void setAge(int value) throws PropertyVetoException {
this.fireVetoableChange('age', age, value)
age = value
}
}
如果注释放在单个属性上,则仅使该属性可否决
import groovy.beans.Vetoable
class Person {
String name
@Vetoable int age
}
2.1.9. 测试帮助
@groovy.test.NotYetImplemented
@NotYetImplemented
用于反转 JUnit 3/4 测试用例的结果。它在功能尚未实现但测试已经存在的情况下特别有用。在这种情况下,预计测试会失败。用@NotYetImplemented
标记它将反转测试的结果,就像以下示例中那样
import groovy.test.GroovyTestCase
import groovy.test.NotYetImplemented
class Maths {
static int fib(int n) {
// todo: implement later
}
}
class MathsTest extends GroovyTestCase {
@NotYetImplemented
void testFib() {
def dataTable = [
1:1,
2:1,
3:2,
4:3,
5:5,
6:8,
7:13
]
dataTable.each { i, r ->
assert Maths.fib(i) == r
}
}
}
使用这种技术还有另一个优点,那就是你可以在知道如何修复错误之前编写测试用例。如果将来代码的修改通过副作用修复了错误,你就会收到通知,因为原本应该失败的测试通过了。
@groovy.transform.ASTTest
@ASTTest
是一种特殊的 AST 变换,旨在帮助调试其他 AST 变换或 Groovy 编译器本身。它允许开发人员在编译期间“探索” AST 并对 AST 进行断言,而不是对编译结果进行断言。这意味着这种 AST 变换在生成字节码之前可以访问 AST。@ASTTest
可以放置在任何可注释的节点上,并且需要两个参数
-
phase:设置
@ASTTest
将在哪个阶段触发。测试代码将在该阶段结束时对 AST 树进行操作。 -
value:在到达该阶段时,将在注释的节点上执行的代码。
编译阶段必须从以下之一中选择:org.codehaus.groovy.control.CompilePhase。但是,由于无法对同一节点使用相同的注释进行两次注释,因此你无法在两个不同的编译阶段对同一节点使用@ASTTest 。 |
value
是一个闭包表达式,它可以访问一个特殊的变量node
,该变量对应于注释的节点,以及一个辅助的lookup
方法,该方法将在这里讨论。例如,你可以像这样注释一个类节点
import groovy.transform.ASTTest
import org.codehaus.groovy.ast.ClassNode
@ASTTest(phase=CONVERSION, value={ (1)
assert node instanceof ClassNode (2)
assert node.name == 'Person' (3)
})
class Person {
}
1 | 我们正在检查 CONVERSION 阶段之后抽象语法树的状态 |
2 | node 指的是由 @ASTTest 注释的 AST 节点 |
3 | 它可以用来在编译时执行断言 |
@ASTTest
的一个有趣特性是,如果断言失败,那么编译将失败。现在假设我们想在编译时检查 AST 变换的行为。我们将在这里使用@PackageScope
,并且我们希望验证用@PackageScope
注释的属性是否成为包私有字段。为此,我们必须知道变换在哪个阶段运行,这可以在org.codehaus.groovy.transform.PackageScopeASTTransformation 中找到:语义分析。然后可以编写一个这样的测试
import groovy.transform.ASTTest
import groovy.transform.PackageScope
@ASTTest(phase=SEMANTIC_ANALYSIS, value={
def nameNode = node.properties.find { it.name == 'name' }
def ageNode = node.properties.find { it.name == 'age' }
assert nameNode
assert ageNode == null // shouldn't be a property anymore
def ageField = node.getDeclaredField 'age'
assert ageField.modifiers == 0
})
class Person {
String name
@PackageScope int age
}
@ASTTest
注释只能放置在语法允许的地方。有时,你可能想测试一个不可注释的 AST 节点的内容。在这种情况下,@ASTTest
提供了一个方便的lookup
方法,它将搜索带有特殊标记的 AST 节点
def list = lookup('anchor') (1)
Statement stmt = list[0] (2)
1 | 返回标记为 'anchor' 的 AST 节点的列表 |
2 | 由于 lookup 总是返回一个列表,因此始终需要选择要处理的元素 |
例如,假设你想测试 for 循环变量的声明类型。那么你可以像这样操作
import groovy.transform.ASTTest
import groovy.transform.PackageScope
import org.codehaus.groovy.ast.ClassHelper
import org.codehaus.groovy.ast.expr.DeclarationExpression
import org.codehaus.groovy.ast.stmt.ForStatement
class Something {
@ASTTest(phase=SEMANTIC_ANALYSIS, value={
def forLoop = lookup('anchor')[0]
assert forLoop instanceof ForStatement
def decl = forLoop.collectionExpression.expressions[0]
assert decl instanceof DeclarationExpression
assert decl.variableExpression.name == 'i'
assert decl.variableExpression.originType == ClassHelper.int_TYPE
})
void someMethod() {
int x = 1;
int y = 10;
anchor: for (int i=0; i<x+y; i++) {
println "$i"
}
}
}
@ASTTest
还在测试闭包内部公开了这些变量
-
node
如常对应于注释的节点 -
compilationUnit
可以访问当前的org.codehaus.groovy.control.CompilationUnit
-
compilePhase
返回当前的编译阶段(org.codehaus.groovy.control.CompilePhase
)
如果你没有指定phase
属性,后者会很有用。在这种情况下,闭包将在每个编译阶段后(包括)SEMANTIC_ANALYSIS
之后执行。变换的上下文在每个阶段后都会保留,让你有机会检查两个阶段之间发生了什么变化。
例如,以下是你可以如何转储在类节点上注册的 AST 变换列表
import groovy.transform.ASTTest
import groovy.transform.CompileStatic
import groovy.transform.Immutable
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.control.CompilePhase
@ASTTest(value={
System.err.println "Compile phase: $compilePhase"
ClassNode cn = node
System.err.println "Global AST xforms: ${compilationUnit?.ASTTransformationsContext?.globalTransformNames}"
CompilePhase.values().each {
def transforms = cn.getTransforms(it)
if (transforms) {
System.err.println "Ast xforms for phase $it:"
transforms.each { map ->
System.err.println(map)
}
}
}
})
@CompileStatic
@Immutable
class Foo {
}
以下是你可以如何记忆两个阶段之间的测试变量
import groovy.transform.ASTTest
import groovy.transform.ToString
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.control.CompilePhase
@ASTTest(value={
if (compilePhase == CompilePhase.INSTRUCTION_SELECTION) { (1)
println "toString() was added at phase: ${added}"
assert added == CompilePhase.CANONICALIZATION (2)
} else {
if (node.getDeclaredMethods('toString') && added == null) { (3)
added = compilePhase (4)
}
}
})
@ToString
class Foo {
String name
}
1 | 如果当前编译阶段是指令选择 |
2 | 那么我们想确保toString 在CANONICALIZATION 中被添加 |
3 | 否则,如果toString 存在并且上下文中的变量added 为空 |
4 | 那么这意味着当前编译阶段是添加toString 的阶段 |
2.1.10. Grape 处理
@groovy.lang.Grapes
Grape
是嵌入到 Groovy 中的依赖管理引擎,它依赖于几个注释,这些注释在本指南部分中进行了详细描述。
2.2. 开发 AST 变换
有两种类型的变换:全局变换和局部变换。
-
全局变换 由编译器对正在编译的代码应用,无论变换在哪里应用。实现全局变换的编译类位于添加到编译器类路径中的 JAR 文件中,并且包含服务定位器文件
META-INF/services/org.codehaus.groovy.transform.ASTTransformation
,其中包含一行,其中包含变换类的名称。变换类必须具有无参数构造函数并实现org.codehaus.groovy.transform.ASTTransformation
接口。它将在每个源代码的编译中运行,因此请确保不要创建以扩展和耗时的方式扫描所有 AST 的变换,以保持编译器的快速运行。 -
局部变换 是通过注释要变换的代码元素来局部应用的变换。为此,我们重复使用注释符号,这些注释应该实现
org.codehaus.groovy.transform.ASTTransformation
。编译器将发现它们并将变换应用于这些代码元素。
2.2.1. 编译阶段指南
Groovy AST 变换必须在九个定义的编译阶段中的一个阶段执行(org.codehaus.groovy.control.CompilePhase)。
全局变换可以在任何阶段应用,但局部变换只能在语义分析阶段或之后应用。简而言之,编译阶段如下
-
初始化:打开源文件并配置环境
-
解析:使用语法生成表示源代码的令牌树
-
转换:从令牌树创建抽象语法树 (AST)。
-
语义分析:执行语法无法检查的一致性和有效性检查,并解析类。
-
规范化:完成构建 AST
-
指令选择:选择指令集,例如 Java 6 或 Java 7 字节码级别
-
类生成:在内存中创建类的字节码
-
输出:将二进制输出写入文件系统
-
最终化:执行任何最后的清理
一般来说,在后续阶段可以获得更多类型信息。如果你的变换涉及读取 AST,那么信息更丰富的后续阶段可能是一个不错的选择。如果你的变换涉及写入 AST,那么树更稀疏的早期阶段可能更方便。
2.2.2. 局部变换
局部 AST 变换与它们应用的上下文相关。在大多数情况下,上下文由一个注释定义,该注释将定义变换的范围。例如,注释一个字段意味着变换应用于该字段,而注释类意味着变换应用于整个类。
作为一个简单的例子,假设你想要编写一个@WithLogging
变换,它将在方法调用开始和结束时添加控制台消息。因此,以下“Hello World”示例实际上会打印“Hello World”,以及一个开始和停止消息
@WithLogging
def greet() {
println "Hello World"
}
greet()
局部 AST 变换是一种简单的方法。它需要两件事
-
@WithLogging
注释的定义 -
org.codehaus.groovy.transform.ASTTransformation
的实现,它将日志表达式添加到方法中
ASTTransformation
是一个回调,它让你可以访问org.codehaus.groovy.control.SourceUnit,通过它你可以获得对org.codehaus.groovy.ast.ModuleNode(AST)的引用。
AST(抽象语法树)是一个树结构,它主要由org.codehaus.groovy.ast.expr.Expression(表达式)或org.codehaus.groovy.ast.expr.Statement(语句)组成。学习 AST 的一个简单方法是在调试器中探索它。一旦你有了 AST,你就可以分析它以了解有关代码的信息或重写它以添加新功能。
局部变换注释很简单。以下是@WithLogging
注释
import org.codehaus.groovy.transform.GroovyASTTransformationClass
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.METHOD])
@GroovyASTTransformationClass(["gep.WithLoggingASTTransformation"])
public @interface WithLogging {
}
注释保留可以是SOURCE
,因为你不需要在之后使用该注释。这里的元素类型是METHOD
,@WithLogging
因为注释适用于方法。
但最重要的部分是@GroovyASTTransformationClass
注释。它将@WithLogging
注释链接到你要编写的ASTTransformation
类。gep.WithLoggingASTTransformation
是我们将要编写的ASTTransformation
的完全限定类名。这一行将注释连接到变换。
有了它,Groovy 编译器将在源代码单元中找到@WithLogging
时调用gep.WithLoggingASTTransformation
。在运行示例脚本时,在 IDE 中设置的LoggingASTTransformation
中的任何断点现在都会命中。
ASTTransformation
类稍微复杂一些。以下是一个非常简单、非常幼稚的变换,用于为@WithLogging
添加方法开始和停止消息
@CompileStatic (1)
@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS) (2)
class WithLoggingASTTransformation implements ASTTransformation { (3)
@Override
void visit(ASTNode[] nodes, SourceUnit sourceUnit) { (4)
MethodNode method = (MethodNode) nodes[1] (5)
def startMessage = createPrintlnAst("Starting $method.name") (6)
def endMessage = createPrintlnAst("Ending $method.name") (7)
def existingStatements = ((BlockStatement)method.code).statements (8)
existingStatements.add(0, startMessage) (9)
existingStatements.add(endMessage) (10)
}
private static Statement createPrintlnAst(String message) { (11)
new ExpressionStatement(
new MethodCallExpression(
new VariableExpression("this"),
new ConstantExpression("println"),
new ArgumentListExpression(
new ConstantExpression(message)
)
)
)
}
}
1 | 即使不是强制的,如果你用 Groovy 编写 AST 变换,强烈建议使用CompileStatic ,因为它将提高编译器的性能。 |
2 | 使用org.codehaus.groovy.transform.GroovyASTTransformation 进行注释,以告知变换需要在哪个编译阶段运行。这里是在语义分析阶段。 |
3 | 实现ASTTransformation 接口 |
4 | 它只有一个visit 方法 |
5 | nodes 参数是一个包含两个 AST 节点的数组,其中第一个是注释节点(@WithLogging ),第二个是被注释的节点(方法节点) |
6 | 创建一个语句,当我们进入方法时打印一条消息 |
7 | 创建一个语句,当我们退出方法时打印一条消息 |
8 | 获取方法体,在本例中是一个BlockStatement |
9 | 在现有代码的第一个语句之前添加进入方法的消息 |
10 | 在现有代码的最后一个语句之后附加退出方法的消息 |
11 | 创建一个ExpressionStatement ,它封装了一个对应于this.println("message") 的MethodCallExpression |
重要的是要注意,为了简明起见,在这个例子中,我们没有进行必要的检查,例如检查被注释的节点是否真的是一个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 ,它封装了一个对应于this.println("message") 的MethodCallExpression |
2.2.4. AST API 指南
AbstractASTTransformation
虽然您已经看到可以直接实现ASTTransformation
接口,但在几乎所有情况下,您都不会这样做,而是会扩展org.codehaus.groovy.transform.AbstractASTTransformation类。此类提供了一些实用方法,可以使 AST 转换更容易编写。Groovy 中包含的几乎所有 AST 转换都扩展了此类。
ClassCodeExpressionTransformer
能够将一个表达式转换为另一个表达式是一个常见的用例。Groovy 提供了一个类,可以轻松地做到这一点:org.codehaus.groovy.ast.ClassCodeExpressionTransformer
为了说明这一点,让我们创建一个@Shout
转换,它将方法调用参数中的所有String
常量转换为其大写版本。例如
@Shout
def greet() {
println "Hello World"
}
greet()
应该打印
HELLO WORLD
然后,转换的代码可以使用ClassCodeExpressionTransformer
来简化此过程
@CompileStatic
@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS)
class ShoutASTTransformation implements ASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
ClassCodeExpressionTransformer trn = new ClassCodeExpressionTransformer() { (1)
private boolean inArgList = false
@Override
protected SourceUnit getSourceUnit() {
sourceUnit (2)
}
@Override
Expression transform(final Expression exp) {
if (exp instanceof ArgumentListExpression) {
inArgList = true
} else if (inArgList &&
exp instanceof ConstantExpression && exp.value instanceof String) {
return new ConstantExpression(exp.value.toUpperCase()) (3)
}
def trn = super.transform(exp)
inArgList = false
trn
}
}
trn.visitMethod((MethodNode)nodes[1]) (4)
}
}
1 | 在内部,转换将创建一个ClassCodeExpressionTransformer |
2 | 转换器需要返回源代码单元 |
3 | 如果在参数列表中检测到类型为字符串的常量表达式,则将其转换为其大写版本 |
4 | 在被注解的方法上调用转换器 |
AST 节点
编写 AST 转换需要深入了解 Groovy 内部 API。特别是,它需要了解 AST 类。由于这些类是内部的,因此 API 在将来可能会发生变化,这意味着您的转换可能会中断。尽管有这样的警告,但 AST 一直非常稳定,这种情况很少发生。 |
抽象语法树的类属于org.codehaus.groovy.ast
包。建议读者使用 Groovy 控制台,特别是 AST 浏览器工具,以了解这些类。学习的另一个资源是AST Builder测试套件。
2.2.5. 宏
简介
在 2.5.0 版本之前,在开发 AST 转换时,开发人员应该深入了解编译器如何构建 AST(抽象语法树),以便了解如何在编译时添加新的表达式或语句。
虽然使用org.codehaus.groovy.ast.tool.GeneralUtils
静态方法可以减轻创建表达式和语句的负担,但这仍然是直接编写这些 AST 节点的一种低级方式。我们需要一些东西来让我们从直接编写 AST 中抽象出来,这正是 Groovy 宏的用武之地。它们允许您在编译期间直接添加代码,而无需将您脑海中的代码翻译成与org.codehaus.groovy.ast.*
节点相关的类。
语句和表达式
让我们看一个例子,让我们创建一个局部 AST 转换:@AddMessageMethod
。当应用于给定类时,它将向该类添加一个名为getMessage
的新方法。该方法将返回“42”。该注解非常简单
@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.TYPE])
@GroovyASTTransformationClass(["metaprogramming.AddMethodASTTransformation"])
@interface AddMethod { }
如果没有使用宏,AST 转换会是什么样?就像这样
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class AddMethodASTTransformation extends AbstractASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
ClassNode classNode = (ClassNode) nodes[1]
ReturnStatement code =
new ReturnStatement( (1)
new ConstantExpression("42")) (2)
MethodNode methodNode =
new MethodNode(
"getMessage",
ACC_PUBLIC,
ClassHelper.make(String),
[] as Parameter[],
[] as ClassNode[],
code) (3)
classNode.addMethod(methodNode) (4)
}
}
1 | 创建一个返回语句 |
2 | 创建一个常量表达式“42” |
3 | 将代码添加到新方法中 |
4 | 将新方法添加到被注解的类中 |
如果您不习惯 AST API,这绝对不像您脑海中的代码。现在,看看之前代码如何使用宏来简化。
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class AddMethodWithMacrosASTTransformation extends AbstractASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
ClassNode classNode = (ClassNode) nodes[1]
ReturnStatement simplestCode = macro { return "42" } (1)
MethodNode methodNode =
new MethodNode(
"getMessage",
ACC_PUBLIC,
ClassHelper.make(String),
[] as Parameter[],
[] as ClassNode[],
simplestCode) (2)
classNode.addMethod(methodNode) (3)
}
}
1 | 更简单。您希望添加一个返回“42”的返回语句,这正是您在macro 实用方法中看到的内容。您的普通代码将被自动转换为org.codehaus.groovy.ast.stmt.ReturnStatement |
2 | 将返回语句添加到新方法中 |
3 | 将新代码添加到被注解的类中 |
虽然macro
方法在此示例中用于创建**语句**,但macro
方法也可以用于创建**表达式**,这取决于您使用的macro
签名
-
macro(Closure)
:使用闭包内的代码创建一个给定的语句。 -
macro(Boolean,Closure)
:如果为**true**,则将闭包内的表达式包装到一个语句中,如果为**false**,则返回一个表达式 -
macro(CompilePhase, Closure)
:在特定编译阶段使用闭包内的代码创建一个给定的语句 -
macro(CompilePhase, Boolean, Closure)
:在特定编译阶段创建语句或表达式(true == 语句,false == 表达式)。
所有这些签名都可以在org.codehaus.groovy.macro.runtime.MacroGroovyMethods 中找到 |
有时我们可能只对创建给定的表达式感兴趣,而不是整个语句,为了做到这一点,我们应该使用带有布尔参数的任何macro
调用
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class AddGetTwoASTTransformation extends AbstractASTTransformation {
BinaryExpression onePlusOne() {
return macro(false) { 1 + 1 } (1)
}
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
ClassNode classNode = nodes[1]
BinaryExpression expression = onePlusOne() (2)
ReturnStatement returnStatement = GeneralUtils.returnS(expression) (3)
MethodNode methodNode =
new MethodNode("getTwo",
ACC_PUBLIC,
ClassHelper.Integer_TYPE,
[] as Parameter[],
[] as ClassNode[],
returnStatement (4)
)
classNode.addMethod(methodNode) (5)
}
}
1 | 我们告诉宏不要将表达式包装到一个语句中,我们只对表达式感兴趣 |
2 | 分配表达式 |
3 | 使用GeneralUtils 中的方法创建一个ReturnStatement ,并返回表达式 |
4 | 将代码添加到新方法中 |
5 | 将方法添加到类中 |
变量替换
宏很棒,但是如果我们的宏无法接收参数或解析周围变量,我们就无法创建任何有用的或可重用的东西。
在以下示例中,我们创建了一个 AST 转换@MD5
,当应用于给定的 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 | 如果使用标准包之外的类,我们应该添加任何需要的导入或使用限定名称。当使用给定静态方法的限定名称时,您需要确保它在正确的编译阶段被解析。在这种特殊情况下,我们指示宏在语义分析阶段解析它,这是第一个具有类型信息的编译阶段。 |
3 | 为了替换宏内的任何expression ,我们需要使用$v 方法。$v 接收一个闭包作为参数,并且闭包只允许替换表达式,这意味着继承自org.codehaus.groovy.ast.expr.Expression 的类。 |
MacroClass
正如我们之前提到的,macro
方法只能生成statements
和expressions
。但如果我们想要生成其他类型的节点,例如方法、字段等呢?
org.codehaus.groovy.macro.transform.MacroClass
可用于在我们的转换中创建**类**(ClassNode 实例),就像我们之前使用macro
方法创建语句和表达式一样。
下一个示例是一个局部转换@Statistics
。当应用于给定类时,它将添加两个方法getMethodCount()和getFieldCount(),分别返回类中包含的方法和字段数量。以下是标记注解。
@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.TYPE])
@GroovyASTTransformationClass(["metaprogramming.StatisticsASTTransformation"])
@interface Statistics {}
以及 AST 转换
@CompileStatic
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class StatisticsASTTransformation extends AbstractASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
ClassNode classNode = (ClassNode) nodes[1]
ClassNode templateClass = buildTemplateClass(classNode) (1)
templateClass.methods.each { MethodNode node -> (2)
classNode.addMethod(node)
}
}
@CompileDynamic
ClassNode buildTemplateClass(ClassNode reference) { (3)
def methodCount = constX(reference.methods.size()) (4)
def fieldCount = constX(reference.fields.size()) (5)
return new MacroClass() {
class Statistics {
java.lang.Integer getMethodCount() { (6)
return $v { methodCount }
}
java.lang.Integer getFieldCount() { (7)
return $v { fieldCount }
}
}
}
}
}
1 | 创建一个模板类 |
2 | 将模板类方法添加到被注解的类中 |
3 | 传递引用类 |
4 | 提取引用类方法计数值表达式 |
5 | 提取引用类字段计数值表达式 |
6 | 使用引用的方法计数值表达式构建getMethodCount()方法 |
7 | 使用引用的字段计数值表达式构建getFieldCount()方法 |
基本上,我们创建了Statistics类作为模板,以避免编写低级 AST API,然后我们将模板类中创建的方法复制到它们的最终目的地。
MacroClass 实现中的类型应该在内部解析,这就是为什么我们必须编写java.lang.Integer 而不是简单地编写Integer 的原因。 |
请注意,我们正在使用@CompileDynamic 。这是因为我们使用MacroClass 的方式就像我们实际上正在实现它一样。因此,如果您使用的是@CompileStatic ,它会抱怨,因为抽象类的实现不能是另一个不同的类。 |
@Macro 方法
您已经看到,使用 macro
可以节省大量工作,但您可能想知道该方法来自哪里。您没有声明它或静态导入它。您可以将其视为一个特殊的全局方法(或者如果您愿意,每个 Object
上的方法)。这与 println
扩展方法的定义类似。但与 println
不同的是,println
会在编译过程的后期被选择执行,而 macro
扩展是在编译过程的早期完成的。通过使用 @Macro
注解来注解一个 macro
方法的定义,并使用类似的机制来扩展模块,从而将 macro
声明为在此早期扩展中可用的方法之一。此类方法被称为 *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
文件将其注册为扩展模块。
现在,假设该类和元信息文件位于您的类路径上,您可以以下列方式使用 macro 方法
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 变换的分步教程,您可以查看 此研讨会。