同函数式编程类似,元编程,看上去像一门独派武学。 在 《Ruby元编程》一书中,定义:元编程是运行时操作语言构件的编程能力。其中,语言构件指模块、类、方法、变量等。常用的主要是动态创建和访问类和方法。元编程,体现了程序的动态之美。
对于 Java 系程序员来说,不大会使用 Ruby 编程, 更多会考虑 Java 的近亲 Groovy 。 本文将简要介绍 Groovy 元编程的语言特性。Groovy 元编程基于 MOP 协议。
元编程特性
轻松运行时
在 Java 中,要访问私有实例变量或方法,需要通过反射机制来实现,且细节比较繁琐。比如,需要先 setAccessible 为 true ,进行操作,然后再 setAccessible 为 false 。写一堆模板代码。
所幸,在 GroovyObject 中暴漏了一组基础 API ,可以像调用普通方法那样轻松访问私有变量或方法。 这组 API 对于所有 Groovy 对象都适用。 MetaClass 为元编程机制埋下了伏笔。
public interface GroovyObject {
Object invokeMethod(String var1, Object var2);
Object getProperty(String var1);
void setProperty(String var1, Object var2);
MetaClass getMetaClass();
void setMetaClass(MetaClass var1);
}
代码清单一: Expression.groovy
class Expression {
def field
def op
def value
def call = {
println(this)
println(owner)
println(delegate)
def v = {
println(this)
println(owner)
println(delegate)
}
v()
}
private String inner() {
"EXP[$field $op $value]"
}
def match(map) {
map[field] == value
}
def methodMissing(String name, args) {
println("name=$name, args=$args")
}
static void main(args) {
def exp = new Expression(field: "id", op:"=", value:111)
// 动态访问属性
println exp.getProperty("value")
exp.setProperty("value", 123)
def valueProp = "value"
println "exp[$valueProp] = ${exp[valueProp]}"
println "exp.\"$valueProp\" = " + exp."$valueProp"
// 轻松调用私有方法
println exp.invokeMethod('inner', null)
println exp.invokeMethod('match', [id: 123])
exp.call()
exp.unknown('haha')
}
}
可以看到,在 Expression.groovy 中,可以通过 exp.getProperty($valueProp) 或 exp[$valueProp] 或 exp."$valueProp" 来动态访问指定的属性,可以使用 invokeMethod 轻松访问私有方法 inner 。
方法动态分派
上一节讲到动态访问属性。 实现方法的动态分派也是非常简单的。可以使用 obj."$methodName"(args) 来动态调用指定方法。
如下代码所示。有一个测试类,里面有一些测试方法。要运行这些测试方法,可能 Java 会借助注解来优雅地实现。而在 Groovy 中,只要通过 MetaClass.methods 获取到所有方法,然后通过 grep 进行过滤, 就可以调用了。
代码清单二:TestCases.groovy
class TestCases {
def testA() { println 'do testA' }
def testB() { println 'do testB' }
def getTestData() { println "getTestData" }
static void main(args) {
def testCases = new TestCases()
def testMethods = testCases.metaClass.methods.collect { it.name }.grep(~/^test\w+/)
// 动态访问方法
testMethods.each {
testCases."$it"()
}
}
}
属性是闭包
在代码清单一中,定义了一个 call 属性,这个属性是一个闭包。因此这个属性是可以当做方法来调用的。
兜底方法
此外,定义了一个 methodMissing 方法。当在对象上调用不存在的方法时,就会路由到这个方法上。可以称之为 “兜底方法”,用来保证健壮性,避免抛异常。
注意,methodMissing 方法签名中,必须写成 methodMissing(String name, args)
, 而不是 methodMissing(name, args)
。String 修饰符是必要的,否则这个方法会不起作用。
方法拦截
在应用程序中,常常需要在方法前后执行一段逻辑。这种需求可以通过 AOP 来实现。 AOP 本质是方法拦截。
在 Groovy 中实现方法拦截,有两种方式: 实现 GroovyInterceptable 接口 ; 在 MetaClass 中实现 invokeMethod 方法。
GroovyInterceptable
实现 GroovyInterceptable 接口的类,必须实现 invokeMethod 方法。 调用该对象的任意方法(包括不存在的方法),都会被拦截到 invokeMethod 。 如下代码所示:SubExpression 实现了 GroovyInterceptable 接口,并定义了 invokeMethod 方法。调用该对象的 match 或 nonexist 方法,都会被拦截到 invokeMethod 执行。
这里要特别注意的是, 不能在 invokeMethod 中直接调用 println 和 该对象的其它方法。 因为这些方法都会被自动拦截到这个方法里,从而导致重定向循环,直到栈溢出。这里使用了 this.metaClass.getMetaMethod(name)?.invoke(this, args)
的方式来反射调用指定的方法。 使用 ?. 符号,是考虑到会调用到不存在的方法。
代码清单三:SubExpression.groovy
import groovy.util.logging.Log
@Log
class SubExpression extends Expression implements GroovyInterceptable {
def invokeMethod(String name, args) {
log.info("enter method=$name, args=$args")
//println "enter method=$name, args=$args" can't call this, because println call will be intercepted to this method
//match(args) can't call this, because match call will be intercepted to this method
def result = this.metaClass.getMetaMethod(name)?.invoke(this, args)
log.info("exit method=$name, args=$args")
result
}
static void main(args) {
def exp = new SubExpression(field: "id", op:"=", value:111)
println exp.match([id: 123])
println exp.match([id: 111])
println exp.nonexist()
}
}
MetaClass
另一种定义方法拦截的方法,是在指定类的 MetaClass 中注入 invokeMethod 。 如下代码所示。
代码清单四:SubExpression2.groovy
@Log
class SubExpression2 extends Expression {
static void main(args) {
// must be the first line
SubExpression2.metaClass.invokeMethod = { String name, margs ->
log.info("enter method=$name, args=$margs")
def result = SubExpression2.metaClass.getMetaMethod(name)?.invoke(delegate, margs)
log.info("exit method=$name, args=$margs")
result
}
def exp = new SubExpression2(field: "id", op:"=", value:111)
println exp.match([id: 123])
println exp.match([id: 111])
println exp.nonexist()
}
}
方法注入
元编程的另一个重要特性是,可以为指定类动态注入方法。动态注入方法,有两种实现: @Category 打开类,通过指定类的 MetaClass 来注入。
打开类
有时,想要在一个现有类中添加一些新的方法,但是,又没法修改现有类的源代码。怎么办呢? 可以使用“打开类”的方法。
如下代码所示,想为 Map 类增加一个 pretty 打印的方法。 可以定义一个 MapUtil 类,并定义 pretty 方法, 然后在 MapUtil 增加一个 @Category(Map) 的注解。在客户端使用时,需要使用 use(MapUtil) 的语法,限定一个作用域,在该作用域里可以让 map 对象直接调用 pretty 方法。是不是很棒 ?
代码清单五:InjectingMethod.groovy
class InjectingMethod {
static void main(args) {
[id:123, name:'qin', 'skills':'good'].each {
println it
}
use(MapUtil) {
def map = [id:123, name:'qin', 'skills':'good']
println map.pretty()
}
}
}
@Category(Map)
class MapUtil {
def pretty() {
"[" + this.collect { it }.join(",") + "]"
}
}
MetaClass
又回到 MetaClass 了。 也可以直接在 MetaClass 中直接添加指定的方法。 有两种写法。 第一种写法非常直接,直接写 SomeClass.metaClass.methodName = { 闭包 } 。这种写法适合于添加一两个方法。
代码清单六:InjectingMethod2.groovy
class InjectingMethod2 {
static void main(args) {
Map.metaClass.readVal = { path ->
if (delegate?.isEmpty || !path) {
return null
}
def paths = path.split("\\.")
def result = delegate
paths.each { subpath ->
result = result?.get(subpath)
}
result
}
def skills = [id: 123, name: 'qin', 'skills': ['programming': 'good', 'writing': 'good', 'expression': 'not very good']]
println(skills.readVal('name') + " can do:\n" +
['programming', 'writing', 'expression', 'dance'].collect { "skills.$it" }.collect {
"\t$it ${skills.readVal(it)}"
}.join('\n'))
}
}
如果要添加多个方法呢,可以使用 EMC 语法进行打包,如下代码所示。
使用 Map.metaClass { 在这里面定义各种方法 } 可以将 Map 的自定义新方法都打包在一起。客户端使用的时候,跟分别定义是一样的。 这里,定义 static 方法时,需要指定 'static' : { static 方法 } 。
代码清单七:InjectingMethod3.groovy
class InjectingMethod3 {
static void main(args) {
Map.metaClass {
flatMap = { ->
def finalResult = [:]
delegate.each { key, value ->
if (value instanceof Map) {
def innerMap = [:]
value.each { k, v ->
innerMap[key+'.'+k] = v
}
finalResult.putAll(innerMap)
}
else {
finalResult[key] = value
}
}
finalResult
}
methodMissing = { name, margs ->
"Unknown method=$name, args=$margs"
}
'static' {
pretty = { map ->
"[" + map.collect { it }.join(",") + "]"
}
}
}
def skills = [id:123, name:'qin', 'skills': ['programming':'good', 'writing': 'good', 'expression':'not very good']]
println "pretty print: " + Map.pretty(skills)
println 'flatMap:' + skills.flatMap()
println 'nonexist: ' + skills.nonexist()
}
}
方法混入
方法混入,是将其它类的方法借为己用,更轻松地获取更多能力的方式。 有两种形式: 在类中静态混入和 动态混入。
静态混入
如下代码所示。首先定义一个 SingleExpUtil.from ,将一个字符串转换成 Expression 对象。现在,想在 Expression 中借用这个方法。可以直接加个注解 @Mixin(SingleExpUtil) 即可 【静态混入】。
代码清单八:ExpressionWithMixin.groovy
@Mixin(SingleExpUtil)
class ExpressionWithMixin extends Expression {
def cons(str) {
// 静态 mixin
from(str)
}
static void main(args) {
def exp = new ExpressionWithMixin().cons('state = 5')
println exp.invokeMethod('inner', null)
println exp.match(['state': '5'])
}
}
class SingleExpUtil {
Expression from(expstr) {
def (field, op, value) = expstr.split(" ")
new Expression(field: field, op: op, value: value)
}
}
动态混入
如下代码所示:使用了 CombinedExpression.mixin CombinedExpressionUtil 的语法进行动态方法混入。在不能修改类 CombinedExpression 源代码的情况下,这种方式更加灵活。
代码清单九:CombinedExpression.groovy
class CombinedExpression {
List expressions
def desc() {
"[" + expressions?.collect { it.invokeMethod('inner', null) }?.join(",") + "]"
}
static void main(args) {
// 动态混入
CombinedExpression.mixin CombinedExpressionUtil
def ce = new CombinedExpression().from("state = 6 && type = 1")
println ce.desc()
println new CombinedExpression().desc()
}
}
@Mixin(SingleExpUtil)
class CombinedExpressionUtil {
CombinedExpression from(expstr) {
def conds = expstr.split("&&")
def expressions = conds.collect { cond -> from(cond.trim()) }
new CombinedExpression(expressions: expressions)
}
}
动态创建类
通常,需要根据一些元数据来动态创建类。比如说,根据 DB 表里的字段,动态创建含有与字段对应的属性的类,而不是固定写死。 仔细观察类,发现它其实只是一些实例变量(可以用Map 来表达)及实例方法、静态方法组成。 在 Groovy 中,可以使用 Expando 类来动态创建类。Expando 实际是一个含有属性 Map 的实现了 GroovyObject 的类。
如下代码所示。使用 Expando 创建一个类,并赋给对象 exp 后,也可以进行进行动态注入方法 (match) ,之后,就可以使用访问对象的 API 去访问这个对象了。 这种做法叫做 “DuckingType”: 管它是不是鸭,只要能像鸭一样干活就行。
代码清单十:DynamicCreating
class DynamicCreating {
static void main(args) {
def exp = new Expando(field: "id", op:"=", value:111,
inner: {
"EXP[$field $op $value]"
})
exp.match = { map ->
map[field] == value
}
println exp.getProperty("value")
exp.setProperty("value", 123)
def valueProp = "value"
println "exp[$valueProp] = ${exp[valueProp]}"
println "exp.\"$valueProp\" = " + exp."$valueProp"
println exp.invokeMethod('inner', null)
println(exp.match([id:123]))
}
}
方法调用流程图
如下展示了 Groovy 方法调用的流程图,其优先级是:
STEP1: 实现了 GroovyInterceptable 的 invokeMethod 方法;
STEP2: 实现了 MetaClass.invokeMethod 方法;
STEP3: 含有某个属性与方法同名,并且该属性正好是闭包(可调用对象);
STEP4: methodMissing 方法;
STEP5: 自定义的 invokeMethod 方法;
STEP6: 抛出 MissingMethodException 。
在 Groovy 中调用方法有什么疑惑时,可以参考该图。比如说,如果一个类同时实现了 GroovyInterceptable 和 MetaClass.invokeMethod ,会调用哪个? 后者。如果一个类没有实现 GroovyInterceptable , 但定义了 invokeMethod, 且定义了 MetaClass.invokeMethod 会调用哪个? 仍然是后者。 诸如此类。
小结
元编程,是一种实用编程技术,也是一种新的看待程序的动态视角。 从动态视角来看程序,想象的空间更大,因为程序的运行本身就是动态的,而不是像代码那样的静态结构。
最后,借用《Ruby元编程》第七章大师的一句话: 从来就没有元编程,只有编程而已。
参考
- 《Ruby元编程》
- 《Programming Groovy 2》 PartⅢ