Groovy元編程簡明教程

同函數式編程相似,元編程,看上去像一門獨派武學。 在 《Ruby元編程》一書中,定義:元編程是運行時操做語言構件的編程能力。其中,語言構件指模塊、類、方法、變量等。經常使用的主要是動態建立和訪問類和方法。元編程,體現了程序的動態之美。程序員

對於 Java 系程序員來講,不大會使用 Ruby 編程, 更多會考慮 Java 的近親 Groovy 。 本文將簡要介紹 Groovy 元編程的語言特性。Groovy 元編程基於 MOP 協議。

express

元編程特性

輕鬆運行時

在 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 進行過濾, 就能夠調用了。this

代碼清單二:TestCases.groovycode

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元編程》第七章大師的一句話: 歷來就沒有元編程,只有編程而已。

參考

相關文章
相關標籤/搜索