前言
在上一篇中,主要讲解了Groovy类、方法与闭包详解,在这一篇中将会对Groovy 动态特性及元编程进行详解。
1. Groovy 动态特性
说起动态特性,在第一篇了解Gradle及自动化构建里,仅仅是一笔带过,没有详解,在这里就要先从动态语言与静态语言的区别开始说起:
-
动态类型语言 动态类型语言是指在运行期间才去做数据类型检查的语言,也就是说,在用动态类型的语言编程时,可以不用给变量指定数据类型,该语言会在你第一次赋值给变量时,在内部将数据类型记录下来。Python和Ruby这些就是一种典型的动态类型语言
-
静态类型语言 静态类型语言与动态类型语言刚好相反,它的数据类型是在编译其间检查的,也就是说在写程序时要声明所有变量的数据类型,C/C++是静态类型语言的典型代表,其他的静态类型语言还有C#、Java等。
各种类型语言
一大堆理论说完了,接下来我们直接开始愉快的撸码环节
先看咱们所熟悉的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讲解了。