Groovy 语言支持两种风格的元编程:运行时元编程和编译时元编程。前者允许在运行时修改类模型以及程序的行为,而后者只发生在编译期。二者各有优劣,我们将在本节中详细说明。
1. 运行时元编程
借助运行时元编程,我们可以把「拦截、注入甚至合成类和接口的方法」这一决定推迟到运行时进行。要深入理解 Groovy 的元对象协议(Meta-Object Protocol,MOP),我们需要理解 Groovy 对象以及 Groovy 的方法处理机制。
在 Groovy 中,我们会与三种对象打交道:POJO、POGO 和 Groovy 拦截器(Groovy Interceptor)。Groovy 支持对所有这些类型的对象进行元编程,但方式各不相同。
- POJO --- 一个普通的 Java 对象,其类可以用 Java 或任何其他面向 JVM 的语言编写。
- POGO --- 一个用 Groovy 编写的 Groovy 对象。它继承自
java.lang.Object,并且默认实现了 groovy.lang.GroovyObject 接口。 - Groovy 拦截器 --- 一个实现了 groovy.lang.GroovyInterceptable 接口的 Groovy 对象,具备方法拦截能力,这一能力将在 GroovyInterceptable 一节中讨论。
对于每一次方法调用,Groovy 都会检查该对象是 POJO 还是 POGO。对于 POJO,Groovy 会从 groovy.lang.MetaClassRegistry 获取其 MetaClass,并把方法调用委托给它。对于 POGO,Groovy 会多走几个步骤,如下图所示:

