MOP方法注入

在Groovy中,可以随时打开一个类。也就是说,可以动态地向类中添加方法,允许它们在运行时改变行为。能够修改类的行为,是元编程和Groovy元对象协议(MOP)的核心。

Groovy的MOP支持以下3种技术注入行为:

  • 分类(Category)
  • ExpandoMetaClass
  • Mixin

使用分类

Groovy的分类提供了一种可控的方法注入方式——方法注入的作用可以限定在一个代码块内。分类(category)是一种能够修改类的MetaClass的对象,而且这种修改仅在代码块的作用域和执行线程内有效,退出代码块时,一切恢复原状。分类可以嵌套,也可以在一个代码块内应用多个分类。

要使用为对象添加的新的方法,只需要调用一个特殊的方法——use()。use方法接受两个参数:一个分类,一个闭包代码块。注入的方法就在该代码块内生效。

class StringUtil {
    def static toSSN(self) {    //如果只想限制为String类型,则可以定义为toSSN(String self)
        if (self.size() == 9) {
            "${self[0..2]}-${self[3..4]}-${self[5..8]}"
        }
    }
}

use(StringUtil){
    println "123456789".toSSN()
    println new StringBuffer("987654321").toSSN()
}

try{
    println "123456789".toSSN()
}catch (Exception e){
    println e
}

其中,StringUtil就是分类。self参数会被指派为目标实例。注意toSSN方法的定义,它是静态的。Groovy的分类注入的方法是静态的,而且至少接受一个参数,第一个参数指向的是方法调用的目标。要注入的方法所需要的参数都放在后面。

@Category(String)
class StringUtilAnnotated {
    def toSSN() {
        if (size() == 9) {
            "${this[0..2]}-${this[3..4]}-${this[5..8]}"
        }
    }
}
use(StringUtilAnnotated) {
    println "123456789".toSSN()
}

另一种可供选择的Groovy分类语法。这样,Groovy编译器就可以帮我们去转换为最开始的那个代码定义了。然而,这样的写法会限定方法只能使用参数中指定的类型,除非我们用指定参数类型为Object。

//参数为闭包
class FindUtil{
    def static extractOnly(String self,closure){
        def result = ''
        self.each{
            if(closure(it)){result += it}
        }
        result
    }

    def static toSSN(self){
        if (self.size() == 9) {
            "${self[0..3]}-${self[4..5]}-${self[6..8]}"
        }
    }
}
use(FindUtil){
    println "121254123".extractOnly{
        it == '4' || it == '5'
    }
}

//use的参数可以为一个分类,或由一个分类组成的列表
use(StringUtil,FindUtil){
    str = '123456789'
    println str.toSSN() //存在相同的命名,use参数列表中的最后一个分类优先级最高
    println str.extractOnly{it == '8' || it=='1'}
}

//拦截已有的方法,并取代。(可以理解为重载)
class Helper{
    def static toString(String self){
        def method = self.metaClass.methods.find{it.name == 'toString'}
        '!!' + method.invoke(self,null) + '!!'
    }
}
use(Helper){
    println 'hello'.toString()
}

分类的限制:其作用限定在use()块内,所以也就限定于执行线程。因此注入的方法也是有限制的。注入的方法只能在use块内调用。多次进入和退出这个块时有代价的。每次进入时,Groovy都必须检查静态方法,并将其加入到新作用域的一个方法列表中。在块的最后还要清理该作用域。

什么情况下使用:调用不太频繁,而且想要分类这种可控的方法注入所提供的隔离性。

使用ExpandoMetaClass

通过向类MetaClass添加方法可以实现向类中注入方法。这种注入方法是全局可用的。

Integer.metaClass.daysFromNow = { ->
    Calendar today = Calendar.instance
    today.add(Calendar.DAY_OF_MONTH,delegate)
    today.time
}
println 5.daysFromNow()

以上代码用了一个闭包实现了daysFromNow(),然后将其引入到Integer的MetaClass中。如果想要引入的是一个属性XXX,那么我们需要一个getXXX()方法,如:

Integer.metaClass.getDaysFromNow={ ->
    Calendar today = Calendar.instance
    today.add(Calendar.DAY_OF_MONTH,delegate)
    today.time
}
println 5.daysFromNow

如果我们需要将一个方法注入到多个类中呢?

daysFromNow = { ->
    Calendar today = Calendar.instance
    today.add(Calendar.DAY_OF_MONTH, (int)delegate)
    today.time
}
Integer.metaClass.daysFromNow = daysFromNow
Long.metaClass.daysFromNow = daysFromNow

println 6.daysFromNow()
println 6L.daysFromNow()

这样,我们就可以向多个类中注入同一个方法了。同时,我们还可以向基类中注入方法,那么它的子类就直接拥有了该方法:

Number.metaClass.someMethod = {->
    println "someMethod called"
}
2.someMethod()
2L.someMethod()

如果向类中注入静态方法,只需要将其加入到MetaClass的static属性中:

Integer.metaClass.'static'.isEven = {val -> val % 2 == 0}
println 'Is 2 even? '+ Integer.isEven(2)
println 'Is 3 even? '+ Integer.isEven(3)

通过定义一个名为constructor的特殊属性可以加入构造器。因为我们是要添加一个构造器,而不是替换一个现有的,所以使用了<<操作符。注意:使用<<来覆盖现有的构造器或方法,Groovy会报错。

Integer.metaClass.constructor << { Calendar calendar ->
    new Integer(calendar.get(Calendar.DAY_OF_YEAR))
}
println new Integer(Calendar.instance)

如果向替换掉构造器,可以使用=。在被覆盖的构造器内仍然可以使用反射调用原来的实现。

