【Groovy翻译-进阶篇】运行时元编程与编译时元编程(上)

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 中的地位。GroovyObjectgroovy.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'
  1. 除了 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
}
  1. TimeCategoryInteger 添加了方法
  2. TimeCategoryDate 添加了方法

use 方法接受类别类作为第一个参数,接受一个闭包代码块作为第二个参数。在 Closure 内部,可以访问类别方法。从上面的例子可以看出,即使是 java.lang.Integerjava.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 的自定义实现来替换它。通常,你会想继承现有的某个元类,比如 MetaClassImplDelegatingMetaClassExpandoMetaClassProxyMetaClass;否则你需要实现完整的方法查找逻辑。在使用一个新的元类实例之前,你应该调用 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 提供了以下方法:

  • getMetaMethod
  • hasMetaMethod
  • getMetaProperty
  • hasMetaProperty

为什么不能直接用反射?嗯,因为 Groovy 与众不同,它有「真实」的方法,也有只在运行时才可用的方法。这些方法有时(但不总是)以 MetaMethod 表示。MetaMethod 告诉你运行时有哪些方法可用,从而你的代码可以据此做出调整。

这在覆写 invokeMethodgetProperty 和/或 setProperty 时特别有用。

GroovyObject 方法

ExpandoMetaClass 的另一个特性是,它允许覆写 invokeMethodgetPropertysetProperty 方法,这些方法都可以在 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 上的方法,无论它是在运行时还是在编译时添加的。

同样的逻辑可以用来覆写 setPropertygetProperty

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 实例。如果它存在,就调用 MetaPropertygetProperty 方法,并传入 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
        }
    }
}
  1. 扩展类
  2. 静态方法的第一个参数对应消息的接收者,也就是被扩展的实例

然后,在声明了你的扩展类之后,你可以这样调用它:

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!'
    }
}
  1. 静态扩展类
  2. 静态方法的第一个参数对应被扩展的类,并且是未使用

在这种情况下,你可以直接在 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 上被找到,那么类型检查器就会感知到这些扩展方法,在你调用它们时不会报错。它也兼容静态编译。

相关推荐
CHHH_HHH1 小时前
【Linux系统篇】深入解析Linux文件系统:从磁盘寻址到软硬链接
linux·服务器·开发语言·后端·ubuntu
星火10241 小时前
【Groovy翻译-进阶篇】运行时元编程与编译时元编程(下)
后端·groovy
用户106911794101 小时前
SpringSercuirty整合jwt
后端
岁月如歌77861 小时前
一次讲透 Redis 缓存击穿、穿透、雪崩,从原理到实战解决方案
java·后端·架构
IT_陈寒1 小时前
Vue的v-for为啥把我的渲染顺序搞乱套了?
前端·人工智能·后端
用户106911794101 小时前
SpringSecuirty自定义接口或者资源权限
后端
天涯明月19932 小时前
SGLang深度研究报告
人工智能·后端·推理·sglang
0xBADCODE2 小时前
Flask SSTI读SECRET_KEY+伪造Session:税务系统渗透全流程
前端·后端·python·安全·web安全·网络安全·flask
程序员爱钓鱼2 小时前
Rust where详解:让泛型与Trait约束更加清晰
前端·后端·rust