1.1. GroovyObject 接口
groovy.lang.GroovyObject 在 Groovy 中的地位,就好比 Object 类在 Java 中的地位。GroovyObject 在 groovy.lang.GroovyObjectSupport 类中有默认实现,它负责把调用转发给 groovy.lang.MetaClass 对象。GroovyObject 的源码如下所示:
java
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() 方法的简单例子:
groovy
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() 方法来拦截。下面是一个简单的例子:
groovy
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'
- 除了
field3之外,把请求转发给所有属性的 getter。
你可以通过覆写 setProperty() 方法来拦截对属性的写访问:
groovy
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 接口实现,并将其赋给对象,从而改变拦截机制:
groovy
// getMetaclass
someObject.metaClass
// setMetaClass
someObject.metaClass = new OwnMetaClassImplementation()
注意: 你可以在 GroovyInterceptable 主题中找到另一个例子。
1.2. get/setAttribute
这个功能与 MetaClass 实现相关。在默认实现中,你可以在不调用其 getter 和 setter 的情况下访问字段。下面的例子演示了这种方法:
groovy
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'
groovy
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 的不同之处在于,它只在方法分派失败时被调用------也就是针对给定的方法名和/或给定的参数找不到任何方法时:
groovy
class Foo {
def methodMissing(String name, def args) {
return "this is me"
}
}
assert new Foo().someUnknownMethod(42l) == 'this is me'
通常,在使用 methodMissing 时,可以把结果缓存起来,以便下次调用同一方法时使用。
例如,考虑 GORM 中的动态查找器(dynamic finders)。它们就是基于 methodMissing 实现的。代码大致如下:
groovy
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 参数:
groovy
class Foo {
def propertyMissing(String name) { name }
}
assert new Foo().boo == 'boo'
只有当 Groovy 运行时找不到给定属性的 getter 方法时,才会调用 propertyMissing(String) 方法。
对于 setter 方法,可以添加第二个 propertyMissing 定义,它额外接受一个值参数:
groovy
class Foo {
def storage = [:]
def propertyMissing(String name, value) { storage[name] = value }
def propertyMissing(String name) { storage[name] }
}
def f = new Foo()
f.foo = "bar"
assert f.foo == "bar"
与 methodMissing 一样,最佳实践是在运行时动态注册新属性,以提高整体查找性能。
1.5. static methodMissing
methodMissing 方法的静态变体可以通过 ExpandoMetaClass 添加,也可以在类级别通过 $static_methodMissing 方法来实现。
groovy
class Foo {
static def $static_methodMissing(String name, Object args) {
return "Missing static method name is $name"
}
}
assert Foo.bar() == 'Missing static method name is bar'
1.6. static propertyMissing
propertyMissing 方法的静态变体可以通过 ExpandoMetaClass 添加,也可以在类级别通过 $static_propertyMissing 方法来实现。
groovy
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 接口是一个标记接口(marker interface),它继承自 GroovyObject,用于通知 Groovy 运行时:所有方法都应该通过 Groovy 运行时的方法分派机制进行拦截。
java
package groovy.lang;
public interface GroovyInterceptable extends GroovyObject {
}
当一个 Groovy 对象实现了 GroovyInterceptable 接口时,任何方法调用都会触发它的 invokeMethod()。下面你可以看到一个这种类型对象的简单例子:
groovy
class Interception implements GroovyInterceptable {
def definedMethod() { }
def invokeMethod(String name, Object args) {
'invokedMethod'
}
}
下面这段代码是一个测试,它表明对已存在方法和不存在方法的调用都会返回相同的值。
groovy
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 都有效,如下例所示:
groovy
class InterceptionThroughMetaClassTest extends GroovyTestCase {
void testPOJOMetaClassInterception() {
String invoking = 'ha'
invoking.metaClass.invokeMethod = { String name, Object args ->
'invoked'
}
assert invoking.length() == 'invoked'
assert invoking.someMethod() == 'invoked'
}
void testPOGOMetaClassInterception() {
Entity entity = new Entity('Hello')
entity.metaClass.invokeMethod = { String name, Object args ->
'invoked'
}
assert entity.build(new Object()) == 'invoked'
assert entity.someMethod() == 'invoked'
}
}
注意 : 关于
MetaClass的更多信息可以在 MetaClasses 一节中找到。
1.8. Categories(类别)
有些情况下,如果我们能给一个不受我们控制的类添加额外的方法,会非常有用。为了实现这种能力,Groovy 实现了一个从 Objective-C 借鉴而来的特性,叫作 Categories(类别)。
类别是通过所谓的 类别类(category class)实现的。类别类的特殊之处在于,它必须满足一些预定义规则,才能定义扩展方法。
系统中内置了几个类别,用于为类添加功能,使它们在 Groovy 环境中更好用:
类别类默认是不启用的。要使用类别类中定义的方法,必须应用 GDK 提供的、在每个 Groovy 对象实例内部都可用的、作用域受限的 use 方法:
groovy
use(TimeCategory) {
println 1.minute.from.now (1)
println 10.hours.ago
def someDate = new Date() (2)
println someDate - 3.months
}
TimeCategory为Integer添加了方法TimeCategory为Date添加了方法
use 方法接受类别类作为第一个参数,接受一个闭包代码块作为第二个参数。在 Closure 内部,可以访问类别方法。从上面的例子可以看出,即使是 java.lang.Integer 或 java.util.Date 这样的 JDK 类,也可以用用户自定义的方法来增强。
类别不必直接暴露给用户代码,下面的写法也可以:
groovy
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 代码块中的类上:
groovy
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 注解,它可以在编译期把被注解的类转换成类别类。
groovy
class Distance {
def number
String toString() { "${number}m" }
}
@Category(Number)
class NumberCategory {
Distance getMeters() {
new Distance(number: this)
}
}
use(NumberCategory) {
assert 42.meters.toString() == '42m'
}
应用 @Category 注解的好处是,可以使用实例方法,而无需把目标类型作为第一个参数。目标类型类改为作为注解的参数传入。
注意 : 关于
@Category,在编译时元编程一节中有一个专门的段落。
1.9. Metaclasses(元类)
如前所述,元类在方法解析中扮演着核心角色。对于 Groovy 代码中的每一次方法调用,Groovy 都会为给定对象找到 MetaClass,并通过 groovy.lang.MetaClass#invokeMethod(java.lang.Class,java.lang.Object,java.lang.String,java.lang.Object,boolean,boolean) 把方法解析委托给元类。不要把它与 groovy.lang.GroovyObject#invokeMethod(java.lang.String,java.lang.Object) 混淆,后者是元类最终可能会调用的一个方法。
1.9.1. 默认元类 MetaClassImpl
默认情况下,对象会获得一个 MetaClassImpl 实例,它实现了默认的方法查找。这种方法查找包括在对象类中查找方法(「常规」方法),但如果这样找不到方法,就会转而调用 methodMissing,最终调用 groovy.lang.GroovyObject#invokeMethod(java.lang.String,java.lang.Object)。
groovy
class Foo {}
def f = new Foo()
assert f.metaClass =~ /MetaClassImpl/
1.9.2. 自定义元类
你可以更改任意对象或类的元类,用 groovy.lang.MetaClass 的自定义实现来替换它。通常,你会想继承现有的某个元类,比如 MetaClassImpl、DelegatingMetaClass、ExpandoMetaClass 或 ProxyMetaClass;否则你需要实现完整的方法查找逻辑。在使用一个新的元类实例之前,你应该调用 groovy.lang.MetaClass#initialize(),否则元类可能不会按预期工作。
Delegating 元类
如果你只需要装饰一个现有的元类,DelegatingMetaClass 可以简化这种场景。旧的元类实现仍然可以通过 super 访问,这样就能方便地对输入做预处理、路由到其他方法以及对输出做后处理。
groovy
class Foo { def bar() { "bar" } }
class MyFooMetaClass extends DelegatingMetaClass {
MyFooMetaClass(MetaClass metaClass) { super(metaClass) }
MyFooMetaClass(Class theClass) { super(theClass) }
Object invokeMethod(Object object, String methodName, Object[] args) {
def result = super.invokeMethod(object,methodName.toLowerCase(), args)
result.toUpperCase();
}
}
def mc = new MyFooMetaClass(Foo.metaClass)
mc.initialize()
Foo.metaClass = mc
def f = new Foo()
assert f.BAR() == "BAR" // the new metaclass routes .BAR() to .bar() and uppercases the result
Magic package(魔法包)
可以通过给元类一个特殊构造的(魔法)类名和包名,在启动时更改元类。要更改 java.lang.Integer 的元类,只需在 classpath 中放入一个 groovy.runtime.metaclass.java.lang.IntegerMetaClass 类即可。例如,在使用框架时,如果你想在框架执行你的代码之前完成元类更改,这就很有用。魔法包的一般形式是 groovy.runtime.metaclass.[package].[class]MetaClass。在下面的例子中,[package] 是 java.lang,[class] 是 Integer:
groovy
// 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。下面的例子将使用这个新的元类:
groovy
// File testInteger.groovy
def i = 10
assert i.isBiggerThan5()
assert !i.isBiggerThan15()
println i.isBiggerThan5()
用 groovy -cp . testInteger.groovy 运行该文件,IntegerMetaClass 就会出现在 classpath 中,因此它会成为 java.lang.Integer 的元类,拦截对 isBiggerThan*() 方法的调用。
1.9.3. 每个实例的元类
你可以分别更改单个对象的元类,因此同一类的多个对象可以拥有不同的元类。
groovy
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。它的特殊之处在于,它允许通过简洁的闭包语法动态地添加或修改方法、构造器、属性,甚至静态方法。
在模拟(mocking)或打桩(stubbing)场景中,应用这些修改尤其有用,如测试指南中所示。
Groovy 为每个 java.lang.Class 都提供了一个特殊的 metaClass 属性,它会给你一个 ExpandoMetaClass 实例的引用。然后就可以用这个实例来添加方法或修改已有方法的行为。
注意 : 默认情况下,
ExpandoMetaClass不执行继承。要启用继承,你必须在应用启动之前(例如在 main 方法或 servlet 引导中)调用ExpandoMetaClass#enableGlobally()。
下面几节将详细介绍 ExpandoMetaClass 如何在各种场景中使用。
Methods(方法)
一旦通过调用 metaClass 属性访问到 ExpandoMetaClass,就可以使用左移 << 或 = 运算符来添加方法。
注意 : 注意,左移运算符用于追加 新方法。如果类或接口(包括从父类和父接口继承的方法,但不包括运行时添加到
metaClass的方法)声明了一个名称和参数类型都相同的公有方法,就会抛出异常。如果你想替换 类或接口声明的方法,可以使用=运算符。
这些运算符作用于 metaClass 上不存在的属性,并传入一个 Closure 代码块的实例。
groovy
class Book {
String title
}
Book.metaClass.titleInUpperCase << {-> title.toUpperCase() }
def b = new Book(title:"The Stand")
assert "THE STAND" == b.titleInUpperCase()
上面的例子展示了如何通过访问 metaClass 属性,并用 << 或 = 运算符赋值一个 Closure 代码块,从而向类添加新方法。Closure 的参数被解释为方法参数。无参数的方法可以通过使用 {→ ...} 语法来添加。
Properties(属性)
ExpandoMetaClass 支持两种添加或覆写属性的机制。
首先,它支持通过简单地向 metaClass 的属性赋值来声明一个可变属性:
groovy
class Book {
String title
}
Book.metaClass.author = "Stephen King"
def b = new Book()
assert "Stephen King" == b.author
另一种方法是使用添加实例方法的标准机制来添加 getter 和/或 setter 方法。
groovy
class Book {
String title
}
Book.metaClass.getAuthor << {-> "Stephen King" }
def b = new Book()
assert "Stephen King" == b.author
在上面的源码例子中,属性由闭包决定,并且是一个只读属性。也可以添加一个对应的 setter 方法,但那样的话,属性值需要被存储起来供后续使用。可以如下例所示来实现。
groovy
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 的某些场景就是这样做的)。
Constructors(构造器)
可以通过使用一个特殊的 constructor 属性来添加构造器。既可以使用 << 也可以使用 = 运算符来赋值一个 Closure 代码块。当代码在运行时执行时,Closure 的参数将成为构造器参数。
groovy
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'
注意: 不过,添加构造器时要小心,因为这很容易陷入栈溢出(stack overflow)的麻烦。
Static Methods(静态方法)
静态方法可以使用与实例方法相同的技术来添加,只需在方法名之前加上 static 限定符。
groovy
class Book {
String title
}
Book.metaClass.static.create << { String title -> new Book(title:title) }
def b = Book.create("The Stand")
Borrowing Methods(借用方法)
通过 ExpandoMetaClass,可以使用 Groovy 的方法指针语法从其他类借用方法。
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()
Dynamic Method Names(动态方法名)
由于 Groovy 允许你使用字符串作为属性名,这反过来又允许你在运行时动态创建方法和属性名。要创建具有动态名称的方法,只需使用「把属性名作为字符串引用」这一语言特性即可。
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 应用框架中找到。「动态编解码器(dynamic codecs)」的概念就是通过动态方法名实现的。
HTMLCodec 类
groovy
class HTMLCodec {
static encode = { theTarget ->
HtmlUtils.htmlEscape(theTarget.toString())
}
static decode = { theTarget ->
HtmlUtils.htmlUnescape(theTarget.toString())
}
}
上面的例子展示了一个编解码器实现。Grails 自带各种编解码器实现,每个都定义在一个单独的类中。在运行时,应用 classpath 中会有多个编解码器类。在应用启动时,框架会向某些元类添加一个 encodeXXX 方法和一个 decodeXXX 方法,其中 XXX 是编解码器类名的第一部分(例如 encodeHTML)。下面的 Groovy 伪代码展示了这种机制:
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()
Runtime Discovery(运行时发现)
在运行时,通常很有用的是:知道在方法执行时存在哪些其他方法或属性。截至撰写本文时,ExpandoMetaClass 提供了以下方法:
getMetaMethodhasMetaMethodgetMetaPropertyhasMetaProperty
为什么不能直接用反射?嗯,因为 Groovy 与众不同,它有「真实」的方法,也有只在运行时才可用的方法。这些方法有时(但不总是)以 MetaMethod 表示。MetaMethod 告诉你运行时有哪些方法可用,从而你的代码可以据此做出调整。
这在覆写 invokeMethod、getProperty 和/或 setProperty 时特别有用。
GroovyObject 方法
ExpandoMetaClass 的另一个特性是,它允许覆写 invokeMethod、getProperty 和 setProperty 方法,这些方法都可以在 groovy.lang.GroovyObject 类中找到。
下面的例子展示了如何覆写 invokeMethod:
groovy
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。
groovy
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 方法,并传入 delegate。
覆写静态 invokeMethod
ExpandoMetaClass 甚至允许通过一种特殊的 invokeMethod 语法来覆写静态方法。
groovy
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 实例。
Extending Interfaces(扩展接口)
可以用 ExpandoMetaClass 向接口添加方法。不过,要这样做,必须 在应用启动之前用 ExpandoMetaClass.enableGlobally() 方法全局启用它。
groovy
List.metaClass.sizeDoubled = {-> delegate.size() * 2 }
def list = []
list << 1
list << 2
assert 4 == list.sizeDoubled()
1.10. 扩展模块(Extension modules)
1.10.1. 扩展现有类
扩展模块允许你向现有类添加新方法,包括那些已经预编译的类,比如来自 JDK 的类。这些新方法,与通过元类或类别定义的方法不同,是全局可用的。例如,当你写下:
标准的扩展方法
groovy
def file = new File(...)
def contents = file.getText('utf-8')
getText 方法在 File 类上并不存在。但是 Groovy 知道它,因为它定义在一个特殊的类 ResourceGroovyMethods 中:
ResourceGroovyMethods.java
java
public static String getText(File file, String charset) throws IOException {
return IOGroovyMethods.getText(newReader(file, charset));
}
你可能已经注意到,扩展方法是通过辅助类(其中定义了各种扩展方法)中的静态方法来定义的。getText 方法的第一个参数对应接收者(receiver),其余参数则对应扩展方法的参数。因此,这里我们是在 File 类上定义了一个名为 getText 的方法(因为第一个参数是 File 类型),它接受一个参数(编码 String)。
创建扩展模块的过程很简单:
- 像上面那样编写一个扩展类
- 编写一个模块描述符文件
然后你需要让扩展模块对 Groovy 可见,这很简单:只要让扩展模块的类和描述符在 classpath 上可用即可。这意味着你有以下选择:
- 要么把类和模块描述符直接放到 classpath 上
- 要么把扩展模块打包成一个 jar 以便复用
一个扩展模块可以向类添加两种方法:
- 实例方法(在类的实例上调用)
- 静态方法(在类本身上调用)
1.10.2. 实例方法
要向现有类添加实例方法,你需要创建一个扩展类。例如,假设你想在 Integer 上添加一个 maxRetries 方法,它接受一个闭包,并最多执行 n 次,直到不再抛出异常为止。要做到这一点,你只需要编写以下内容:
MaxRetriesExtension.groovy
groovy
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
}
}
}
- 扩展类
- 静态方法的第一个参数对应消息的接收者,也就是被扩展的实例
然后,在声明了你的扩展类之后,你可以这样调用它:
groovy
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. 静态方法
也可以向类添加静态方法。在这种情况下,静态方法需要定义在它自己的 文件中。静态扩展方法和实例扩展方法不能出现在同一个类中。
StaticStringExtension.groovy
groovy
class StaticStringExtension { (1)
static String greeting(String self) { (2)
'Hello, world!'
}
}
- 静态扩展类
- 静态方法的第一个参数对应被扩展的类,并且是未使用的
在这种情况下,你可以直接在 String 类上调用它:
groovy
assert String.greeting() == 'Hello, world!'
1.10.4. 模块描述符
为了让 Groovy 能够加载你的扩展方法,你必须声明你的扩展辅助类。你必须在 META-INF/groovy 目录下创建一个名为 org.codehaus.groovy.runtime.ExtensionModule 的文件:
org.codehaus.groovy.runtime.ExtensionModule
groovy
moduleName=Test module for specifications
moduleVersion=1.0-test
extensionClasses=support.MaxRetriesExtension
staticExtensionClasses=support.StaticStringExtension
模块描述符需要 4 个键:
- moduleName:你的模块的名称
- moduleVersion:你的模块的版本。注意,版本号仅用于检查你是否以两个不同的版本加载了同一个模块。
- extensionClasses:实例方法的扩展辅助类列表。你可以提供多个类,用逗号分隔。
- staticExtensionClasses:静态方法的扩展辅助类列表。你可以提供多个类,用逗号分隔。
注意,一个模块并不要求同时定义静态辅助类和实例辅助类,而且你可以在一个模块中添加多个类。你还可以在一个模块中扩展不同的类而不出问题。甚至可以在一个扩展类中使用不同的类,但建议按功能集把扩展方法分组到不同的类中。
1.10.5. 扩展模块与 classpath
值得注意的是,你不能使用一个与使用它的代码同时编译的扩展。这意味着,要使用某个扩展,它必须 作为已编译的类在 classpath 上可用,且要先于使用它的代码编译。通常,这意味着你不能把测试类放在与扩展类本身相同的源码单元中。由于通常测试源码与普通源码是分开的,并且在构建的另一个步骤中执行,所以这不是问题。
1.10.6. 与类型检查的兼容性
与类别不同,扩展模块与类型检查是兼容的:如果它们在 classpath 上被找到,那么类型检查器就会感知到这些扩展方法,在你调用它们时不会报错。它也兼容静态编译。