Android Gradle—Groovy动态特性及元编程

·  阅读 545

前言

在上一篇中,主要讲解了Groovy类、方法与闭包详解,在这一篇中将会对Groovy 动态特性及元编程进行详解。

1. Groovy 动态特性

说起动态特性,在第一篇了解Gradle及自动化构建里,仅仅是一笔带过,没有详解,在这里就要先从动态语言与静态语言的区别开始说起:

  • 动态类型语言

动态类型语言是指在运行期间才去做数据类型检查的语言,也就是说,在用动态类型的语言编程时,可以不用给变量指定数据类型,该语言会在你第一次赋值给变量时,在内部将数据类型记录下来。Python和Ruby这些就是一种典型的动态类型语言

  • 静态类型语言

静态类型语言与动态类型语言刚好相反,它的数据类型是在编译其间检查的,也就是说在写程序时要声明所有变量的数据类型,C/C++是静态类型语言的典型代表,其他的静态类型语言还有C#、Java等。

各种类型语言

图片1.png

一大堆理论说完了,接下来我们直接开始愉快的撸码环节

先看咱们所熟悉的java代码

public class JavaTest2 {
    static class User {
        String userName = "hqk";
        int age;
        void setName(String name) {
            System.out.println("setName(String name)");
            this.userName = name;
        }
        void setName(Object name) {
            System.out.println("setName(Object name)");
            this.userName = name.toString();
        }
    }
    public static void main(String[] args) {
        User user=new User();
        user.setName(123);
        user.setName("zzz");
    }
}
复制代码

这是一段非常简单的java代码,我们可以看到,在函数入口那,分别传入两种不同类型的数据源,为了代码不报错,额外重载了方法setName,但最终userName属性类型还是String。换句话说,变量在定义的时候(运行前),就已经决定了它的类型,要是不满足它,编译就不会让你通过

那继续看看我们现在所学的Groovy。

class User {
    def userName = 'hqk'
    String age

    void setName(String name) {
        println "setName(String name)"
        this.userName = name
    }

    void setName(Object name) {
        println "setName(Object name)"
        this.userName = name
    }
}

def user = new User()
println "赋值前"
println user.userName.class
println "\n"

println "赋值为object类型"
user.userName = new Object()
println user.userName.class
println "\n"

println "赋值为int类型"
user.userName = 123
println user.userName.class
println "\n"

println "赋值为User类型"
user.userName = new User()
println user.userName.class
println "\n"
复制代码

我们可以看到,用相同的逻辑也在Groovy这里也实现一遍,赋了不同值后也依次打印了该变量的类型,这样写代码没有错误提示信息,那么运行来看看效果。

赋值前
class java.lang.String


赋值为object类型
class java.lang.Object


赋值为int类型
class java.lang.Integer


赋值为User类型
class com.zee.gradle.groovy.User
复制代码

因为Groovy它是可以直接通过对象.变量直接赋值的,所以这里暂时没通过定义好的setName方法赋值,不过也能够看出这里通过def定义的userName这个变量将会随着值类型的改变而改变,它是动态的。这里直接赋值是这样的结果,那我们也和刚刚java方式一样调用setName方法试试。

println "下面将是通过setName方法赋值\n"
Object name = "hqk"
println "定义Object变量,值为String"
println name.class
user.setName(name)
println user.userName.class

println "\n"

println "定义Object变量,值为int"
name = 123
println name.class
user.setName(name)
println user.userName.class
复制代码

这里定义了一个Object类型的变量name,先是赋值了String类型的值,将对应的值通过setName方法赋值;随后将Object类型的变量name赋值了int类型的值,再次将对应的值通过setName方法赋值。我们来看看运行效果。

下面将是通过setName方法赋值

定义Object变量,值为String
class java.lang.String
setName(String name)
class java.lang.String


定义Object变量,值为int
class java.lang.Integer
setName(Object name)
class java.lang.Integer
复制代码

现在我们看到,一个Object类型的值,在赋了不同值的情况下,它的类型也发生了改变,随后调用了不同的setName方法。

那么现在问题来了,既然在Groovy里面变量能够随意变,难道这Groovy和JS一样是弱类型变量? 带着这样的问题,我们再次研究下Groovy代码。

class User {
    def userName = 'hqk'
    String age

    void setName(String name) {
        println "setName(String name)"
        this.userName = name
    }

    void setName(Object name) {
        println "setName(Object name)"
        this.userName = name
    }
}

def user = new User()
user.age = "123"
println user.age.class

user.age = 123
println user.age.class

user.age=new Object()
println user.age.class
复制代码

在这里我在Class里面定义了age变量,类型也完全确认为String类型,但是在下面赋值的时候,我赋值了不同类型的值,最终打印了赋值好的变量类型,接下来我们看看运行效果。

class java.lang.String
class java.lang.String
class java.lang.String
复制代码

这里我们看出当确定了是什么类型的时候,无论赋值类型怎么变,它都不会发生任何改变。这就说明了Groovy它是强类型变量。而通过def、Object定义变量,它自身的类型将会自动转为对应赋值对象的类型,并非弱类型变量。

