Groovy笔记(六)之元编程

本文介绍了Groovy的元编程概念,包括元对象协议(MOP)、GroovyObject接口及其方法,如invokeMethod、getProperty等。重点讨论了ExpandoMetaClass的使用,如何动态添加和修改方法、属性、构造函数。同时提到了GroovyInterceptable接口以及类别(Categories)的运用,展示了Groovy如何扩展静态和非静态类的能力。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

元对象协议(MOP)

  1. 在java中使用反射可以在运行时探索程序的结果,类、方法、参数等。但是我们仍然局限于所创建的静态结果。无法在运行时修改一个对象的类型,或是让它动态获得行为。如果可以基于应用的动态状态或基于应用所接受的输入,动态地添加方法和行为,代码会变得更灵活,在groovy中元编程就提供了这一功能
  2. 元编程意味着编写能够操作程序的程序,包括操作程序自身。像Groovy这样的动态语言通过元对象协议(MOP:MetaObject Protocol)提供了这种能力。在Groovy中使用MOP可以动态调用方法,甚至在运行时合成类和方法。
  3. groovy对象是带有附加功能的java对象,在Groovy中,Groovy对象比编译好的java对象具有更多的动态行为。此外,对于java对象和Groovy对象上的方法调用,Groovy的处理方式也是不同的。
  4. POJO(java对象)、POGO(groovy编写的对象,扩展了java.lang.Object,同时也实现了groovy.lang.GroovyOBject接口)、Groovy拦截器是扩展了GroovyInterceptable的Groovy对象,具有方法拦截功能
  5. GroovyInterceptable是一个标记接口,对于实现了该接口的对象而言,其上的所有方法调用,不管是存在的还是不存在的,都会被它的invokeMethod()方法拦截。
  6. 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接口

  1. groovy.lang.GroovyObject是Groovy中的主要接口,就像Object类是Java中的主要接口一样。GroovyObjectgroovy.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

  1. 当调用的方法不存在于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

  1. 对属性的每个读取访问都可以通过覆盖当前对象的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

  1. 访问对象的metaClass或设置自己的MetaClass实现以更改默认拦截机制。 例如,您可以编写自己的MetaClass接口实现,并将其分配给对象,从而更改拦截机制:

        // getMetaclass
        someObject.metaClass
        
        // setMetaClass
        someObject.metaClass = new OwnMetaClassImplementation()
    

get/setAttribute

  1. 此功能与MetaClass实现相关。 在默认实现中,您可以访问字段而不调用它们的gettersetter

        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

  1. Groovy支持methodMissing的概念。 此方法与invokeMethod的不同之处在于,它仅在失败的方法分派的情况下被调用,当没有找到给定名称和/或给定参数的方法时。通常当使用methodMissing时,可以缓存结果,以便下次调用相同的方法。

  2. 当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

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

        public interface GroovyInterceptable extends GroovyObject {
        }
    
  2. 当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()
    
    
  3. 如果我们要拦截所有方法调用,但不想实现GroovyInterceptable接口,我们可以在对象的MetaClass上实现invokeMethod(),此方法适用于POGO和POJO

        String hello = 'hello'
        hello.metaClass.invokeMethod = { String name, Object args ->
            'invoked'
        }
        
        
        //调用字符串不存在的方法,输出:invoked
        println hello.run()
    

Categories

  1. 有些情况下,如果想为一个无法控制的类添加额外的方法,Categories就派上用场了。Categories使用所谓的Category 类实现。Category 类是特殊的类,因为它需要满足用于定义扩展方法的某些预定义规则。

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

        groovy.time.TimeCategory
        groovy.servlet.ServletCategory
        groovy.xml.dom.DOMCategory
    
    
  3. 默认情况下不启用Category类。 要使用Category类中定义的方法,需要使用由GDK提供并可从每个Groovy对象实例中获取的use方法。use方法将Category类作为其第一个参数,将闭包代码块作为第二个参数。 在Closure内可以访问Category的方法。 从上面的例子可以看出,甚至像java.lang.Integerjava.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
        }
    
    
  4. 自定义

    
        /**
         * 1. Category类的方法必须是静态方法
         * 2. 静态方法的第一个参数必须定义该方法一旦被激活时附加的类型
         */
        class StringCategory{
            def static run(String str){
                println "run"
            }
        }
        
        
        use(StringCategory){
           "aaa".run()
        }
    
    
  5. 使用@Category注解,在编译时将加了@Category注解的类转换为Category类。应用@Category注解具有能够使用没有目标类型作为第一个参数的实例方法的优点

        @Category(String)
        class StringCategory2 {
            //方法不需要指定String类型,也不需要static关键字
            def run() {
                println "run"
            }
        }
        use(StringCategory2) {
            "aaa".run()
        }    
    

