Groovy元编程简明教程

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

参考##

posted @ 2019-05-05 19:56  琴水玉  阅读(1591)  评论(0编辑  收藏  举报