运行时和编译时元编程—运行时元编程(二)

1.7.3 ExpandoMetaclass

Groovy有一个特殊的MetaClass类叫做ExpandoMetaClass。它的特别之处在于支持动态添加或修改方法,构造函数,属性,甚至通过使用一个闭包语法来添加或修改静态方法。
这些特性测试场景将会非常使用,具体在测试指南将会说明。
在Groovy里,每一个java.lang.Class类都有一个特殊的metaClass属性,可以通过它拿到一个ExpandoMetaCalss实例。这个实例可以被用于添加方法或修改一个已经存在的方法的行为。
默认ExpandoMetaCalss是不能被继承的,如果你需要这样做必须在你的应用启动前或servlet启动类前调用ExpandoMetaClass#enableGlobally()
下面的小节将详细说明如何在各种场景使用ExpandoMetaCalss。

Methods
一旦ExpandoMetaClass通过metaClass属性被调用,就可以使用<<或 = 操作符来添加方法。
注意 << 是用来添加新方法,如果一个方法已经存在使用它会抛出异常。如果你想替换一个方法可以使用 = 操作符。
对于一个不存在的metaClass属性通过传入一个闭包代码块实例来实现

1 class Book {
2    String title
3 }
4  
5 Book.metaClass.titleInUpperCase &amp;amp;amp;amp;lt;&amp;amp;amp;amp;lt; {-&amp;amp;amp;amp;gt; title.toUpperCase() }
6  
7 def b = new Book(title:"The Stand")
8  
9 assert "THE STAND" == b.titleInUpperCase()

上面的示例演示了如何通过metaClass属性使用 << 或 = 操作符赋值到一个闭包代码块将一个新方法添加到一个类。闭包参数将作为方法参数被拦截。不确定的方法参数可以使用{→ …} 语法。
Properties
ExpandoMetaClass支持两种添加或重载属性的机制。
第一种,支持通过赋值到一个metaCalss属性来声明一个可变属性。

1 class Book {
2    String title
3 }
4  
5 Book.metaClass.author = "Stephen King"
6 def b = new Book()
7  
8 assert "Stephen King" == b.author

第二种使用标准机制来添加getter或 setter方法:

1 class Book {
2   String title
3 }
4 Book.metaClass.getAuthor &amp;amp;amp;amp;lt;&amp;amp;amp;amp;lt; {-&amp;amp;amp;amp;gt; "Stephen King" }
5  
6 def b = new Book()
7  
8 assert "Stephen King" == b.author

上面的示例代码中,闭包里的属性是一个制度属性。当然添加一个类似的setter方法也是可行的,但是属性值需要被存储起来。具体可以看下面的示例:

01 class Book {
02   String title
03 }
04  
05 def properties = Collections.synchronizedMap([:])
06  
07 Book.metaClass.setAuthor = { String value -&amp;amp;amp;amp;gt;
08    properties[System.identityHashCode(delegate) + "author"] = value
09 }
10 Book.metaClass.getAuthor = {-&amp;amp;amp;amp;gt;
11    properties[System.identityHashCode(delegate) + "author"]
12 }

当然,这不仅仅是一个技术问题。比如在一个servlet容器里一种存储值得方法是放到当前request中作为request的属性。(Grails也是这样做的)
Constructors
构造函数可以通过constructor属性来添加,也可以通过闭包代码块使用 << 或 = 来添加。在运行时闭包参数将变成构造函数参数。

1 class Book {
2     String title
3 }
4 Book.metaClass.constructor &amp;amp;amp;amp;lt;&amp;amp;amp;amp;lt; { String title -&amp;amp;amp;amp;gt; new Book(title:title) }
5  
6 def book = new Book('Groovy in Action - 2nd Edition')
7 assert book.title == 'Groovy in Action - 2nd Edition'

添加构造函数的时候需要注意,很容易导致栈溢出问题。
Static Methods
静态方法可以通过同样的技术来实现,仅仅是比实例方法的方法名字前多一个static修饰符。

