在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))
混入的顺序相当重要。方法调用会向链条中的左侧传递。