• 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(~/^testw+/)
    
            // 动态访问方法
            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:
    " +
                    ['programming', 'writing', 'expression', 'dance'].collect { "skills.$it" }.collect {
                        "	$it ${skills.readVal(it)}"
                    }.join('
    '))
        }
    
    }
    

    如果要添加多个方法呢,可以使用 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元编程》第七章大师的一句话: 从来就没有元编程,只有编程而已。

    参考##

  • 相关阅读:
    php自动保存文章内容中的图片
    javascript中字符串操作函数
    iis 301永久重定向图文教程
    ORA27101,ORA27102 错误解决方法
    IIS中启用Gzip压缩传输网页方法
    asp.net抓取163邮箱联系人实现代码
    windows IIS 日志文件如何查看及分析
    输出页面所有HTML 包括 JS 添加 的内容
    VS2005无法启动调试 及 解决办法
    Json转行DataTable
  • 原文地址:https://www.cnblogs.com/lovesqcc/p/10815868.html
Copyright © 2020-2023  润新知