1 class Book {
2    String title
3 }
4  
5 Book.metaClass.static.create &amp;amp;amp;amp;lt;&amp;amp;amp;amp;lt; { String title -&amp;amp;amp;amp;gt; new Book(title:title) }
6  
7 def b = Book.create("The Stand")

Borrowing Methods
使用ExpandoMetaClass,可以实现使用Groovy方法指针从其他类中借用方法。

01 class Person {
02     String name
03 }
04 class MortgageLender {
05    def borrowMoney() {
06       "buy house"
07    }
08 }
09  
10 def lender = new MortgageLender()
11  
12 Person.metaClass.buyHouse = lender.&amp;amp;amp;amp;amp;borrowMoney
13  
14 def p = new Person()
15  
16 assert "buy house" == p.buyHouse()

动态方法名(Dynamic Method Names)
因为Groovy支持你使用字符串作为属性名同样也支持在运行时动态创建方法和属性。要创建一个动态名字的方法仅仅使用引用属性名作为字符串这一特性即可。

01 class Person {
02    String name = "Fred"
03 }
04  
05 def methodName = "Bob"
06  
07 Person.metaClass."changeNameTo${methodName}" = {-&amp;amp;amp;amp;gt; delegate.name = "Bob" }
08  
09 def p = new Person()
10  
11 assert "Fred" == p.name
12  
13 p.changeNameToBob()
14  
15 assert "Bob" == p.name

同样的概念可以用于静态方法和属性。
在Grails网络应用程序框架里我们可以找到动态方法名字的实例。“动态编码”这个概念就是动态方法名字的具体实现。
HTMLCodec类

1 class HTMLCodec {
2     static encode = { theTarget -&amp;amp;amp;amp;gt;
3         HtmlUtils.htmlEscape(theTarget.toString())
4     }
5  
6     static decode = { theTarget -&amp;amp;amp;amp;gt;
7         HtmlUtils.htmlUnescape(theTarget.toString())
8     }
9 }

上面的代码演示了一种编码的实现。Grails对于每个类都有很多编码实现可用。在运行时可以配置多个编码类在应用程序classpath里。在应用程序启动框架里添加一个encodeXXX和一个decodeXXX方法到特定的meta-classes类。XXX是编码类的第一部分(比如encodeHTML)。这种机制在Groovy预处理代码中如下:

01 def codecs = classes.findAll { it.name.endsWith('Codec') }
02  
03 codecs.each { codec -&amp;amp;amp;amp;gt;
04     Object.metaClass."encodeAs${codec.name-'Codec'}" = { codec.newInstance().encode(delegate) }
05     Object.metaClass."decodeFrom${codec.name-'Codec'}" = { codec.newInstance().decode(delegate) }
06 }
07  
08 def html = '&amp;amp;amp;amp;lt;html&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;body&amp;amp;amp;amp;gt;hello&amp;amp;amp;amp;lt;/body&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;/html&amp;amp;amp;amp;gt;'
09  
10 assert '&amp;amp;amp;amp;lt;html&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;body&amp;amp;amp;amp;gt;hello&amp;amp;amp;amp;lt;/body&amp;amp;amp;amp;gt;&amp;amp;amp;amp;lt;/html&amp;amp;amp;amp;gt;' == html.encodeAsHTML()

Runtime Discovery
在运行时,当方法被执行的时候如果知道其他方法或属性的存在性是非常有用的。ExpandoMetaClass提供了下面的方法来获取:

  • getMetaMethod
  • hasMetaMethod
  • getMetaProperty
  • hasMetaProperty

为何不直接使用反射?因为Groovy不同于Java,Java的方法是真正的方法并且只能在运行时存在。Groovy是(并不总是)通过MetaMethods来呈现。MetaMethods告诉你在运行时哪些方法可用,因此你的代码可以适配。
重载invokeMethod,getProperty和setProperty是一种特别的用法。
GroovyObject Methods
ExpandoMetaClass的另外一个特点是支持重载invokeMethod,getProperty和setProperty。这些方法可以在groovy.lang.GroovyObject类里找到。
下面的代码演示了如何重载invokeMethod方法:

