7. Groovy 元对象协议:前序
编程中我们总是会听到 “元” ( Meta- ) 信息,或者 “元” 数据的一些说法,它们的通俗解释就是:描述信息的信息,或者是描述数据的数据。拿一张相片举例子,除了相片自身携带的图像信息以外,有关于它的印刷时间,拍摄时间,标签等都可以属于 “元信息”。在本篇文章中,信息本身就是 POJO / POGO 对象,而它的描述信息则是元方法 MetaMethod
和元属性 MetaProperty
,后文统称两者为元信息。
由此引申,元编程 ( Metaprogramming ),就是 “用于编写程序的程序”,简单来说,通过操纵,创建各种元信息来达到动态编程,或者是自动化编程的手段。一个例子就是:Grails/GORM 利用元编程的能力为数据查询动态合成类和方法。元编程依赖一种约定 —— 那就是元对象协议 Meta Object Protocol,简称 MOP。Groovy 的 MOP 本身很简单 —— 仅仅用一个 groovy.lang.MetaObjectProtocol
接口规范了如何获取元信息,设置元信息。我们真正的乐子在于如何基于 MOP 去做一些单靠 Java 实现不了或难以实现的功能。
在 Java 当中,有关一个类的所有信息都已经在编译期间尘埃落定。我们常说 “Java 的动态反射” 也仅限于 “动态获取已有的静态信息”,而不能在运行时修改它的类型,或者赋予它全新的行为。但如果一个类是在运行期间基于各种元信息构建 ( 拼凑 ) 出来的呢?这就完全不一样了 —— 说得通俗点:我们想让它在运行时长什么样子,它就是什么样子。更多情况下,这将由用户传入的数据决定。
本章首先介绍 MOP 最基本的内容,对于 POJO 或者 POGO,应该如何获取它们的元信息,然后在之后的章节去再介绍怎么动态操纵这些元信息。本章是学习以下内容的基础:
- 利用 MOP 动态拦截方法
- 利用 MOP 动态注入或合成方法
- 利用 MOP 设计 DSL
- 了解 Groovy 生成器
7.1 POGO & POJO,MetaClass
POJO ( Plain Old Java Object ),简单 Java 对象,代表没有额外类继承 ( 只继承于 java.lang.Object
),没有额外接口实现,也没有被其它框架侵入的 Java 对象。从广义上来讲,它指不受任何特定 Java 对象模型,约定,框架约束的 Java 对象;而正因为它不和任何详细的业务功能绑定,从狭义上来看,POJO 又可以代表那些只用于承载数据,仅设置了 setter
和 getter
方法的 Java 对象,因此 POJO 有时又会和 Entity,DTO 等概念混淆,本文的 POJO 指广义的概念。
同理,在 Groovy 中,POGO 代表了普通的 Groovy 对象,它既是 java.lang.Object
的子类,又天然地实现了 groovy.lang.GroovyObject
接口;而 POJO 在这里可以是任何其它 JVM 语言实现的。下面是 groovy.lang.GroovyObject
接口的内容:
// 来自 IDEA 的反编译内容。
public interface GroovyObject {
@Internal
default Object invokeMethod(String name, Object args) {
return this.getMetaClass().invokeMethod(this, name, args);
}
@Internal
default Object getProperty(String propertyName) {
return this.getMetaClass().getProperty(this, propertyName);
}
@Internal
default void setProperty(String propertyName, Object newValue) {
this.getMetaClass().setProperty(this, propertyName, newValue);
}
MetaClass getMetaClass();
void setMetaClass(MetaClass var1);
}
复制代码
从方法名称来看,它们都和 元类型 MetaClass
紧密相关,看样子,想要了解并驾驭 Groovy 动态编程的能力,我们得先获取到这个 MetaClass
。幸运的是,不论一个对象属于 POGO 还是 POJO,我们总通过 .metaClass
简单地提取出它们的元类型:
// 该对象是一个 Java 实现
def pojo = new POJO(10)
// 该对象是一个 Groovy 实现
def pogo = new POGO()
// 任何在 Groovy Script 中使用的类都可以使用 .metaClass ,这而与它的源 JVM 语言无关。
// 结合了之前 "统一访问原则" 的设计思想。
pogo.metaClass
pojo.metaClass
复制代码
实际上,两者的 metaClass
的获取途径并不相同:对于 POJO,Groovy 维护用于获取 MetaClass
的一个 MetaClassRegistry
。而对于 POGO ,其 .metaClass
指向接口中的 setMetaClass
方法 ( 这个语法糖在前文介绍 “统一访问原则” 时提到过 ) 。
7.2 基于 MetaClass 获取元信息
无论是 POJO 还是 POGO,一旦获取了其 MetaClass
,我们就能以类似 “Java 反射” 的形式在运行时获取这个类一切的元方法 MetaMethod
和元属性 MetaProperty
( 所以说访问修饰符在 Groovy 中 “形同虚设” )。
元方法 ( Meta Method ),包含了实例方法和静态方法,因此对应的获取方法叫 getMetaMethod()
,getStaticMetaMethod()
。如果要获取重载的方法列表,那么对应的方法则是 getMetaMethods()
和 getStaticMetaMethods()
。获取的元方法可以拿去调用。下面是一个简单的例子:
def pojo = new POJO(10)
def methodName = "yawn"
def staticMethodName = "greet"
// 获取实例方法 (元方法) 的一个 "句柄"。
def method = pojo.metaClass.getMetaMethod(methodName)
// 通过 invoke 方法绑定一个实例并调用。
// 如果调用的方法是空括号方法,那么可以只传进一个实例。
method.invoke(pojo)
// 如果获取的元方法需要参数列表,则需要以可变参数的形式提供这些参数类型 Class, 或者是对应类型的任意 Object。
// 这里存在方法重载,如果只传入 null, 那么 Groovy 会疑惑第二个参数是 Class[] 还是 Object[] 因而报错。
// null as Class[] 是避免歧义的做法。
def staticMethod = pojo.metaClass.getStaticMetaMethod(staticMethodName,null as Class[])
staticMethod.invoke(pojo)
// 下面演示了获取有参数列表的 "静态" 元方法的方式。
def staticMethodWithParam = pojo.metaClass.getStaticMetaMethod(staticMethodName,String.class)
// 静态方法不需要绑定特定对象。
staticMethodWithParam.invoke(null,"Tom")
复制代码
元属性 ( Meta Property ),包含了实例属性和静态属性,对应的获取方法叫 getMetaProperty
,getStaticMetaProperty()
。假定我们只是想对某些元信息的 “存在性” 做检查,那么可以使用 responseTo
( 检查元方法,在之前我们曾用于对 “鸭子类型” 进行检查 ) 或者是 hasProperty
( 检查元属性 ) 。
def pojo = new POJO(10)
def methodName = "SayHi"
def propertyName = "property"
println( "the pojo has the method ${methodName} ? ${pojo.respondsTo(methodName)?"yes":"no"}")
println( "the pojo has the method ${propertyName} ? ${pojo.hasProperty(propertyName)?"yes":"no"}")
复制代码
7.3 Groovy 调用方法的策略
在调用一个方法时,Groovy 会根据这个对象是 POJO 还是 POGO 而采取不同的策略:如果一个对象是 POJO,Groovy 则会通过 MetaClassRegistry
获取 MetaClass
,然后再将方法调用转发给它。换句话说,在 Groovy 中即便是想简单地调用一个 POJO 方法也会变得复杂起来,感兴趣的读者可以借助一些反编译工具去观察 ( 这可能也是 Groovy 运行效率不如 Java 的原因之一 )。同时 如果我们在 MetaClass
当中拦截或者注入了一些其它方法,那么它们的优先级都会高于 POJO 自身定义的方法。
如果对象是一个 POGO ,那么步骤可能要稍微更复杂一些,为了方便解释,这里举一个例子:
class POGO {
// POGO 的一个名为 greet 的方法,优先被调用
def greet(){
println "greet by method"
}
// POGO 的一个名为 greet 的闭包属性,由于同名方法已经存在,因此该闭包不会被优先调用
// 结合统一访问原则,以下写法会优先调用闭包: new POGO().greet, new POGO().getGreet().call()
def greet = {
println "greet by closure"
}
// 在同名方法不存在的情况下,Groovy 尝试去调用该闭包的 .call() 方法
def hi = {
println "hi by closure"
}
// 当发现方法不存在时的重载版 methodMiss() 方法
def methodMissing(String name, def args) {
println "a method $name doesn't exist."
return null
}
}
复制代码
成功调用一个 POGO 方法的条件是:要么 POGO 类内部定义了这样的方法 ( 优先 ),要么定义了同名的闭包属性 ( 其次 ) 。否则,此时 Groovy 会进行最后一次尝试:那就是尝试调用这个类定义的 invokeMethod()
方法。否则,这个调用被认为是失败了。
失败仍然分为两种情况,一种是我们主动定义 methodMissing()
方法 ( IDEA 可以通过 alt
+ Insert
快捷键生成 ) 决定如何处理,另一种让默认的 methodMissing
方法抛出异常 ( 就是我们通常见到的控制台报错 ) 。
在学习方法注入之后,我们会了解到成功调用的第三个条件:那就是 POGO 实现了一个名为 GroovyInterceptable
的标记接口,然后一切方法调用优先被路由到它的 invokeMethod()
并成功调用 ( 优先级最高 )。本文先暂时不考虑这种情况。
通过观察以下代码的运行结果,调用优先级一目了然:
// 当方法和闭包属性同名时,这种写法优先选择前者。
// greet by method
pogo.greet()
// 注意,这种写法总是获取闭包属性,如果想要调用它后面还需要跟上 .call() 。
pogo.greet
// 没有可用方法时选择同名的闭包属性并调用 call 方法,或者说使用这个闭包属性 "顶替" 不存在的同名方法。
// hi by closure
pogo.hi()
// 调用确实不存在的方法或闭包属性,会被引导至 methodMissing 方法。
// 如果自身没有实现 methodMissing 方法,那么就会抛出异常。
// a method echo doesn't exist.
pogo.echo()
复制代码
7.4 动态访问对象
我们之前已经介绍了一个动态访问对象属性的语法糖 —— 通过重载的 getAt
方法。这个属性完全可能是对象已有的,或者是我们通过操纵元属性添加的。
def pojo = new POJO(10)
println pojo["property"]
复制代码
下面介绍通过另一种方式:
println pojo."property"
复制代码
或者是通过 GString 引用的方式指代属性名:
def propertyName = "property"
println pojo."${propertyName}"
复制代码
同理,如果要动态调用方法,除了 invokeMethod()
之外,还可以写成:
def methodName = "SayHi"
// 等价于 pojo.SayHi()
println pojo."${methodName}"()
复制代码
此外,如果要查看 POGO 的所有属性 ( 包括 class
信息 ),可以通过 .properties
( .getProperties()
方法 ) 获取 Map 形式,或者是通过 .metaPropertyValues
获取 List 形式。这两个方法不是元对象协议的一部分,对 POJO 使用只能访问到 class
信息。
// 获取一个 POGO 对象的所有属性。
pogo.properties
// 由于返回值是 Map,因此可以像 k-v 对那样 "取值"。
pogo.properties["property"]
// 或者拿到 for 循环里遍历:
def name,value
(name,value) = ["",""]
def str = /property name {${-> name}},value is {${-> value}}/
pogo.properties.each {
entry ->
name = entry.key
value = entry.value
println str
}
// 获取链表形式
// getProperties() 方法本质上就是通过这个方法获取的,只不过额外包装了放入 Map 的过程。
pogo.metaPropertyValues.each {println "[${-> it.value}:${it.name}]"}
复制代码