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告诉你在运行时可用的方法,因此你的代码可以适应。
当覆盖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
。 如果方法可以找到一切都很好,它被委托。 如果不是,则返回一个虚拟值。