zoukankan      html  css  js  c++  java
  • 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元编程》第七章大师的一句话: 从来就没有元编程,只有编程而已。

    参考##

  • 相关阅读:
    select、poll和epoll
    Linux 常用命令之文件和目录
    SmartPlant Review 帮助文档机翻做培训手册
    SmartPlant Foundation 基础教程 3.4 菜单栏
    SmartPlant Foundation 基础教程 3.3 标题栏
    SmartPlant Foundation 基础教程 3.2 界面布局
    SmartPlant Foundation 基础教程 3.1 DTC登陆界面
    SmartPlant Foundation 基础教程 1.4 SPF架构
    SmartPlant Foundation 基础教程 1.3 SPF其他功能
    SmartPlant Foundation 基础教程 1.2 SPF集成设计功能
  • 原文地址:https://www.cnblogs.com/lovesqcc/p/10815868.html
Copyright © 2011-2022 走看看