同函数式编程类似,元编程,看上去像一门独派武学。 在 《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. SQL Server 之事务执行,让语句在事务中执行

    BEGIN TRAN     BEGIN  TRY DELETE FROM dbo.表 INSERT INTO  dbo.表(    Id,   字段....) SELECTId,字段...    F ...

  2. NFS挂载遇到的问题

    问题描述:生产环境中需要经常运用NFS挂载,就在测试环境中测试一下,将服务器中192.168.1.4 /u01/app/oracle/product/11.2.0/dbhome_1/dbs  挂载到1 ...

  3. 11 K-Means 原理及案例

    11 K-Means 原理及案例 非监督学习 unsupervised learning (非监督学习) ,只有特征值,没有目标值 聚类: 主要方法 - k-means (K - 需要分成的类别数) ...

  4. Vue项目中使用jquery插件

    1.引入jquery,并且在vue.config.js里配置 config.plugin('provide') .use(webpack.ProvidePlugin, [{ $: 'jquery', ...

  5. 《java编程思想(第四版)》第一二章学习笔记

    目录 一.Introduction 1.抽象过程 2.面向对象语言(包括Java)的五个基本特性 3.每个对象都提供服务 4.public.private.protected三者的区别 5.Java的 ...

  6. 浏览器及Windows常用快捷键汇总

    浏览器常用快捷键: F5 刷新 Ctrl+N 打开新窗口 Ctrl+T 打开新标签 Ctrl +  O  打开浏览器的时候打开文件 Ctrl+Shift+N 隐身模式打开窗口 F2 F3 切换  Ct ...

  7. Spring Boot 2 快速教程:WebFlux 快速入门(二)

    摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! 02:WebFlux 快速入门实践 文章工程: JDK 1.8 ...

  8. GO-逻辑判断(if,else if,else,switch)

    一.if逻辑判断 package main import "fmt" func main() { var a =10; if a>10 { //大括号前不能回车 fmt.Pr ...

  9. GO基础之切片

    一.什么是切片 Go语言切片是对数组的抽象. Go数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"): 与数组相比切 ...

  10. 利用 Docker Compose 搭建 SpringBoot 运行环境(超详细步骤和分析)

    0.前言 相信点进来看这篇文章的同学们已经对 Docker Dompose 有一定的了解了,下面,我们拿最简单的例子来介绍如何使用 Docker Compose 来管理项目. 本文例子: 一个应用服务 ...