同函数式编程类似,元编程,看上去像一门独派武学。 在 《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<Expression> 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元编程》第七章大师的一句话: 从来就没有元编程,只有编程而已。

参考##

Groovy元编程简明教程的更多相关文章

  1. Groovy元编程应用之自动生成订单搜索接口测试用例集

    背景 在 "Groovy元编程简明教程" 一文中,简明地介绍了 Groovy 元编程的特性. 那么,元编程可以应用哪些场合呢?元编程通常可以用来自动生成一些相似的模板代码. 在 & ...

  2. Vbs 脚本编程简明教程之一

    —为什么要使用 Vbs ? 在 Windows 中,学习计算机操作也许很简单,但是很多计算机工作是重复性劳动,例如你每周也许需要对一些计算机文件进行复制.粘贴.改名.删除,也许你每天启动 计算机第一件 ...

  3. Java网络编程简明教程

    Java网络编程简明教程 网络编程  计算机网络相关概念 计算机网络是两台或更多的计算机组成的网络,同一网络内的任意两台计算机可以直接通信,所有计算机必须遵循同一种网络协议. 互联网 互联网是连接计算 ...

  4. Groovy常用编程知识点简明教程

    概述 Groovy 是一门基于JVM的动态类型语言,可以与 Java 平台几乎无缝融合(与Java代码一样编译成字节码). 使用 Groovy ,可以增强 Java 应用的灵活性和可扩展性,提升开发效 ...

  5. Python 简明教程 --- 26,Python 多进程编程

    微信公众号:码农充电站pro 个人主页:https://codeshellme.github.io 学编程最有效的方法是动手敲代码. 目录 1,什么是多进程 我们所写的Python 代码就是一个程序, ...

  6. AWK 简明教程

    AWK 简明教程 转自:http://coolshell.cn/articles/9070.html 有一些网友看了前两天的<Linux下应该知道的技巧>希望我能教教他们用awk和sed, ...

  7. C++模板元编程(C++ template metaprogramming)

    实验平台:Win7,VS2013 Community,GCC 4.8.3(在线版) 所谓元编程就是编写直接生成或操纵程序的程序,C++ 模板给 C++ 语言提供了元编程的能力,模板使 C++ 编程变得 ...

  8. Java8简明教程(转载)

    ImportNew注:有兴趣第一时间学习Java 8的Java开发者,欢迎围观<征集参与Java 8原创系列文章作者>. 以下是<Java 8简明教程>的正文. “Java并没 ...

  9. HTML简明教程(一)

    HTML简明教程(一) 内容主体来自:W3School 一.HTML 简介 二.HTML 基础 三.HTML 元素 四.HTML 属性 五.HTML 标题 六.HTML 段落 七.HTML 文本格式化 ...

随机推荐

  1. bayaim——达梦数据库安装DM7-单实例-Centos7.txt

    bay——安装_达梦DM7-单实例-Centos7.txt IP:10.20.100.22 ---------------------------------------------1.下载 注意:要 ...

  2. SA详细注释不压行代码

    ){ //变量含义:m是字符集大小,n是字符串长度,c是一个桶数组,a[i]是字符串(下标从1开始) //rk[i]就是suffix(i)的字典序排名,sa[i]就是要求的排名为i的后缀的起始位置,即 ...

  3. MongoDB自学------(4)MongoDB主从搭建

    MongoDB复制原理 mongodb的复制至少需要两个节点.其中一个是主节点,负责处理客户端请求,其余的都是从节点,负责复制主节点上的数据. mongodb各个节点常见的搭配方式为:一主一从.一主多 ...

  4. 利用Python进行数据分析-Pandas(第五部分-数据规整:聚合、合并和重塑)

    在许多应用中,数据可能分散在许多文件或数据库中,存储的形式也不利于分析.本部分关注可以聚合.合并.重塑数据的方法. 1.层次化索引 层次化索引(hierarchical indexing)是panda ...

  5. ASP.NET Server对象

    Server.HtmlEncode() 执行文本代码Server.HtmlDecode()可以将代码显示 而不是执行它 但是ASP.NET会认为恶意 我们可以将aspx代码开头添加validateRe ...

  6. R3环申请内存时页面保护与_MMVAD_FLAGS.Protection位的对应关系

    Windows内核分析索引目录:https://www.cnblogs.com/onetrainee/p/11675224.html 技术学习来源:火哥(QQ:471194425) R3环申请内存时页 ...

  7. Python实现一个键对应多个值的字典(multidict)

    一个字典就是一个键对应一个单值的映射.如果你想要一个键映射多个值,那么你就需要将这多个值放到另外的容器中, 比如列表或者集合里面.比如,你可以像下面这样构造这样的字典: d = { 'a' : [1, ...

  8. MySQL学习——管理事务

    MySQL学习——管理事务 摘要:本文主要学习了使用DCL语句管理事务的操作. 了解事务 什么是事务 事务是一组逻辑处理单位,可以是执行一条SQL语句,也可以是执行几个SQL语句. 事务用来保证数据由 ...

  9. Hacking/Penetrating tester bookmark collection

    Blogs http://carnal0wnage.blogspot.com/ http://www.mcgrewsecurity.com/ http://www.gnucitizen.org/blo ...

  10. JS 输出

    JS 输出 JavaScript 通常用于操作 HTML 元素. 操作 HTML 元素 如需从 JavaScript 访问某个 HTML 元素,您可以使用 document.getElementByI ...