对于元编程,这个概念可能比较陌生,如果说下面的这个场景,就会逐步引入元编程的思想:Java中的反射机制,通过反射拿到构造方法,调用类中的方法,属性,invoke;但是反射不能做的就是,能够在运行的时候,改变类的行为,比如往类中添加属性或者方法
当前以上的情景,使用字节码插桩技术(javassist、ASM等)实现,修改class文件,但是Groovy元编程就能够原生地实现这项技术
MOP 元编程
1 MOP协议
MOP,Meta Object Protocol,元对象协议,如果Groovy类中没有某个方法,那么就可以使用MOP元编程的方式,在运行的时候,将某个方法注入,动态调用这个方法,这个方法在源码中是看不到的
2 MOP的实现方式
2.1方法拦截
方法拦截,类似于Android中的Hook技术,在调用某个方法的时候,先捕获这个方法,然后改变这个方法执行的方式 ,在Groovy中,通过实现GroovyInterceptable,实现invokeMethod方法
class GroovyTest02 implements GroovyInterceptable{
def func(){
System.out.println("func")
}
@Override
Object invokeMethod(String name, Object args) {
System.out.println("$name invokeMethod")
}
static void main(String[] args) {
new GroovyTest02().func() // func invokeMethod
}
}
这个时候,调用func方法将不会再走func这个方法,而是走invokeMethod,其中name就是执行的func方法的名字,这也是AOP面向切面编程的思想
2.1.1 responseTo
在使用invokeMethod执行一个方法的时候,如何判断这个方法是否存在,就是使用responseTo来判断,这个方法返回一个方法列表,如果有这个方法,那么列表就不为空
@Override
Object invokeMethod(String name, Object args) {
System.out.println("$name invokeMethod")
respondsTo(name)
}
注意这里不能这么用,注解调用会造成无限递归造成栈溢出,应该使用metaClass
@Override
Object invokeMethod(String name, Object args) {
System.out.println("$name invokeMethod")
if(metaClass.invokeMethod(this,"respondsTo",name,args)){
System.out.println("$name 方法存在")
System.out.println("$name 方法执行前")
metaClass.invokeMethod(this,name,args)
}
}
func invokeMethod
func 方法存在 [28]
func 方法执行前
I'm 28 岁了
如果判断当前方法存在了,那么就可以执行invokeMethod方法,那么我在方法之前,就可以加上自己的业务逻辑
2.1.2 metaClass
因为GroovyInterceptable是实现了GroovyObject,而一个类也是实现了GroovyObject,那么可以直接拿到这个类的metaClass,做方法拦截
class GroovyTest03 {
def func() {
println("这是一个方法")
}
static void main(String[] args) {
def test = new GroovyTest03()
test.metaClass.func = {
//这里会覆盖已有方法的内容
println("这是被覆盖的一个方法")
}
test.func()
}
}
输出:这是被覆盖的一个方法
当拿到这个类的metaClass之后,如果拿到的方法没有,那么就会注入一个方法;如果当前方法已经存在了,那么传入的闭包就会覆盖之前方法
test.metaClass.func2 = {
//这里动态注入的一个方法
println("这是动态注入的一个方法")
}
test.func2()
那么既然这样能够覆盖已知的方法,那么在2.1.1中的invokeMethod同样也可以覆盖
def test = new GroovyTest03()
test.metaClass.invokeMethod = { name,parmas->
//这里会覆盖已有方法的内容
println("$name 方法被拦截")
}
test.func()
//这里又创建了几个类
new GroovyTest03().func()
new GroovyTest03().func()
new GroovyTest03().func()
func方法被拦截了
这是一个方法
这是一个方法
这是一个方法
这里其实跟实现了 GroovyInterceptable接口是一样的效果;这里只是针对test一个对象的方法拦截,如果想要对所有的对象进行拦截,那么就直接对这个GroovyTest03类的metaClass进行处理即可
GroovyTest03.metaClass.invokeMethod = { name,parmas->
//这里会覆盖已有方法的内容
println("$name 方法被拦截")
}
那么既然invokeMethod能够拦截到所有的方法,那么对于String类中的toString拦截做一下修改,也是可以的
String.metaClass.invokeMethod = {
name,params->
def method = getMetaClass().getMetaMethod(name,args)
if(method != null && method.name == 'toString'){
'toString 被修改了'
}
}
2.2 方法注入
在Kotlin中,是能够对一个类进行扩展的,这样的灵活性也非常便利;而在Groovy中,如果要对一个类进行扩展,可以使用方法注入的方式来实现
2.2.1 metaClass注入
在前面中,使用metaClass可以实现注入,如果当前类中没有这个方法,就会注入一个方法
def test = new GroovyTest03()
test.metaClass.func1 = {
System.out.println("新注入方法 func1")
}
test.func1()
新注入方法 func1
打印一下,注入前后,metaClass有什么变化
[groovy.lang.MetaClassImpl@8909f18[class GroovyTest03]]
[groovy.lang.ExpandoMetaClass@60704c[class GroovyTest03]]
可以看到,在注入之后,metaClass变为了ExpandoMetaClass,这就是第2种注入方式
2.2.2 ExpandoMetaClass注入
def emc = new ExpandoMetaClass(GroovyTest03)
emc.func1 = {
println("这是注入的一个方法")
}
emc.initialize()
GroovyTest03.metaClass = emc
new GroovyTest03().func1()
通过创建一个ExpandoMetaClass对象,其中传入的参数就是要扩展注入的类;最终当前的ExpandoMetaClass对象就赋值给扩展类的metaClass
2.2.3 分类注入
首先写一个工具类,用来定义某个方法,这也是在Java中常用的方式
static class StringUtils{
static def isEmpty(String self){
println("StringUtils isEmpty")
self == null || self.length() == 0
}
}
那么如何在对象中,调用这个扩展方法,使用use
use(StringUtils){
"123".isEmpty()
}
即便是在String类中有一个isEmpty方法,但是这样使用最终还是走的是StringUtils中方法;如果还有一个类中,存在相同的方法
static class StringUtils2{
static def isEmpty(String self){
println("StringUtils isEmpty2")
self == null || self.length() == 0
}
}
use(StringUtils,StringUtils2){
"123".isEmpty()
}
那么最终会调用的是后面StringUtils2中的isEmpty方法
上面的方式,每次都需要定义一个静态方法,还要另外一种写法,不需要使用静态方法
@Category(String)
static class StringUtils{
def isEmpty(){
println("StringUtils isEmpty")
this == null || this.length() == 0
}
}
使用Category注解,表明当前工具类中针对那个类做的扩展方法,isEmpty只能为String类提供扩展方法,使用this关键字就能代替入参
2.3 属性注入
属性注入跟方法注入稍微有点出入,Groovy并没有给出特定的api来注入新的参数,可以使用拦截属性的api来完成参数注入
class Person{
def username
}
def person = new Person()
println person.age
如果像上面的访问方式访问age属性肯定是访问不到的,通过propertyMissing拦截属性,给属性赋值默认值,或者直接给属性赋值
class Person{
def username
def propertyMissing(String name){
println "拦截到了属性 $name"
19
}
}
如果在代码中,选择给没有的属性赋值,则会走下面这个方法
def propertyMissing(String name,def args){
println "拦截到了属性 $name 参数 $args"
args
}
person.age = 21
拦截到了属性 age 参数 21
有propertyMissing,也会有methodMissing,当然这种只是为了做特殊处理,防止报错,并不能达到赋值的效果
3 Groovy语法的补充
3.1 快速取值
如果一个字符串,通过某个字符分割,往常都是通过split函数分割,得到一个char数组,然后从数组中根据index取出数据,那么Groovy怎么快捷取出数据呢?
def str = "com.google.bbb:appcompact:1.1.0"
def (group,name,version) = str.split(":")
println group
println name
println version
在Kotlin中也有对应的取值方式