Groovy源代码分析(十一)

2021SC@SDUSC

运行时元编程(下)

ExpandoMetaClass

Groovy带有一个特殊的MetaClass,它就是ExpandoMetaClass。 它是特别的,它允许通过使用一个整洁的闭包语法动态添加或更改方法,构造函数,属性,甚至静态方法。

每个java.lang.Class由Groovy提供,并有一个特殊的metaClass属性,它将提供对ExpandoMetaClass实例的引用。 然后,此实例可用于添加方法或更改已有现有方法的行为。

默认情况下ExpandoMetaClass不执行继承。 要启用它,我们必须在应用程序启动之前调用ExpandoMetaClass#enableGlobally(),例如在main方法或servlet引导中。

以下部分详细介绍了ExpandoMetaClass如何在各种情况下使用。

方法

一旦通过调用metaClass属性访问ExpandoMetaClass,可以使用左移<<=运算符来添加方法。

注意,左移位运算符用于附加一个新方法。 如果类或接口声明了具有相同名称和参数类型的公共方法,包括继承自父类和父接口但不包括在运行时添加到metaClass的那些方法,那么将抛出异常。 如果要替换由类或接口声明的方法,可以使用=运算符。

运算符通过一个Closure代码块实例,将功能应用于metaClass的不存在的属性上。

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中的某些情况下所做的那样)。

构造函数

可以通过使用特殊的构造函数属性来添加构造函数。 可以使用<<=运算符来分配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'

当添加构造函数时要小心,因为它很容易陷入堆栈溢出问题。

静态方法

可以使用与实例方法相同的技术添加静态方法,并在方法名称之前添加静态限定符。

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允许使用Strings作为属性名,这反过来允许我们在运行时动态创建方法和属性名。 要创建具有动态名称的方法,只需使用引用属性名称的语言特性作为字符串。

class Person {
   String name = "Fred"
}

def methodName = "Bob"

Person.metaClass."changeNameTo${methodName}" = {-> delegate.name = "Bob" }

def p = new Person()

assert "Fred" == p.name

p.changeNameToBob()

assert "Bob" == p.name

相同的概念可以应用于静态方法和属性。

动态方法名称的一个应用程序可以在Grails Web应用程序框架中找到。 “动态编解码器”的概念通过使用动态方法名称来实现。

HTMLCodec 类

class HTMLCodec {
    static encode = { theTarget ->
        HtmlUtils.htmlEscape(theTarget.toString())
    }

    static decode = { theTarget ->
        HtmlUtils.htmlUnescape(theTarget.toString())
    }
}

上面的示例显示了编解码器实现。 Grails有各种编解码器实现,每个在一个类中定义。 在运行时,应用程序类路径中将有多个编解码器类。 在应用程序启动时,框架向某些元类添加encodeXXX和decodeXXX方法,其中XXX是编解码器类名称的第一部分(例如,encodeHTML)。 这种机制在下面显示的一些Groovy伪代码中:

def codecs = classes.findAll { it.name.endsWith('Codec') }

codecs.each { codec ->
    Object.metaClass."encodeAs${codec.name-'Codec'}" = { codec.newInstance().encode(delegate) }
    Object.metaClass."decodeFrom${codec.name-'Codec'}" = { codec.newInstance().decode(delegate) }
}


def html = '<html><body>hello</body></html>'

assert '<html><body>hello</body></html>' == html.encodeAsHTML()

运行时发现

在运行时,知道在执行该方法时存在什么其他方法或属性通常是有用的。 ExpandoMetaClass已经提供了以下方法:

  • getMetaMethod
  • hasMetaMethod
  • getMetaProperty
  • hasMetaProperty

你为什么不能使用反射? 因为Groovy是不同的,它有方法是“真正的”方法,同时方法只在运行时可用。 这些有时(但不总是)表示为MetaMethods。 MetaMethod告诉你在运行时可用的方法,因此你的代码可以适应。

当覆盖invokeMethodgetProperty和/或setProperty时,这是特别有用的。

GroovyObject 方法

ExpandoMetaClass的另一个特点是它允许重写方法invokeMethodgetPropertysetProperty,所有这些都可以在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。 如果方法可以找到一切都很好,它被委托。 如果不是,则返回一个虚拟值。

上一篇:Groovy


下一篇:Groovy源代码分析(十)