01 class Stuff {
02    def invokeMe() { "foo" }
03 }
04  
05 Stuff.metaClass.invokeMethod = { String name, args -&amp;amp;amp;gt;
06    def metaMethod = Stuff.metaClass.getMetaMethod(name, args)
07    def result
08    if(metaMethod) result = metaMethod.invoke(delegate,args)
09    else {
10       result = "bar"
11    }
12    result
13 }
14  
15 def stf = new Stuff()
16  
17 assert "foo" == stf.invokeMe()
18 assert "bar" == stf.doStuff()

在闭包代码里,第一步是通过给定的名字和参数查找MetaMethod。如果一个方法准备就绪就委托执行,否则将返回一个默认值。
MetaMethod是一个存在于MetaClass上的方法,可以在运行时和编译时被添加进来。
同样的逻辑可以用来重载setProperty和getProperty

01 class Person {
02    String name = "Fred"
03 }
04  
05 Person.metaClass.getProperty = { String name -&amp;amp;amp;gt;
06    def metaProperty = Person.metaClass.getMetaProperty(name)
07    def result
08    if(metaProperty) result = metaProperty.getProperty(delegate)
09    else {
10       result = "Flintstone"
11    }
12    result
13 }
14  
15 def p = new Person()
16  
17 assert "Fred" == p.name
18 assert "Flintstone" == p.other

这里值得注意的一个重要问题是不是MetaMethod而是MetaProperty实例将会查找。如果一个MetaProperty的getProperty方法已经存在,将会直接调用。

重载Static invokeMethod

ExpandoMetaClass甚至允许重载静态方法,通过一个特殊的invokeMethod语法

01 class Stuff {
02    static invokeMe() { "foo" }
03 }
04  
05 Stuff.metaClass.'static'.invokeMethod = { String name, args -&amp;amp;amp;gt;
06    def metaMethod = Stuff.metaClass.getStaticMetaMethod(name, args)
07    def result
08    if(metaMethod) result = metaMethod.invoke(delegate,args)
09    else {
10       result = "bar"
11    }
12    result
13 }
14  
15 assert "foo" == Stuff.invokeMe()
16 assert "bar" == Stuff.doStuff()

重载静态方法的逻辑和前面我们见到的从在实例方法的逻辑一样。唯一的区别在于方位metaClass.static属性需要调用getStaticMethodName作为静态MetaMehod实例的返回值。

Extending Interfaces

有时候我们需要在ExpandoMetaClass接口里添加方法,为实现这个,必须支持在应用启动前全局支持ExpandoMetaClass.enableGlobally()方法。

1 List.metaClass.sizeDoubled = {-&amp;amp;amp;gt; delegate.size() * 2 }
2  
3 def list = []
4  
5 list &amp;amp;amp;lt;&amp;amp;amp;lt; 1
6 list &amp;amp;amp;lt;&amp;amp;amp;lt; 2
7  
8 assert 4 == list.sizeDoubled()

1.8 拓展模型

1.8.1 拓展已经存在的类

拓展模型允许你添加新方法到已经存在的类中。这些类包括预编译类,比如JDK中的类。这些新方法不同于使用metaclass或category,可以全局使用。比如,
标准拓展方法:

1 def file = new File(...)
2 def contents = file.getText('utf-8')

getText方法不存在于File类里,当然,Groovy知道它定义在一个特殊的类里,ResourceGroovyMethods:
ResourceGroovyMethods.java

1 public static String getText(File file, String charset) throws IOException {
2  return IOGroovyMethods.getText(newReader(file, charset));
3 }

