Groovy源代码分析(十)

2021SC@SDUSC

运行时元编程(中)

GroovyInterceptable

groovy.lang.GroovyInterceptable接口是用于通知Groovy运行时的扩展GroovyObject的标记接口,所有方法都应通过Groovy运行时的方法分派器机制拦截。

package groovy.lang;

public interface GroovyInterceptable extends GroovyObject {
}

当Groovy对象实现GroovyInterceptable接口时,对任何方法的调用都会调用invokeMethod()方法。 下面你可以看到一个这种类型的对象的简单例子:

class Interception implements GroovyInterceptable {

    def definedMethod() { }

    def invokeMethod(String name, Object args) {
        'invokedMethod'
    }
}

下面这段代码是一个测试,显示对现有和不存在的方法的调用都将返回相同的值。

class InterceptableTest extends GroovyTestCase {

    void testCheckInterception() {
        def interception = new Interception()

        assert interception.definedMethod() == 'invokedMethod'
        assert interception.someMethod() == 'invokedMethod'
    }
}

我们不能使用默认groovy方法,如println,因为这些方法被注入到所有groovy对象中,所以它们也会被拦截。

如果我们要拦截所有方法调用,但不想实现GroovyInterceptable接口,我们可以在对象的MetaClass上实现invokeMethod()。 此方法适用于POGO和POJO,如此示例所示:

class InterceptionThroughMetaClassTest extends GroovyTestCase {

    void testPOJOMetaClassInterception() {
        String invoking = 'ha'
        invoking.metaClass.invokeMethod = { String name, Object args ->
            'invoked'
        }

        assert invoking.length() == 'invoked'
        assert invoking.someMethod() == 'invoked'
    }

    void testPOGOMetaClassInterception() {
        Entity entity = new Entity('Hello')
        entity.metaClass.invokeMethod = { String name, Object args ->
            'invoked'
        }

        assert entity.build(new Object()) == 'invoked'
        assert entity.someMethod() == 'invoked'
    }
}

Categories分类

有些情况下,如果想为一个无法控制的类添加额外的方法,Categories就派上用场了。 为了启用此功能,Groovy实现了一个从Objective-C借用的功能,称为Categories。

Categories使用所谓的Category 类实现。Category 类是特殊的类,因为它需要满足用于定义扩展方法的某些预定义规则。

系统中包括几个Categories,用于向类添加功能,以使这些类在Groovy环境中更易于使用:

groovy.time.TimeCategory
groovy.servlet.ServletCategory
groovy.xml.dom.DOMCategory

默认情况下不启用Category 类。 要使用Category 类中定义的方法,需要使用由GDK提供并可从每个Groovy对象实例中获取的use方法:

use(TimeCategory)  {
    println 1.minute.from.now       //TimeCategory给Integer添加了方法
    println 10.hours.ago

    def someDate = new Date()       //TimeCategory将方法添加到Date
    println someDate - 3.months
}

use方法将Category 类作为其第一个参数,将闭包代码块作为第二个参数。 在Closure内可以访问category的方法。 从上面的例子可以看出,甚至像java.lang.Integerjava.util.Date这样的JDK中的类也可以用用户定义的方法来丰富功能。

一个category不需要直接暴露给用户代码,如下:

class JPACategory{
    //让我们在不修改JPA EntityManager的前提下,增强JPA EntityManager的功能
    static void persistAll(EntityManager em , Object[] entities) { //添加一个接口用于保存所有entities
        entities?.each { em.persist(it) }
    }
}

def transactionContext = {
    EntityManager em, Closure c ->
    def tx = em.transaction
    try {
        tx.begin()
        use(JPACategory) {
            c()
        }
        tx.commit()
    } catch (e) {
        tx.rollback()
    } finally {
        //清理你的资源
    }
}

// 用户代码,他们总是忘记关闭异常中的资源,有些甚至忘记提交,让我们不要依赖他们。
EntityManager em; //可能注射
transactionContext (em) {
   em.persistAll(obj1, obj2, obj3)
   // let's do some logics here to make the example sensible
   //让我们在这里做一些逻辑,使示例理智
   em.persistAll(obj2, obj4, obj6)
}

当我们看一下groovy.time.TimeCategory类,我们看到扩展方法都被声明为static方法。 事实上,这是类方法必须满足的要求之一,它的方法被成功地添加到use代码块中的类中:

public class TimeCategory {

    public static Date plus(final Date date, final BaseDuration duration) {
        return duration.plus(date);
    }

    public static Date minus(final Date date, final BaseDuration duration) {
        final Calendar cal = Calendar.getInstance();

        cal.setTime(date);
        cal.add(Calendar.YEAR, -duration.getYears());
        cal.add(Calendar.MONTH, -duration.getMonths());
        cal.add(Calendar.DAY_OF_YEAR, -duration.getDays());
        cal.add(Calendar.HOUR_OF_DAY, -duration.getHours());
        cal.add(Calendar.MINUTE, -duration.getMinutes());
        cal.add(Calendar.SECOND, -duration.getSeconds());
        cal.add(Calendar.MILLISECOND, -duration.getMillis());

        return cal.getTime();
    }

    
}

另一个要求是静态方法的第一个参数必须定义该方法一旦被激活时附加的类型。 其他参数是方法将作为参数的正常参数。

由于参数和静态方法约定,category方法定义可能比正常的方法定义更不直观。 作为替代Groovy带有一个@Category注解,在编译时将加了注解的类转换为category 类。

class Distance {
    def number
    String toString() { "${number}m" }
}

@Category(Number)
class NumberCategory {
    Distance getMeters() {
        new Distance(number: this)
    }
}

use (NumberCategory)  {
    assert 42.meters.toString() == '42m'
}

应用@Category注解具有能够使用没有目标类型作为第一个参数的实例方法的优点。 目标类型class作为注释的参数提供。

上一篇:Groovy源代码分析(十一)


下一篇:groovy生成Xml报文