天天看点

运行时和编译时元编程—运行时元编程运行时和编译时元编程 第一部分

原文链接   译文链接   译者:jackwang

groovy语言支持两种风格的元编程:运行时元编程和编译时元编程。第一种元编程支持在程序运行时修改类模型和程序行为,而第二种发生在编译时。两种元编程有各自的优缺点,在这一章节我们将详细讨论。

注:译者也是第一次接触groovy,由于时间和水平有限(姑且让译者使用这个理由吧,对待知识本应该一丝不苟)部分专有名词可能翻译不准确甚至有误(读者阅读的过程中最好能参考原文),恳请读者不吝留言指出,谢谢!

通过运行时元编程,我们可以推迟运行时的分支决策(译者注:该处原文为we can postpone to runtime the decision,对于decision,译者也找不到一个合适的表达,请读者根据下图和上下文理解,如果读者有更好的翻译请留言指出,谢谢)来拦截,注入甚至合成类或接口的方法。对于groovy mop(译者注:对于初学者,这里突然冒出这个新名词,译者也头大,通过查询,mop是mete object protocol的缩写,读者可参考该文来了解)的更深理解,我们需要理解groovy的对象和方法处理。在groovy里,我们主要使用三种类型的对象:pojo,pogo和groovy拦截器。groovy支持元编程多种方式来对这些类型对象进行元编程。

pojo – 一个普通的java对象,它的类可以使用java或其他支持jvm的语言来编写。

pogo – 一个groovy对象,类用groovy实现。默认继承了java.lang.object并且实现了groovy.lang.groovyobject接口。

groovy 拦截器 – 实现了groovy.lang.groovyinterceptable接口并且具有方法拦截能力的groovy对象,我们将在groovyinterceptable这一节详细讨论。

对于每次方法调用,groovy都会检查对象是一个pojo还是一个pogo。对于pojos,groovy从groovy.lang.metaclassregistry类中携带元信息并且委托方法来调用。对于pogos,groovy有更复杂的不知,我们在下图演示:

运行时和编译时元编程—运行时元编程运行时和编译时元编程 第一部分

groovy.lang.groovyobject的地位和java中的object类一样,是一个主接口。groovyobject有一个默认的实现类groovy.lang.groovyobjectsupport,这个类的主要职责是转换groovy.lang.metaclass对象的调用。groovyobject源码类似下面这样

根据运行时元编程的规定,当你调用的方法不是groovy对象时将会调用这个方法。这儿有一个简单的示例演示重载invokemethod()方法:

通过重载当前对象的getproperty()方法可以使每次读取属性时被拦截。下面是一个简单的示例:

(1) 将请求的getter转到除field3之外的所有属性

你可以重载setproperty()方法来拦截写属性:

你可以访问一个对象的metaclass或者通过改变默认的拦截机制来设置实现你自己的metaclass。比如说你通过写你自己的metaclass实现接口来将一套拦截机制分配到一个对象上:

你可以在groovyinterceptable专题里找到更多的例子。

这个功能和metaclass实现类相关。在该类默认的实现里,你可以无需调用他们的getter和setters方法来访问属性。下面是一个示例:

groovy支持methodmissing的概念。这个方法不同于invokemethod,它只能在方法分发失败的情况下调用,当给定的名字或给定的参数无法找到时被调用:

当我们使用methodmissing的时候,如果下一次同样一个方法被调用其返回的结果可能是缓存的。比如说,考虑在gorm的动态查找器,有一个methodmissing的实现,下面是具体的代码:

注意,如果我们发现一个方法要被调用,我们会使用expandometaclass动态注册一个新的方法在上面。这就是为什么下次相同的方法被调用将会更加快。使用methodmissing并没有invokemethod的开销大。而且如果是第二次调用将基本没有开销。

groovy支持propertymissing的概念,用于拦截可能存在的属性获取失败。在getter方法里,propertymissing使用单个string类型的参数来代表属性名字:

在groovy运行时,propertymissing(string)方法只有在没有任何getter方法可以被给定的property所找到才会被调用。

对于setter方法,可以添加第二个propertymissing定义来添加一个额外的值参数

methodmissing方法的最适用地方在动态注册新的属性时能极大提供查找属性所花费的性能。

methodmissing和propertymissing方法可以通过expandometaclass来添加静态方法和属性。

groovy.lang.groovyinterceptable接口是一个继承了groovyobject的标记接口,在groovy运行时,用于标记所有方法可以通过groovy的方法分发机制被拦截。

当一个groovy对象实现了groovyinterceptable接口,它的invokemethod()将在任何方法调用时被调用。下面是这个类型的一个简单示例:

下一块代码是一个测试类,不管调用存在的方法还是不存在的方法都将返回相同的结果。

我们不能使用默认的groovy方法比如println,因为这些方法是被注入到groovy对象中区,因此它们也会被拦截。

如果我们想拦截所有所有方法但又不想实现groovyinterceptable接口,我们可以在一个对象的metaclass类上实现invokemethod()。对于pogos和pojos,这种方式都是可以的。下面是一个示例:

关于metaclass类的详细信息可以在metaclass章节找到。

有这样一种场景,如果能让一个类的某些方法不受控制将会是很有用的。为了实现这种可能性,groovy从object-c借用实现了一个特性,叫做categories。

categories特性实现了所谓的category类,一个category类是需要满足某些特定的预定义的规则来定义一些拓展方法。

