同函數式編程相似,元編程,看上去像一門獨派武學。 在 《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 接口的類,必須實現 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 中注入 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 中直接添加指定的方法。 有兩種寫法。 第一種寫法很是直接,直接寫 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元編程》第七章大師的一句話: 歷來就沒有元編程,只有編程而已。