Metaclasses

  1. Metaclasses在方法解析中起到核心作用,对于来自groovy代码的每个方法调用,Groovy将找到给定对象的MetaClass,并通过MetaClass#invokeMethod将方法解析委托给Metaclasses,不应该与GroovyObject#invokeMethod混淆,后者恰好是Metaclasses最终可能调用的方法。

  2. 默认情况下,对象获取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

  1. Groovy有一个特殊的MetaClass,它就是groovy.lang.ExpandoMetaClass,它允许通过使用一个整洁的闭包语法动态添加或更改方法,构造函数,属性,甚至静态方法。
  2. 默认情况下,ExpandoMetaClass不进行继承。 要启用此功能,必须在应用程序启动之前调用ExpandoMetaClass #enableGlobally(),例如在main方法或servlet引导程序中。

方法

  1. 一旦通过调用metaClass属性访问ExpandoMetaClass,可以使用左移<<=运算符来添加方法。

  2. 注意,左移位运算符用于附加一个新方法。 如果类或接口声明了具有相同名称和参数类型的公共方法,包括继承自父类和父接口但不包括在运行时添加到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()
    

属性

  1. 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
      
      

构造方法

  1. 通过使用特殊的构造函数属性来添加构造函数。可以使用<<=运算符来分配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
    

静态方法

  1. 可以使用与实例方法相同的技术添加静态方法,并在方法名称之前添加静态限定符。

    
        class Book {
            String title
        }
        Book.metaClass.static.create << { String title -> new Book(title:title) }
        def book4 = Book.create("Python")
        println book4.title
    

借用方法

  1. 使用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()
    

动态方法名称

  1. 由于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
    

运行时发现

  1. 在运行时,知道在执行该方法时存在什么其他方法或属性通常是有用的。

        getMetaMethod
        hasMetaMethod
        getMetaProperty
        hasMetaProperty
    
    
  2. 为什么你不能只使用反射?因为Groovy是不同的,它具有只在运行时可用的"真实"方法方法。 这些有时(但不总是)表示为MetaMethods。 MetaMethods告诉您运行时可用的方法,因此您的代码可以适应

GroovyObject方法

  1. ExpandoMetaClass的另一个特点是它允许重写方法invokeMethodgetPropertysetProperty,所有这些都可以在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() 
    
    

查询方法和属性

  1. 在运行时,可以查询一个对象的方法和属性,以确定该对象是否支持某一个特定行为,对于要在运行时动态添加的行为,这一点尤为有用,不仅可以向类添加行为,还可以向类的一些实例添加行为。

        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()检查属性
                 */
        
        
            }
        }
    
  2. 动态访问对象: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}
            }
        }
    
    
    

扩展模块

扩展现有的类

  1. 扩展模块允许您向现有类添加新方法,包括预编译的类,如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));
            }
    
  2. 扩展方法是在辅助类(其中定义了各种扩展方法)中使用静态方法定义的。getText方法的第一个参数对应于接收者,而附加参数对应于扩展方法的参数。 所以这里,我们在File类上定义一个名为getText的方法(因为第一个参数是File类型),它接受一个参数作为参数(编码String)。

  3. 创建扩展模块的过程

    • 编写一个类似上面的扩展类
    • 写一个模块描述符文件
    • 必须让扩展模块对Groovy可见
      • 直接在类路径上提供类和模块描述符
      • 将您的扩展模块捆绑到一个jar中以便重用
  4. 扩展模块可以向类添加两种方法

    • 实例方法(要在类的实例上调用)
    • 静态方法(要在类本身上调用)

定义步骤

  1. 需求:在Integer中添加一个maxRetries方法,该方法接受一个闭包并执行它最多n次,直到没有抛出异常

  2. 定义扩展类

        // 扩展类
        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!'
            }
        }
    
    
    
  3. 要使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
      
  4. 调用

    
        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()
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值