下面有几个categories是在groovy环境中系统提供的一些额外功能:

groovy.time.timecategory

groovy.servlet.servletcategory

groovy.xml.dom.domcategory

category类默认是不能使用的,要使用这些定义在一个category类的方法需要使用 use 方法,这个方法是gdk提供的一个内置于groovy对象中的实例:

(1) timecategory添加一个方法到integer

(2) timecategory添加一个方法到date

use 方法把category类作为第一个参数,一个闭包代码块作为第二个参数。在closure里可以访问catetory。从上面的例子可以看到,即便是jdk的类,比如java.lang.integer或java.util.date也是可以被包含到用户定义的方法里的。

一个category不需要直接暴露给用户代码,下面的示例说明了这一点:

如果我们去看groovy.time.timecategory类的嗲吗我们会发现拓展方法都是被声明为static方法。事实上,一个category类的方法要能被成功地加到use代码块里必须要这样写:

另外一个要求是静态方法的第一个参数必须定义类型,只要方法被激活。另外一个参数可以作为一个普通的参数当成方法的变量。

因为参数和静态方法的转变,category方法的定义可能比一般的方法定义不那么直观。不过groovy提供了一个@category注解,可以在编译时将一个类转化为category类。

使用@category注解可以直接使用示例方法二不必将目标类型作为第一个参数的好处。目标类型类在注解里作为了一个参数。

在编译时元编程章节里有@category的详细说明。

(tbd)

delegating metaclass

magic package(maksym stavyskyi)

groovy有一个特殊的metaclass类叫做expandometaclass。它的特别之处在于支持动态添加或修改方法,构造函数,属性,甚至通过使用一个闭包语法来添加或修改静态方法。

这些特性测试场景将会非常使用,具体在测试指南将会说明。

在groovy里,每一个java.lang.class类都有一个特殊的metaclass属性,可以通过它拿到一个expandometacalss实例。这个实例可以被用于添加方法或修改一个已经存在的方法的行为。

默认expandometacalss是不能被继承的,如果你需要这样做必须在你的应用启动前或servlet启动类前调用expandometaclass#enableglobally()

下面的小节将详细说明如何在各种场景使用expandometacalss。

methods

一旦expandometaclass通过metaclass属性被调用,就可以使用<<或 = 操作符来添加方法。

注意 << 是用来添加新方法,如果一个方法已经存在使用它会抛出异常。如果你想替换一个方法可以使用 = 操作符。

对于一个不存在的metaclass属性通过传入一个闭包代码块实例来实现

上面的示例演示了如何通过metaclass属性使用 << 或 = 操作符赋值到一个闭包代码块将一个新方法添加到一个类。闭包参数将作为方法参数被拦截。不确定的方法参数可以使用{→ …} 语法。

properties

expandometaclass支持两种添加或重载属性的机制。

第一种,支持通过赋值到一个metacalss属性来声明一个可变属性。

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

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

当然,这不仅仅是一个技术问题。比如在一个servlet容器里一种存储值得方法是放到当前request中作为request的属性。(grails也是这样做的)

constructors

构造函数可以通过constructor属性来添加,也可以通过闭包代码块使用 << 或 = 来添加。在运行时闭包参数将变成构造函数参数。

添加构造函数的时候需要注意,很容易导致栈溢出问题。

static methods

静态方法可以通过同样的技术来实现,仅仅是比实例方法的方法名字前多一个static修饰符。

borrowing methods

使用expandometaclass,可以实现使用groovy方法指针从其他类中借用方法。

动态方法名(dynamic method names)

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

同样的概念可以用于静态方法和属性。

在grails网络应用程序框架里我们可以找到动态方法名字的实例。“动态编码”这个概念就是动态方法名字的具体实现。

htmlcodec类

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

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方法:

在闭包代码里,第一步是通过给定的名字和参数查找metamethod。如果一个方法准备就绪就委托执行,否则将返回一个默认值。

metamethod是一个存在于metaclass上的方法,可以在运行时和编译时被添加进来。

同样的逻辑可以用来重载setproperty和getproperty

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

重载static invokemethod

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

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

extending interfaces

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

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

标准拓展方法:

gettext方法不存在于file类里,当然,groovy知道它定义在一个特殊的类里,resourcegroovymethods:

resourcegroovymethods.java

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

创建一个拓展模型非常简单

写一个像上面类似的拓展类

写一个模块描述文件

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

要么直接在类路径下提供类文件和模块描述文件

或者将拓展模块打包成jar包以便重用

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

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

静态方法(也叫作类方法)

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

(1)拓展类

(2)静态方法的第一个参数和接收的信息一致,也就是拓展实例

下一步,声明了拓展类之后,你可以这样调用它:

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

(1)静态拓展类

(2)静态方法的第一个从那时候和被拓展的保持一致

这个例子,可以直接从string类里调用

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

org.codehaus.groovy.runtime.extensionmodule

模块描述需要4个主键

modulename:你的模块名字

moduleversion:你的模块版本号。注意版本号仅仅用于检验你是否有将两个不同的版本导入同一个模块

extensionclasses:拓展帮助类中实例方法列表,你可以提供好几个类,使用逗号分隔

staticextensionclasses:拓展帮助类中静态方法裂列表,你可以提供好几个类,使用逗号分隔

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

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

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