在这里总结一下Groovy的动态特性:

  • 使用def/Object定义变量,类型由运行时对其赋值的类型类确定。
  • 可以使用MOP进行元编程

这里提到了MOP元编程,那么在这就要对应MOP元编程进行详解。

2. Groovy元编程

说起元编程,先说说纯Java能做的事:

在Java中可以通过反射,在运行时动态的获取类的属性,方法等信息,然后反射调用。但是没法直接做到往内中添加属性、方法和行为。( 需要通过动态字节码技术如ASM、javassist等技术来实现动态的修改class )

而在Groovy中可以直接使用MOP进行元编程,我们可以基于应用当前的状态,动态的添加或者改变类的方法和行为。比如在某个Groovy类中并没有实现某个方法,这个方法的具体操作由服务器来控制,使用元编程,为这个类动态添加方法,或者替换原来的实现,然后可以进行调用。

这里归纳总结就是一句话,Java能做的Groovy也能做,Groovy能做的Java不一定能做。而Groovy能做的事无非就是动态修改和动态注入,动态修改就涉及到拦截。所以接下来就从Groovy的拦截以及注入这两个方面详解。

2.1 Groovy拦截

2.1.1 通过 GroovyInterceptable 拦截

class Person implements GroovyInterceptable {

    def func() {
        System.out.println "I have a dream!"
    }

    @Override
    Object invokeMethod(String name, Object args) {
        //println "invokeMethod"
        System.out.println "$name invokeMethod"
        //respondsTo(name) //判断方法是否存在
        if (metaClass.invokeMethod(this, 'respondsTo', name, args)) {
            System.out.println "$name 方法存在"
            System.out.println "$name 执行前.."
            metaClass.invokeMethod(this, name, args)
            System.out.println "$name 执行后.."
        }
    }
}

new Person().func()
复制代码

代码解析

一看到这代码,不就是之前我讲解的静态代理与动态代理详解,简直是一模一样,只不过是以Java方式实现的(感兴趣的小伙伴阔以去看一下)。我们继续看看运行效果:

func invokeMethod
func 方法存在
func 执行前..
I have a dream!
func 执行后..
复制代码

注意:如果要使用这种方式拦截,必须要实现GroovyInterceptable 接口,重写invokeMethod方法。这种方式太麻烦了,那有没有不实现这个接口就能拦截的方式呢?所以就有了第二种

2.1.2 通过MetaClass进行方法拦截

class Person3 {
    def func() {
        System.out.println "I have a dream!"
    }
}

def person3 = new Person3()
person3.func()
// 这里拦截某个对象的某一个方法
person3.metaClass.func = {
    println "I have a new dream !!!"
}
person3.func()
复制代码

运行效果

I have a dream!
I have a new dream !!!
复制代码

从这里可以看出,通过 metaClass 可以直接修改对应方法里面的逻辑。它这样的方式等价于:

class Person3 {
    def func() {
        System.out.println "I have a dream!"
    }
}

def person3 = new Person3()
// 等价与实现拦截接口
person3.metaClass.invokeMethod = {
    String name, Object args ->// invokeMethod(String name, Object args)
        println "$name 被拦截"
}
person3.func()
new Person3().func()
new Person3().func()
new Person3().func()
new Person3().func()
new Person3().func()
复制代码

运行效果

func 被拦截
I have a dream!
I have a dream!
I have a dream!
I have a dream!
I have a dream!
复制代码

我们发现,这里仅仅是拦截了一次,并没有做到全局拦截,那么要想做到全局拦截应该怎样呢?

class Person3 {
    def func() {
        System.out.println "I have a dream!"
    }
}

def person3=new Person3()
person3.func()

Person3.metaClass.invokeMethod = {
    String name, Object args ->// invokeMethod(String name, Object args)
        println "$name 被拦截"
}

person3=new Person3()
person3.func()
new Person3().func()
new Person3().func()
new Person3().func()
new Person3().func()
new Person3().func()
复制代码

这里我们拦截前后,也多次实例化调用了对应方法,现在看看效果

I have a dream!
func 被拦截
func 被拦截
func 被拦截
func 被拦截
func 被拦截
func 被拦截
复制代码

这里我们能看出,拦截前,运行的之前的代码,拦截后就变成最新的逻辑了。到现在为止,我们拦截的都是自己创建的代码,那么系统类能否拦截呢?

String.metaClass.invokeMethod = {
    String name, Object args ->
        println "String.metaClass.invokeMethod"
        MetaMethod method = delegate.metaClass.getMetaMethod(name)
        if (method != null && name == 'toString') {
            "你被我拦截了,还想toString"
        }
}
println "hqk".toString()
复制代码

这里我们看到直接将String的toString方法给拦截了,看看运行效果

String.metaClass.invokeMethod
你被我拦截了,还想toString
复制代码

这里我们看出,并没有打印我们想要的字符串,而是打印了已经被拦截后的字符串。到这,Groovy拦截方式差不多讲完了,现在该讲解Groovy注入了。

2.2 Groovy注入

