文章目录
元对象协议(MOP)
- 在java中使用反射可以在运行时探索程序的结果,类、方法、参数等。但是我们仍然局限于所创建的静态结果。无法在运行时修改一个对象的类型,或是让它动态获得行为。如果可以基于应用的动态状态或基于应用所接受的输入,动态地添加方法和行为,代码会变得更灵活,在groovy中元编程就提供了这一功能
- 元编程意味着编写能够操作程序的程序,包括操作程序自身。像Groovy这样的动态语言通过元对象协议(MOP:MetaObject Protocol)提供了这种能力。在Groovy中使用MOP可以动态调用方法,甚至在运行时合成类和方法。
- groovy对象是带有附加功能的java对象,在Groovy中,Groovy对象比编译好的java对象具有更多的动态行为。此外,对于java对象和Groovy对象上的方法调用,Groovy的处理方式也是不同的。
- POJO(java对象)、POGO(groovy编写的对象,扩展了java.lang.Object,同时也实现了groovy.lang.GroovyOBject接口)、Groovy拦截器是扩展了
GroovyInterceptable
的Groovy对象,具有方法拦截功能 GroovyInterceptable
是一个标记接口,对于实现了该接口的对象而言,其上的所有方法调用,不管是存在的还是不存在的,都会被它的invokeMethod()方法拦截。- Groovy支持POJO和POGO进行元编程,对于POJO,groovy维护了MetaClass的一个MetaClassRegistry。POGO有一个其到MetaClass的直接引用。当我们调用一个方法时,Groovy会检查目标对象是一个POJO还是一个POGO,对于不同的对象类型,Groovy的方法处理是不同的。对于POJO,Groovy会去应用类的MetaClassregistry取它的MetaClass,并将方法调用委托给它。因此我们在它的MetaClass上定义的任何拦截器或方法,都优先于POJO原来的方法
7.对于POGO,Groovy会采取一些额外的步骤。如果对象实现了GroovyInterceptable
接口,那么所有的调用都会被路由给它的invokeMethod()
,类似AOP
GroovyObject接口
-
groovy.lang.GroovyObject
是Groovy中的主要接口,就像Object类是Java中的主要接口一样。GroovyObject
在groovy.lang.GroovyObjectSupport
类中有一个默认实现,它负责将调用传递给groovy.lang.MetaClass
对象。 GroovyObject源代码如下:public interface GroovyObject { Object invokeMethod(String name, Object args); Object getProperty(String propertyName); void setProperty(String propertyName, Object newValue); MetaClass getMetaClass(); void setMetaClass(MetaClass metaClass); }
invokeMethod
-
当调用的方法不存在于Groovy对象上时,将调用此方法
class Person { def run() { println "run" } /** * 当您调用的方法不存在于Groovy对象上时,将调用此方法 */ def invokeMethod(String name, Object args) { println "called invokeMethod $name $args" } } def person = new Person() //输出:run person.run() //输出:called invokeMethod eat [] person.eat()
get/setProperty
-
对属性的每个读取访问都可以通过覆盖当前对象的getProperty()方法来拦截,覆盖setProperty()方法来拦截对属性的写访问权限:
class User { def username = "jannal" def age = 10 def idCard def getProperty(String name) { if (name != 'password') //将请求转发到getter以获取除password之外的所有属性。 return metaClass.getProperty(this, name) else return '123456' } def getAddress(){ return "Beijing" } //setProperty()方法来拦截对属性的写访问权限: void setProperty(String name, Object value) { this.@"$name" = '4574' } } def user = new User() //jannal println user.username //10 println user.age //Beijing println user.address //123456 println user.password user.idCard = "411522199111114578" //4574 println user.idCard
get/setMetaClass
-
访问对象的metaClass或设置自己的MetaClass实现以更改默认拦截机制。 例如,您可以编写自己的MetaClass接口实现,并将其分配给对象,从而更改拦截机制:
// getMetaclass someObject.metaClass // setMetaClass someObject.metaClass = new OwnMetaClassImplementation()
get/setAttribute
-
此功能与
MetaClass
实现相关。 在默认实现中,您可以访问字段而不调用它们的getter
和setter
class Customer { def username = "jannal" def age = 10 def getUsername(){ return "jannal2" } def setAge(def age){ this.age = "100" } } def customer = new Customer() //jannal2 println customer.username println customer.age //jannal println customer.metaClass.getAttribute(customer, 'username') //10 println customer.metaClass.getAttribute(customer, 'age') customer.metaClass.setAttribute(customer,'age',200) //200 println customer.age
methodMissing与propertyMissing
-
Groovy支持
methodMissing
的概念。 此方法与invokeMethod
的不同之处在于,它仅在失败的方法分派的情况下被调用,当没有找到给定名称和/或给定参数的方法时。通常当使用methodMissing时,可以缓存结果,以便下次调用相同的方法。 -
当Groovy运行时找不到给定属性的
getter
方法时才调用propertyMissing(String)
方法。class Foo { def methodMissing(String name, def args) { println "this is me" } static def $static_methodMissing(String name, Object args) { println "Missing static method name is $name" } static def $static_propertyMissing(String name) { println "Missing static property name is $name" } def propertyMissing(String name) { println name } def invokeMethod(String name, Object args) { println "called invokeMethod $name $args" } } /** * 调用一个不存在的方法,输出:this is me * 1. 为什么invokeMethod没有被调用呢??? * 2. propertyMissing与invokeMethod区别是什么??? */ new Foo().run() //调用一个不存在的属性,输出:age new Foo().age //静态方法 Foo.run() //静态属性丢失 Foo.age
GroovyInterceptable
-
groovy.lang.GroovyInterceptable
接口是用于通知Groovy运行时的扩展GroovyObject
的标记接口,所有方法都应通过Groovy运行时的方法分派器机制拦截public interface GroovyInterceptable extends GroovyObject { }
-
当Groovy对象实现
GroovyInterceptable
接口时,对任何方法的调用都会调用invokeMethod()
方法class Car implements GroovyInterceptable { def check() { System.out.println("check called") } def start() { System.out.println("start called") } def invokeMethod(String name, args) { System.out.println("call to $name intercepted") if (name != 'check') { System.out.println("开始调用检查") Car.metaClass.getMetaMethod('check').invoke(this, null) } def validMethod = Car.metaClass.getMetaMethod(name, args) if (validMethod != null) { validMethod.invoke(this, args) } else { Car.metaClass.invokeMethod(this, name, args) } } } def car = new Car() /** * call to start intercepted * 开始调用检查 * check called * start called */ car.start() /** * call to check intercepted * check called */ car.check()
-
如果我们要拦截所有方法调用,但不想实现
GroovyInterceptable
接口,我们可以在对象的MetaClass上实现invokeMethod()
,此方法适用于POGO和POJOString hello = 'hello' hello.metaClass.invokeMethod = { String name, Object args -> 'invoked' } //调用字符串不存在的方法,输出:invoked println hello.run()
Categories
-
有些情况下,如果想为一个无法控制的类添加额外的方法,Categories就派上用场了。Categories使用所谓的Category 类实现。Category 类是特殊的类,因为它需要满足用于定义扩展方法的某些预定义规则。
-
系统中包括几个Categories,用于向类添加功能,以使这些类在Groovy环境中更易于使用:
groovy.time.TimeCategory groovy.servlet.ServletCategory groovy.xml.dom.DOMCategory
-
默认情况下不启用
Category
类。 要使用Category
类中定义的方法,需要使用由GDK提供并可从每个Groovy
对象实例中获取的use
方法。use方法将Category
类作为其第一个参数,将闭包代码块作为第二个参数。 在Closure内可以访问Category
的方法。 从上面的例子可以看出,甚至像java.lang.Integer
或java.util.Date
这样的JDK中的类也可以用用户定义的方法来丰富功能。use(TimeCategory) { println 1.minute.from.now //TimeCategory给Integer添加了方法 println 10.hours.ago def someDate = new Date() //TimeCategory将方法添加到Date println someDate - 3.months }
-
自定义
/** * 1. Category类的方法必须是静态方法 * 2. 静态方法的第一个参数必须定义该方法一旦被激活时附加的类型 */ class StringCategory{ def static run(String str){ println "run" } } use(StringCategory){ "aaa".run() }
-
使用
@Category
注解,在编译时将加了@Category
注解的类转换为Category
类。应用@Category
注解具有能够使用没有目标类型作为第一个参数的实例方法的优点@Category(String) class StringCategory2 { //方法不需要指定String类型,也不需要static关键字 def run() { println "run" } } use(StringCategory2) { "aaa".run() }
Metaclasses
-
Metaclasses
在方法解析中起到核心作用,对于来自groovy代码的每个方法调用,Groovy将找到给定对象的MetaClass,并通过MetaClass#invokeMethod
将方法解析委托给Metaclasses
,不应该与GroovyObject#invokeMethod
混淆,后者恰好是Metaclasses
最终可能调用的方法。 -
默认情况下,对象获取
groovy.lang.MetaClassImpl
的实例,该实例实现默认方法查找。 这个方法查找包括查找对象类中的方法("常规"方法),但是如果没有找到方法,它将采用调用methodMissing
并最终调用GroovyObject#invokeMethod
class AA {} def aa = new AA() /** * org.codehaus.groovy.runtime.HandleMetaClass@8909f18[groovy.lang.MetaClassImpl@8909f18[class com.jannal.base.AA]] */ println aa.metaClass
ExpandoMetaClass
- Groovy有一个特殊的
MetaClass
,它就是groovy.lang.ExpandoMetaClass
,它允许通过使用一个整洁的闭包语法动态添加或更改方法,构造函数,属性,甚至静态方法。 - 默认情况下,
ExpandoMetaClass
不进行继承。 要启用此功能,必须在应用程序启动之前调用ExpandoMetaClass #enableGlobally()
,例如在main方法或servlet引导程序中。
方法
-
一旦通过调用
metaClass
属性访问ExpandoMetaClass
,可以使用左移<<
或=运算符
来添加方法。 -
注意,
左移位运算符
用于附加一个新方法。 如果类或接口声明了具有相同名称和参数类型的公共方法,包括继承自父类和父接口但不包括在运行时添加到metaClass
的那些方法,那么将抛出异常。 如果要替换由类或接口声明的方法,可以使用=运算符
。class Book { String title } /** * 1. Closure代码块将功能应用于metaClass的不存在的属性上 * 2. 通过访问metaClass属性并使用<<或=运算符来分配一个Closure代码块来将新方法添加到类中。 * 3. Closure参数被解释为方法参数。 无参数方法可以使用{-> ...}语法添加 */ Book.metaClass.titleInUpperCase << { -> title.toUpperCase() } def b = new Book(title: "Thinking In Java") //输出:THINKING IN JAVA println b.titleInUpperCase()
属性
ExpandoMetaClass
支持两种机制来添加或覆盖属性。-
通过向metaClass的属性赋值来声明一个可变属性
class Book { String title } Book.metaClass.author = "jannal" def book = new Book() //输出:jannal println book.author
-
通过使用添加实例方法的标准机制来添加getter和/或setter方法
class Book { String title } Book.metaClass.getAuthor << {-> "jannal" } def book = new Book() //输出:jannal println book.author
-
添加一个等效的setter方法
//setter def properties = Collections.synchronizedMap([:]) Book.metaClass.setPrice = { String value -> properties[System.identityHashCode(delegate) + "price"] = value } Book.metaClass.getPrice = {-> properties[System.identityHashCode(delegate) + "price"] } def book2 = new Book() book2.price = "1000" //1000 println book2.price
-
构造方法
-
通过使用特殊的构造函数属性来添加构造函数。可以使用
<<
或=
运算符来分配Closure代码块。当添加构造函数时要小心,因为它很容易陷入堆栈溢出问题。class Book { String title } //添加一个构造函数 Book.metaClass.constructor << { String title -> new Book(title:title) } def book3 = new Book('Groovy in Action') //Groovy in Action println book3.title
静态方法
-
可以使用与实例方法相同的技术添加静态方法,并在方法名称之前添加静态限定符。
class Book { String title } Book.metaClass.static.create << { String title -> new Book(title:title) } def book4 = Book.create("Python") println book4.title
借用方法
-
使用ExpandoMetaClass,可以使用Groovy的方法指针语法从其他类中借用方法
class Book { String title } class Duck { def fly() { "duck fly" } } def duck = new Duck(); Book.metaClass.fly2 = duck.&fly def book5 = new Book() println book5.fly2()
动态方法名称
-
由于Groovy允许使用Strings作为属性名,这反过来允许您在运行时动态创建方法(实例方法或者静态方法)和属性名(普通或者静态)。 要创建具有动态名称的方法,只需使用引用属性名称的语言特性作为字符串。
class Duck { def name } //动态方法名称 def methodName = "Jannal" Duck.metaClass."changeNameTo${methodName}" = { -> delegate.name = "jannal" } def duck2 = new Duck() duck2.changeNameToJannal() println duck2.name
运行时发现
-
在运行时,知道在执行该方法时存在什么其他方法或属性通常是有用的。
getMetaMethod hasMetaMethod getMetaProperty hasMetaProperty
-
为什么你不能只使用反射?因为Groovy是不同的,它具有只在运行时可用的
"真实"方法
和方法
。 这些有时(但不总是)表示为MetaMethods。 MetaMethods告诉您运行时可用的方法,因此您的代码可以适应
GroovyObject方法
-
ExpandoMetaClass
的另一个特点是它允许重写方法invokeMethod
,getProperty
和setProperty
,所有这些都可以在groovy.lang.GroovyObject
类中找到。class Stuff { def invokeMe() { "foo" } } Stuff.metaClass.invokeMethod = { String name, args -> def metaMethod = Stuff.metaClass.getMetaMethod(name, args) def result if (metaMethod) { result = metaMethod.invoke(delegate, args) } else { //找不到返回一个虚拟值 result = "bar" } } def stf = new Stuff() //foo println stf.invokeMe() //bar println stf.doStuff()
查询方法和属性
-
在运行时,可以查询一个对象的方法和属性,以确定该对象是否支持某一个特定行为,对于要在运行时动态添加的行为,这一点尤为有用,不仅可以向类添加行为,还可以向类的一些实例添加行为。
class MetaData { public static void main(String[] args) { def str = "hello" //方法名可能来自动态输入 def methodName = "toUpperCase"; //metaMethod表示java对象上的一个方法,类似java反射的Method def toUpperCase = str.metaClass.getMetaMethod(methodName) // 动态调用的方法产生的输出 HELLO println toUpperCase.invoke(str); //respondsTo 返回 List<MetaMethod> // [public java.lang.String java.lang.String.toUpperCase(), public java.lang.String java.lang.String.toUpperCase(java.util.Locale)] println String.metaClass.respondsTo(str, 'toUpperCase') // respondsTo检查方法是否存在 yes yes no。 条件运算符?:,如果存在方法则返回true println String.metaClass.respondsTo(str, 'toUpperCase') ? 'yes' : 'no' println String.metaClass.respondsTo(str, 'compareTo', 'test') ? 'yes' : 'no' println String.metaClass.respondsTo(str, 'toUpperCase', 5) ? 'yes' : 'no' /** * 如果要找一个static方法,可以使用getStaticMetaMethod() * 要获得重载方法的列表getMetaMethods()和getStaticMetaMethods * getMetaProperty()和getStaticMetaProperty()可以获得一个元属性 * ResponseTo()检查方法是否存在 hasProperty()检查属性 */ } }
-
动态访问对象:Groovy中有比较方便的访问属性和调用方法的方式
class MetaDataObject { public static void main(String[] args) { def strObj="hello" def userRequsetProperty = "bytes" def userRequestMethod ='toUpperCase' //访问属性输出 [104, 101, 108, 108, 111] println strObj[userRequsetProperty] println strObj."$userRequsetProperty" //调用方法输出 HELLO println strObj."$userRequestMethod"() println strObj.invokeMethod(userRequestMethod,null); /** * 迭代一个对象的所有属性,可以使用properties属性或者getProperties()方法 * 输出: * class=class java.lang.String * bytes=[B@7a187f14 * empty=false */ strObj.properties.each {println it} } }
扩展模块
扩展现有的类
-
扩展模块允许您向现有类添加新方法,包括预编译的类,如JDK中的类。 这些新方法与通过元类或使用类定义的方法不同,在全局范围内可用。
File
类中不存在getText
方法。 但是,Groovy可以使你调用它,因为它在一个特殊的类org.codehaus.groovy.runtime.ResourceGroovyMethods
中定义://标准扩展方法 def file = new File(...) def contents = file.getText('utf-8') //ResourceGroovyMethods中定义了getText方法 public static String getText(File file, String charset) throws IOException { return IOGroovyMethods.getText(newReader(file, charset)); }
-
扩展方法是在辅助类(其中定义了各种扩展方法)中使用静态方法定义的。
getText
方法的第一个参数对应于接收者,而附加参数对应于扩展方法的参数。 所以这里,我们在File类上定义一个名为getText的方法(因为第一个参数是File类型),它接受一个参数作为参数(编码String)。 -
创建扩展模块的过程
- 编写一个类似上面的扩展类
- 写一个模块描述符文件
- 必须让扩展模块对Groovy可见
- 直接在类路径上提供类和模块描述符
- 将您的扩展模块捆绑到一个jar中以便重用
-
扩展模块可以向类添加两种方法
- 实例方法(要在类的实例上调用)
- 静态方法(要在类本身上调用)
定义步骤
-
需求:在Integer中添加一个maxRetries方法,该方法接受一个闭包并执行它最多n次,直到没有抛出异常
-
定义扩展类
// 扩展类 class MaxRetriesExtension { //静态方法的第一个参数对应于消息的接收者,也就是说扩展实例 static void maxRetries(Integer self, Closure code) { assert self >= 0 int retries = self Throwable e = null while (retries > 0) { try { code.call() break } catch (Throwable err) { e = err retries-- } } if (retries == 0 && e) { throw e } } } //静态扩展类 class StaticStringExtension { //静态方法的第一个参数对应于正在扩展并且未使用的类 static String greeting(String self) { 'Hello, world!' } }
-
要使Groovy能够加载您的扩展方法,必须声明扩展帮助器类。 您必须在
META-INF/services
目录中创建一个名为org.codehaus.groovy.runtime.ExtensionModule
的文件-
moduleName:模块的名称
-
moduleVersion:您的模块的版本。 请注意,版本号仅用于检查您不加载同一模块在两个不同的版本。
-
extensionClasses:实例方法的扩展辅助类列表。 您可以提供几个类,假定它们是逗号分隔的。
-
staticExtensionClasses:静态方法的扩展辅助类列表。 您可以提供几个类,假定它们是逗号分隔的
-
请注意,模块不需要定义静态辅助函数和实例辅助函数,并且可以向单个模块添加多个类。 您还可以在单个模块中扩展不同的类,而不会有问题。 甚至可能在单个扩展类中使用不同的类,但建议按扩展类将扩展方法分组。
moduleName=Test module for specifications moduleVersion=1.0 extensionClasses=com.jannal.base.MaxRetriesExtension staticExtensionClasses=com.jannal.base.StaticStringExtension
-
-
调用
int i = 0 5.maxRetries { i++ } assert i == 1 i = 0 try { 5.maxRetries { i++ throw new RuntimeException("oops") } } catch (RuntimeException e) { assert i == 5 } println String.greeting()