Integer.metaClass.constructor = { int val ->
    println "Intercepting constructor call"
    constructor = Integer.class.getConstructor(Integer.TYPE)
    constructor.newInstance(val)
}
println new Integer(4)
println new Integer(Calendar.instance)

如果想要添加一堆的方法,ClassName.metaClass.method={…}的语法设置会让我们写起来很费劲。Groovy提供了一种可以将这些方法分组的方式,组织成一种叫作ExpandoMetaClass DSL的方便语法。

Integer.metaClass {
    daysFromNow = { ->
        Calendar today = Calendar.instance
        today.add(Calendar.DAY_OF_MONTH, delegate)
        today.time
    }

    getDaysFromNow = { ->
        Calendar today = Calendar.instance
        today.add(Calendar.DAY_OF_MONTH, delegate)
        today.time
    }

    'static' {
        isEven = { val -> val % 2 == 0 }
    }

    constructor = { Calendar calendar ->
        new Integer(calendar.get(Calendar.DAY_OF_YEAR))
    }

    constructor = {int val ->
        println "Intercepting constructor call"
        constructor = Integer.class.getConstructor(Integer.TYPE)
        constructor.newInstance(val)
    }
}

println new Integer(4)
println new Integer(Calendar.instance)
println 'Is 2 even? '+ Integer.isEven(2)
println 'Is 3 even? '+ Integer.isEven(3)
println 5.daysFromNow
println  5.daysFromNow()

注意这里的加入构造器使用=

使用ExpandoMetaClass注入的方法只能在Groovy代码内使用,不能从编译过的Java代码中调用,也不能从Java代码中通过方式来使用。

向具体的实例中注入方法

如果我们只想为某个实例注入方法,可以采用以下的方式:

class Person{
    def play(){
        println 'playing...'
    }
}

def emc = new ExpandoMetaClass(Person)
emc.sing = {->
    'oh baby baby...'
}
emc.initialize()

def jack = new Person()
def paul = new Person()

jack.metaClass = emc
println  jack.sing()

try{
    paul.sing()
}catch (ex){
    println ex
}

Groovy提供了一种方便的方式来从实例中去掉这些注入的方法——只需要将metaClass属性设置为null。

jack.metaClass = null
try{
    jack.sing()
}catch (ex){
    println ex
}

一种简单的方式:

class Person{
    def play(){
        println'playing'
    }
}

def jack = new Person()
def paul = new Person()

jack.metaClass.sing = {->
    'oh baby baby...'
}
println jack.sing()

jack.metaClass = null
try{
    jack.sing()
}catch (ex){
    println ex
}

再来一组语法糖:

jack.metaClass{
    sing = {->
        'oh baby baby...'
    }
    dance = {->
        'start the music...'
    }
}

使用Mixin注入方法

Groovy的Mixin是一种运行时的能力,可以将多个类中的实现引入进来或混入。

如果将一个类混入到另一个类中,Groovy会在内存中把这些类的类实例链接起来。当调用一个方法时,Groovy首先将调用路由到混入的类中,如果该方法存在于这个类中,则在此处理。否则由主类处理。可以将多个类混入到一个类中,最后加入的Mixin优先级最高。

class Friend{
    def listen(){
        "$name is listening as a friend"
    }
}

@Mixin(Friend)
class Human{
    String firstName
    String lastName
    String getName(){
        "$firstName $lastName"
    }
}

john = new Human(firstName: "John",lastName: "Smith")
println john.listen()

@Mixinz注解会将作为参数提供的类中的方法添加到被注解的类中。注解本身限制了这种方式只能由类提供者本身使用。

向已有的类中提供注入的语法:

class Dog{
    String name
}
Dog.mixin(Friend)

buddy = new Dog(name: "Buddy")
println buddy.listen()

//对某个实例注入
class Cat{
    String name
}
socks = new Cat(name:"Socks")
socks.metaClass.mixin(Friend)    //对该实例mixin
println socks.listen()

在类中使用多个Mixin

当混入多个类时,所有这些类的方法在目标类中都是可用的。当作为Mixin的两个或多个类中存在名字相同、参数签名也相同的方法时,最后加入到Mixin中的方法会隐藏掉已经注入的方法。

abstract class Writer {
    abstract void write(String message)
}

class StringWriter extends Writer {
    def target = new StringBuffer()

    @Override
    void write(String message) {
        target.append(message)
    }


    @Override
    public String toString() {
        return target.toString();
    }

}
def writeStuff(writer){
    writer.write("This is stupid")
    println writer
}

def create(theWriter,Object[] filters =[]){
    def instance = theWriter.newInstance()
    filters.each {filter -> instance.metaClass.mixin(filter)}
    instance
}

class UppercaseFilter{
    void write(String message){
        def allUpper = message.toUpperCase()
        invokeOnPreviousMixin(metaClass,"write",allUpper)
    }
}

Object.metaClass.invokeOnPreviousMixin = {
    MetaClass currentMixinMetaClass,String method,Object[] args ->
        def previousMixin = delegate.getClass()
        for(mixin in mixedIn.mixinClasses){
            if(mixin.mixinClass.theClass == currentMixinMetaClass.delegate.theClass){
                break;
            }
            previousMixin = mixin.mixinClass.theClass
        }
        mixedIn[previousMixin]."$method"(*args)
}

class ProfanityFilter{
    void write(String message){
        def filtered = message.replaceAll('stupid','s*****')
        invokeOnPreviousMixin(metaClass,"write",filtered)
    }
}

writeStuff(create(StringWriter,UppercaseFilter))
writeStuff(create(StringWriter,ProfanityFilter))
writeStuff(create(StringWriter,UppercaseFilter,ProfanityFilter))
writeStuff(create(StringWriter,ProfanityFilter,UppercaseFilter))

混入的顺序相当重要。方法调用会向链条中的左侧传递。