运行时和编译时元编程 第一部分
Groovy语言支持两种风格的元编程:运行时元编程和编译时元编程。第一种元编程支持在程序运行时修改类模型和程序行为,而第二种发生在编译时。两种元编程有各自的优缺点,在这一章节我们将详细讨论。
注:译者也是第一次接触Groovy,由于时间和水平有限(姑且让译者使用这个理由吧,对待知识本应该一丝不苟)部分专有名词可能翻译不准确甚至有误(读者阅读的过程中最好能参考原文),恳请读者不吝留言指出,谢谢!
1.运行时元编程
通过运行时元编程,我们可以推迟运行时的分支决策(译者注:该处原文为we can postpone to runtime the decision,对于decision,译者也找不到一个合适的表达,请读者根据下图和上下文理解,如果读者有更好的翻译请留言指出,谢谢)来拦截,注入甚至合成类或接口的方法。对于Groovy MOP(译者注:对于初学者,这里突然冒出这个新名词,译者也头大,通过查询,MOP是Mete Object Protocol的缩写,读者可参考该文来了解)的更深理解,我们需要理解Groovy的对象和方法处理。在Groovy里,我们主要使用三种类型的对象:POJO,POGO和Groovy拦截器。Groovy支持元编程多种方式来对这些类型对象进行元编程。
对于每次方法调用,Groovy都会检查对象是一个POJO还是一个POGO。对于POJOs,Groovy从groovy.lang.MetaClassRegistry类中携带元信息并且委托方法来调用。对于POGOs,Groovy有更复杂的不知,我们在下图演示:
1.1 GroovyObject接口
Groovy.lang.GroovyObject的地位和Java中的Object类一样,是一个主接口。GroovyObject有一个默认的实现类groovy.lang.GroovyObjectSupport,这个类的主要职责是转换groovy.lang.MetaClass对象的调用。GroovyObject源码类似下面这样
03 |
public interface GroovyObject { |
05 |
Object invokeMethod(String name, Object args);
|
07 |
Object getProperty(String propertyName);
|
09 |
void setProperty(String propertyName, Object newValue);
|
11 |
MetaClass getMetaClass();
|
13 |
void setMetaClass(MetaClass metaClass);
|
1.1.1 invokeMethod
根据运行时元编程的规定,当你调用的方法不是Groovy对象时将会调用这个方法。这儿有一个简单的示例演示重载invokeMethod()方法:
01 |
class SomeGroovyClass { |
03 |
def invokeMethod(String name, Object args) {
|
04 |
return "called invokeMethod $name $args"
|
08 |
return 'method exists'
|
12 |
def someGroovyClass = new SomeGroovyClass() |
14 |
assert someGroovyClass.test() == 'method exists' |
15 |
assert someGroovyClass.someMethod() == 'called invokeMethod someMethod []' |
1.1.2 get/setProperty
通过重载当前对象的getProperty()方法可以使每次读取属性时被拦截。下面是一个简单的示例:
01 |
class SomeGroovyClass { |
11 |
def getProperty(String name) {
|
13 |
return metaClass.getProperty(this, name) //(1)
|
18 |
def someGroovyClass = new SomeGroovyClass() |
20 |
assert someGroovyClass.field1 == 'getHa' |
21 |
assert someGroovyClass.field2 == 'ho' |
22 |
assert someGroovyClass.field3 == 'field3' |
23 |
assert someGroovyClass.field4 == 'hu' |
(1) 将请求的getter转到除field3之外的所有属性
你可以重载setProperty()方法来拦截写属性:
05 |
void setProperty(String name, Object value) {
|
06 |
this.@"$name" = 'overriden'
|
13 |
assert pogo.property == 'overriden' |
1.1.3 get/setMetaClass
你可以访问一个对象的metaClass或者通过改变默认的拦截机制来设置实现你自己的MetaClass。比如说你通过写你自己的MetaClass实现接口来将一套拦截机制分配到一个对象上:
5 |
someObject.metaClass = new OwnMetaClassImplementation() |
你可以在GroovyInterceptable专题里找到更多的例子。
1.2 get/setAttribute
这个功能和MetaClass实现类相关。在该类默认的实现里,你可以无需调用他们的getter和setters方法来访问属性。下面是一个示例:
01 |
class SomeGroovyClass { |
11 |
def someGroovyClass = new SomeGroovyClass() |
13 |
assert someGroovyClass.metaClass.getAttribute(someGroovyClass, 'field1') == 'ha' |
14 |
assert someGroovyClass.metaClass.getAttribute(someGroovyClass, 'field2') == 'ho' |
06 |
void setProperty1(String property1) {
|
07 |
this.property1 = "setProperty1"
|
12 |
pogo.metaClass.setAttribute(pogo, 'field', 'ha') |
13 |
pogo.metaClass.setAttribute(pogo, 'property1', 'ho') |
15 |
assert pogo.field == 'ha' |
16 |
assert pogo.property1 == 'ho' |
1.3 MethodMissing
Groovy支持methodMissing的概念。这个方法不同于invokeMethod,它只能在方法分发失败的情况下调用,当给定的名字或给定的参数无法找到时被调用:
3 |
def methodMissing(String name, def args) {
|
8 |
assert new Foo().someUnknownMethod(42l) == 'this is me' |
当我们使用methodMissing的时候,如果下一次同样一个方法被调用其返回的结果可能是缓存的。比如说,考虑在GORM的动态查找器,有一个methodMissing的实现,下面是具体的代码:
03 |
def dynamicMethods = [...] // an array of dynamic methods that use regex
|
05 |
def methodMissing(String name, args) {
|
06 |
def method = dynamicMethods.find { it.match(name) }
|
08 |
GORM.metaClass."$name" = { Object[] varArgs ->
|
09 |
method.invoke(delegate, name, varArgs)
|
11 |
return method.invoke(delegate,name, args)
|
13 |
else throw new MissingMethodException(name, delegate, args)
|
注意,如果我们发现一个方法要被调用,我们会使用ExpandoMetaClass动态注册一个新的方法在上面。这就是为什么下次相同的方法被调用将会更加快。使用methodMissing并没有invokeMethod的开销大。而且如果是第二次调用将基本没有开销。
1.4 propertyMissing
Groovy支持propertyMissing的概念,用于拦截可能存在的属性获取失败。在getter方法里,propertyMissing使用单个String类型的参数来代表属性名字:
2 |
def propertyMissing(String name) { name }
|
5 |
assert new Foo().boo == 'boo' |
在Groovy运行时,propertyMissing(String)方法只有在没有任何getter方法可以被给定的property所找到才会被调用。
对于setter方法,可以添加第二个propertyMissing定义来添加一个额外的值参数
03 |
def propertyMissing(String name, value) { storage[name] = value }
|
04 |
def propertyMissing(String name) { storage[name] }
|
methodMissing方法的最适用地方在动态注册新的属性时能极大提供查找属性所花费的性能。
methodMissing和propertyMissing方法可以通过ExpandoMetaClass来添加静态方法和属性。
1.5 GroovyInterceptable
Groovy.lang.GroovyInterceptable接口是一个继承了GroovyObject的标记接口,在Groovy运行时,用于标记所有方法可以通过Groovy的方法分发机制被拦截。
3 |
public interface GroovyInterceptable extends GroovyObject { |
当一个Groovy对象实现了GroovyInterceptable接口,它的invokeMethod()将在任何方法调用时被调用。下面是这个类型的一个简单示例:
1 |
class Interception implements GroovyInterceptable { |
3 |
def definedMethod() { }
|
5 |
def invokeMethod(String name, Object args) {
|
下一块代码是一个测试类,不管调用存在的方法还是不存在的方法都将返回相同的结果。
1 |
class InterceptableTest extends GroovyTestCase { |
3 |
void testCheckInterception() {
|
4 |
def interception = new Interception()
|
6 |
assert interception.definedMethod() == 'invokedMethod'
|
7 |
assert interception.someMethod() == 'invokedMethod'
|
我们不能使用默认的Groovy方法比如println,因为这些方法是被注入到Groovy对象中区,因此它们也会被拦截。
如果我们想拦截所有所有方法但又不想实现GroovyInterceptable接口,我们可以在一个对象的MetaClass类上实现invokeMethod()。对于POGOs和POJOs,这种方式都是可以的。下面是一个示例:
01 |
class InterceptionThroughMetaClassTest extends GroovyTestCase { |
03 |
void testPOJOMetaClassInterception() {
|
04 |
String invoking = 'ha'
|
05 |
invoking.metaClass.invokeMethod = { String name, Object args ->
|
09 |
assert invoking.length() == 'invoked'
|
10 |
assert invoking.someMethod() == 'invoked'
|
13 |
void testPOGOMetaClassInterception() {
|
14 |
Entity entity = new Entity('Hello')
|
15 |
entity.metaClass.invokeMethod = { String name, Object args ->
|
19 |
assert entity.build(new Object()) == 'invoked'
|
20 |
assert entity.someMethod() == 'invoked'
|
关于MetaClass类的详细信息可以在MetaClass章节找到。
1.6 Categories
有这样一种场景,如果能让一个类的某些方法不受控制将会是很有用的。为了实现这种可能性,Groovy从Object-C借用实现了一个特性,叫做Categories。
Categories特性实现了所谓的category类,一个category类是需要满足某些特定的预定义的规则来定义一些拓展方法。
下面有几个categories是在Groovy环境中系统提供的一些额外功能:
Category类默认是不能使用的,要使用这些定义在一个category类的方法需要使用 use 方法,这个方法是GDK提供的一个内置于Groovy对象中的实例:
2 |
println 1.minute.from.now //(1)
|
5 |
def someDate = new Date() //(2)
|
6 |
println someDate - 3.months
|
(1) TimeCategory添加一个方法到Integer
(2) TimeCategory添加一个方法到Date
use 方法把category类作为第一个参数,一个闭包代码块作为第二个参数。在Closure里可以访问catetory。从上面的例子可以看到,即便是JDK的类,比如java.lang.Integer或java.util.Date也是可以被包含到用户定义的方法里的。
一个category不需要直接暴露给用户代码,下面的示例说明了这一点:
02 |
// Let's enhance JPA EntityManager without getting into the JSR committee
|
03 |
static void persistAll(EntityManager em , Object[] entities) { //add an interface to save all
|
04 |
entities?.each { em.persist(it) }
|
08 |
def transactionContext = { |
09 |
EntityManager em, Closure c ->
|
10 |
def tx = em.transaction
|
20 |
//cleanup your resource here
|
24 |
// user code, they always forget to close resource in exception, some even forget to commit, let's not rely on them. |
25 |
EntityManager em; //probably injected |
26 |
transactionContext (em) { |
27 |
em.persistAll(obj1, obj2, obj3)
|
28 |
// let's do some logics here to make the example sensible
|
29 |
em.persistAll(obj2, obj4, obj6)
|
如果我们去看groovy.time.TimeCategory类的嗲吗我们会发现拓展方法都是被声明为static方法。事实上,一个category类的方法要能被成功地加到use代码块里必须要这样写:
01 |
public class TimeCategory { |
03 |
public static Date plus(final Date date, final BaseDuration duration) {
|
04 |
return duration.plus(date);
|
07 |
public static Date minus(final Date date, final BaseDuration duration) {
|
08 |
final Calendar cal = Calendar.getInstance();
|
11 |
cal.add(Calendar.YEAR, -duration.getYears());
|
12 |
cal.add(Calendar.MONTH, -duration.getMonths());
|
13 |
cal.add(Calendar.DAY_OF_YEAR, -duration.getDays());
|
14 |
cal.add(Calendar.HOUR_OF_DAY, -duration.getHours());
|
15 |
cal.add(Calendar.MINUTE, -duration.getMinutes());
|
16 |
cal.add(Calendar.SECOND, -duration.getSeconds());
|
17 |
cal.add(Calendar.MILLISECOND, -duration.getMillis());
|
另外一个要求是静态方法的第一个参数必须定义类型,只要方法被激活。另外一个参数可以作为一个普通的参数当成方法的变量。
因为参数和静态方法的转变,category方法的定义可能比一般的方法定义不那么直观。不过Groovy提供了一个@Category注解,可以在编译时将一个类转化为category类。
03 |
String toString() { "${number}m" }
|
07 |
class NumberCategory { |
08 |
Distance getMeters() {
|
09 |
new Distance(number: this)
|
13 |
use (NumberCategory) { |
14 |
assert 42.meters.toString() == '42m'
|
使用@Category注解可以直接使用示例方法二不必将目标类型作为第一个参数的好处。目标类型类在注解里作为了一个参数。
在编译时元编程章节里有@Category的详细说明。
1.7 MetaClasses
(TBD)
1.7.1 Custom metaclasses
(TBD)
Delegating metaclass
(TBD)
Magic package(Maksym Stavyskyi)
(TBD)
1.7.2 Per instance metaclass
(TBD)
时间: 2024-10-02 03:45:37