你可能已经注意到,这个拓展方法在一个帮助类(定义了各种各样的拓展方法)中使用了static方法来定义。getText方法的第一个参数和传入值应该一直,额外的参数和拓展方法的参数一致。这里我们就定义了File类的getText方法。这个方法进接受一个参数(String类型)。
创建一个拓展模型非常简单

  • 写一个像上面类似的拓展类
  • 写一个模块描述文件

下一步你需要使拓展模型对Groovy可见,需要将拓展模型类和可用的描述类添加到类路径。这意味着你有以下选择:

  • 要么直接在类路径下提供类文件和模块描述文件
  • 或者将拓展模块打包成jar包以便重用

拓展模块有两种方法添加到一个类中

  • 实例方法(也叫作一个类的实例)
  • 静态方法(也叫作类方法)

1.8.2 实例方法

要添加一个实例方法到一个已经存在的类,你需要创建一个拓展类。举个例子,你想添加一个maxRetries放到到Integer类里,它接收一个闭包只要不抛出异常最多执行n次。你需要写下面的代码:

01 class MaxRetriesExtension {                                     //(1)
02     static void maxRetries(Integer self, Closure code) {        //(2)
03         int retries = 0
04         Throwable e
05         while (retries&amp;amp;lt;self) {
06             try {
07                 code.call()
08                 break
09             } catch (Throwable err) {
10                 e = err
11                 retries++
12             }
13         }
14         if (retries==0 &amp;amp;amp;&amp;amp;amp; e) {
15             throw e
16         }
17     }
18 }

(1)拓展类
(2)静态方法的第一个参数和接收的信息一致,也就是拓展实例
下一步,声明了拓展类之后,你可以这样调用它:

01 int i=0
02 5.maxRetries {
03     i++
04 }
05 assert i == 1
06 i=0
07 try {
08     5.maxRetries {
09         throw new RuntimeException("oops")
10     }
11 } catch (RuntimeException e) {
12     assert i == 5
13 }

1.8.3 静态方法

Groovy支持添加一个静态方法到一个类里,这种情况静态方法必须定义在自己的文件里。静态和实例拓展方法不能再同一个类里。

1 class StaticStringExtension {              //(1)
2     static String greeting(String self) {  //(2)
3         'Hello, world!'
4     }
5 }

(1)静态拓展类
(2)静态方法的第一个从那时候和被拓展的保持一致
这个例子,可以直接从String类里调用

1 assert String.greeting() == 'Hello, world!'

1.8.4 模块描述

Groovy允许你加载自己的拓展类,你必须声明你的拓展帮助类。你必须创建一个名为org.codehaus.groovy.runtime.ExtensionModule 到META-INF/services 目录里:
org.codehaus.groovy.runtime.ExtensionModule

1 moduleName=Test module for specifications
2 moduleVersion=1.0-test
3 extensionClasses=support.MaxRetriesExtension
4 staticExtensionClasses=support.StaticStringExtension

模块描述需要4个主键

  • moduleName:你的模块名字
  • moduleVersion:你的模块版本号。注意版本号仅仅用于检验你是否有将两个不同的版本导入同一个模块
  • extensionClasses:拓展帮助类中实例方法列表,你可以提供好几个类,使用逗号分隔
  • staticExtensionClasses:拓展帮助类中静态方法裂列表,你可以提供好几个类,使用逗号分隔

注意并不要求一个模块既定义静态帮助类又定义实例帮助类,你可以添加好几个类到单个模块,也可以拓展不同类到单个模块。还可以使用不同的类到单个拓展类,但是建议根据特性分组拓展方法。

1.8.5 拓展模块和类路径

你不能将一个编译好了的拓展类当成源码一样使用。也就是说使用一个拓展必须在类路径下,而且是一个已经编译好了的类。同城,你不能太拓展类里添加测试类。因为测试类通常和正式源码会分开。

1.8.6 类型检查能力

不像categories,拓展模块是编译后的类型检查。如果不能在类路径下找到,当你调用拓展方法时类型检查将会识别出来。对于静态编译也一样。

上一篇:交互实战:三个按钮的故事


下一篇:从数据中推断用户的行为--建模篇