2.2.1 使用MetaClass来注入方法

class Person4 {
    def func() {
        System.out.println "I have a dream!"
    }
}

// 注入前:org.codehaus.groovy.runtime.HandleMetaClass
println Person4.metaClass 

Person4.metaClass.newFunc = {
    println "newFunc调用"
    //方法返回字符串
    "注入后,后续依然能够访问"
}
// 注入后:groovy.lang.ExpandoMetaClass
println Person4.metaClass 
def person=new Person4()
//注入后 尝试是否能够调用注入后的方法
println person.newFunc()
复制代码

这里我们看到,在Person4 里面并没有 newFunc 方法,于是我们通过 metaClass方式注入了新方法newFunc ,并且实现了对应的逻辑。来看看运行效果

org.codehaus.groovy.runtime.HandleMetaClass@ea27e34[groovy.lang.MetaClassImpl@ea27e34[class com.zee.gradle.groovy.Person4]]
groovy.lang.ExpandoMetaClass@27a0a5a2[class com.zee.gradle.groovy.Person4]
newFunc调用
注入后,后续依然能够访问
复制代码

这里我们看到,注入前和注入后的Class类型发生了改变,它Class类型变成了groovy.lang.ExpandoMetaClass类型,那么是否能直接通过ExpandoMetaClass来实现注入?

2.2.2 使用groovy.lang.ExpandoMetaClass来注入方法

class Person4 {
    def func() {
        System.out.println "I have a dream!"
    }
}

def emc = new ExpandoMetaClass(Person4)
emc.func2 = {
    println "func2"
}
emc.initialize()
Person4.metaClass = emc
new Person4().func2()
复制代码

这里直接实例化了 ExpandoMetaClass 对象,然后通过该对象进行了一系列操作。看看运行效果

func2
复制代码

这里依然能够正常调用注入后的方法

2.2.3 使用分类注入方法(写法一)

class StringUtils {
    static def isNotEmpty(String self) {
        println "isNotEmpty"
        self != null && self.length() > 0
    }
}

class StringUtils2 {
    static def isNotEmpty(Integer self) {
        println "isNotEmpty2"
        //self != null && self.length() > 0 
        //因为 println 没有返回值,所以这里返回为null,因此下面打印为null
    }
}

use(StringUtils, StringUtils2) {
    println "".isNotEmpty()
    println 1.isNotEmpty()
}
复制代码

运行效果

isNotEmpty
false
isNotEmpty2
null
复制代码

这里定义了对应的工具类,里面有对应的非空判断逻辑,通过use方式,直接将这两个类里面的方法注入到对应的系统类当中(String/Integer)。然后就可以在闭包里面就能直接调用对应注入好的方法。

除此之外还有木有其他方式呢?

2.2.4 使用分类注入方法(写法二)

@Category(Object)
class StringUtils2 {
    def isNotEmpty() {
        println "isNotEmpty2"
        if (this instanceof Integer){
            println "数字类型判断"
            return true
        }
        println "非数字类型判断"
        this != null && this.length() > 0
    }
}
use(StringUtils2) {
    println "".isNotEmpty()
    println "\n"
    println 1.isNotEmpty()
}
复制代码

这里我们看到使用了 @Category 注解里面放入了Object,表示在use闭包内,任意类型变量都拥有新的方法isNotEmpty。看看运行效果

isNotEmpty2
非数字类型判断
false


isNotEmpty2
数字类型判断
true
复制代码

这里我们看到通过注解的方式,依然能够成功注入,但是这两种写法只能在use闭包里面,通常用于临时判断,用完就丢的那种。

因为Groovy是动态语言,那么在程序运行中,如果对象里面没有对应的方法和成员变量,在外面强制访问是肯定会报错的。那么该如何避免呢?

2.2.5 关于不存在的方法、变量赋初始值操作

class Person5 {
    def username
    // 对不存在的变量进行get操作
    def propertyMissing(String name) {
        println "propertyMissing"
        if (name == 'age') {
            "19" // 返回默认值
        }
    }
    // 对不存在的变量进行set操作
    def propertyMissing(String name, def arg) {
        println "propertyMissing ${name} : arg${arg}"
        return "default"// 给与返回值
    }
    // 对不存在的方法进行初始值操作
    def methodMissing(String name, def arg) {
        println "$name methodMissing"
        if (name.startsWith 'getFather') {
            "zzzz"
        }
    }
}
def p = new Person5()
println p.age = 12
println "\n"
println p.age
println "\n"
println p.getFather()
复制代码

运行效果

propertyMissing age : arg12
12


propertyMissing
19


getFather methodMissing
zzzz
复制代码

这里可以看出,当对象里面没有对应方法和变量时,可以通过这样的方式避免报错,也可以针对不同的情况做不同的逻辑处理。

3.总结语

因为Gradle大多数都是用Groovy语言写的,所以学习Gradle必须要掌握部分Groovy语法。到这里,整个Groovy语法已经全部讲解完毕了。在下一篇文章中,将会真正开始Gradle讲解了。

分类:
Android
标签:
分类:
Android
标签: