【语言学习】Groovy 进阶 - hippowc/hippowc.github.io GitHub Wiki
GDK
Groovy对Object的扩展
- dump inspect
// dump 获取类实例的相关信息,用于调试、日志,会打印字段、类型、hash等
'str'.dump()
// inspect 说明创建一个对象需要提供什么
- with 支持创建一个上下文,在with作用域的方法都会定向该对象,作用同:identity
// 原理是设置了闭包的delegate属性,有个坑,this和owner仍然是所在类对象,如果所在类对象有属性和方法,还是首先使用所在类的
- 间接调用属性,通过. [] 访问属性 car.color, car['color']
- 间接调用方法,invokeMethod
car.invokeMethod('mehthod', [2,'test'] as Object[])
轻量级的使用
- 安全导航符 ?.
- 异常处理 groovy不强制处理异常,会自动向上抛出,处理catch可以省略异常类型
- return和分号可省略
- 方法和类默认是public
- 静态方法可以使用this引用class对象,便于使用链式对象
- 具名参数
// 将第一个参数设为Map,map参数可以展开传递,实现具名参数的效果
def access(Map l, int weight)
access(x:20,y:10, 10) //前两个参数为map展开
- 可选形参,在定义形参的时候赋值
def log(x,base = 10) {...}
- 多赋值,从方法中返回多个结果,并赋值多个变量
// 返回数组,接受变量放在()中逗号分隔
def (firstname, lastname) = "xxx xxx".split(" ")
- 实现接口 可以将一个映射或一个代码块转换为接口实现
// 使用闭包 + as 接口名,可以得到一个接口的实现类对象
{} as interfaceName -- 是个对象
// 接口的多个方法可以使用map统一处理
map = [xx: {}, yy: {}]
map as interfaceName
- 还有一些还没用到 TODO
其他扩展
- 数组扩展
//是用range对象访问数组
a[1..3]
- Number 扩展 包括Integer Double
1.upto downto step // 一个范围内的迭代
- 系统进程Process扩展
// 通过string获取进程对象
process = 'wc'.execute()
// 将输入发送到进程
process.out.withWriter {
it << "lalalla"
}
// 从进程读取返回
process.in.text 或 process.text
// 或者通过string数组,第一个元素当做命令,其他元素当做参数
- IO扩展
// 读取文件内容
new File("xx.txt").text
// 分行读取
new File("xx.txt").eachLine { line -> println line}
// 写入
new File("xx.txt").withWiter { file -> file << "xxxx" }
- java.util 扩展 以及 扩展模块
groovy脚本
groovy与java不同的点是,groovy不仅可以支持class,也可以支持一段代码独立执行 -- 脚本
脚本变量作用域
- 分为块内作用域和全局作用域
// 使用静态类型修饰的是块内作用域,对binding.variables.变量名 不可见
def a = 'a'
String str = 'str'
// 全局作用域 不使用def,可被binding.variable.变量名 所访问。
a = 'a'
- 脚本中定义的方法是独立块,与脚本中定义的变量不能互相访问
def a = 'a'
def test() {
print a // 访问不到a
}
- 块内(或闭包内)可以访问脚本中变量
def a = 'a'
def closure = {
print a // 可以访问
}
闭包
Groovy的闭包是短小的匿名方法,两个主要的作用:
- 辅助资源清理
- 辅助创建内部DSL
使用方式
可以在方法调用的参数上即时创建闭包,也可以将闭包赋给变量以复用
// 当闭包是方法调用最后一个实参时,可以将闭包附在方法调用上
pick(10, {...})
pick(10) {...} // 这个不是函数定义,是把最后一个{}挂在了方法调用外
pick 10, {} // 去掉括号后,参数间要有逗号,不能省略
闭包传参
对于单参数的闭包,可以直接使用it作为该参数的默认名称,多于一个参数时:
{ Date date, fortune ->
print "${data}, ${fortune}"
}
// 如果希望闭包不接受任何参数,需要以下形式,没有箭头符号的话,默认接受一个参数
{ ->
}
资源清理
Java的自动垃圾收集中,只要释放了引用,就不用担心资源回收问题,但是什么时候回收没有保证,有时候我们希望能够用完直接清理,譬如IO中的destory,close方法就是这个作用。但是有时会忘记做这一动作
闭包可以自动完成这一点
new FileWriter('output.txt').withWriter { writer ->
writer.write('a')
} // 完成后自动flush和close
原理:Execute Around Method模式,定义一个方法,在方法中完成资源的开启,try-catch,关闭,资源相关操作以闭包的形式让用户传入,用户代码只需要关心资源操作的逻辑
curried closure
带有预绑定形参的闭包叫做柯里化闭包,柯里化是数学中减少函数参数的一种变换,在这里也是为了减少闭包的参数,所以会预绑定一些参数,之后就不需要重复为这个形参提供实参了
curriedCloure = cloure.curry(bar1,bar2)
// 使用curry方法可以绑定任意多个形参,但这些形参从前面开始的若干个
获取闭包参数信息
- maximumNumberOfParameters 闭包参数个数
- parameterTypes 获取参数属性
闭包委托
闭包的三个属性,用于确定哪个对象处理该闭包内的方法调用
- this
- owner
- delegate
在执行闭包时,首先闭包会被创建为一个内部类xxx$_run_closure,this是闭包定义所在的类,owner是闭包定义所在的对象,可能是一个类也可能是另一个闭包,delegate是一个三方委托的对象,闭包的this和owner是闭包的词法作用域,不能修改,而delegate是用户可以定义和改变的对象,是建立dsl的强大工具。默认delegate等于owner
委托策略:通过闭包的resolveStrategy属性定义,默认Closure.OWNER_FIRST,可以先owner,也可以先delegate,还可以只owner或者delegate
神奇的地方在于,闭包作为一个代码块,可以随意的被放置,这样就能够把需要用户写的简单的部分提出来,而在主流程中使用闭包代替;只需要在闭包执行前将当前对象委托给闭包的delegate就可以了
尾递归
将递归调用转换成为迭代
Groovy与Java集成
总览
- Groovy 使用 GroovyShell运行Groovy脚本
- Groovy 可以直接使用classpath下的Groovy类
- Groovy 可以直接使用classpath下的Java类
- Java ? 使用Groovy脚本
- Java 可以直接使用classpath下的Groovy类
运行Groovy
- goovy 源码 -- 内存中编译并运行
- groovyc 源码 -- 编译为字节码,java 字节码 运行 (同时要在classpath引入groovy-all-xxx.jar)
Groovy中使用Groovy类
首先保证Groovy类在classpath下,可以是groovy类,也可以是groovy字节码,Groovy会先找groovy文件,没有再查找同名class文件
Groovy与java混用 -- 联合编译
Java中没办法直接使用Groovy文件,所以Java中使用Groovy需要先将Groovy编译为class,可以提前编译Groovy或者是用GroovyClassLoader
Groovy中如果使用到了Java类,在classPath下是可以直接使用的,也可以通过编译,这个时候需要使用groovyc的联合编译,它编译groovy时会先确定是否有任何需要编译的Java类,所以对于混用的场景,只需要调用groovyc就好了,混用的代码通过编译后就可以直接被Java使用了
Java中使用Groovy特性 -- 闭包
譬如当在groovy类中定义了一个方法,参数是闭包,那么java在调用groovy这个方法是,可以传递一个new Object实例,并在实例中实现call()方法,call方法的参数要与闭包参数一致
Java中使用Groovy特性 -- 动态方法
譬如一个groovy类实现了methodMissing方法,那么这个groovy类可以调用任何方法,在Java中使用这个类,可以通过调用invokeMethod方法,传入任何方法和参数
instance.invokeMethod("method", new Object[] {})
Groovy中使用Java类
保证java类在classpath的前提下,想java中一样直接import并使用
Groovy中使用Groovy脚本
使用GroovyShell在其他脚本或类中调用他们,脚本可以是任何文件或文本,每个脚本中都有个默认的binding对象,发起脚本可以将自己的binding传给被执行脚本,可以共享当前脚本的binding对象,如果不想使用当前脚本的binding对象,可以新建一个Binding对象设置properties
这个特性可以帮我们构建并执行DSL
Java中使用Groovy脚本
要使用ScriptEngineManager,并依赖 groovy-engine.jar文件
MOP与元编程
MOP MetaObject Protocol 元对象协议,使用这个协议可以动态调用方法或者运行时合成方法
Groovy对象
groovy对象是带有附加功能的java对象,在groovy应用中有三类对象
- POJO 普通java对象
- POGO 普通groovy对象,扩展了Object并实现了GroovyObject接口
- Groovy拦截器,扩展了GroovyInterceptable接口的Groovy对象,是一个标记接口,实现了该接口所有方法都会被invokeMethod方法拦截
对于POJO和POGO都支持元编程,POJO通过维护一个MetaClassRegistry(维护POJO和Metaclass的关系),POGO直接有Metaclass的引用,当调用方法的时候,groovy会根据pojo还是pogo做不同调用
pogo调用一个方法的顺序:metaclass方法 -- pogo本身方法 -- 同名且为Closure类型的属性 -- 是否有methodMissing方法 -- 调用invokeMethod方法
访问对象方法和属性
proper = "proper"
meth = "meth"
// 访问属性
obj[proper]
obj."$proper"
// 调用方法
obj."$meth"()
obj.invokeMethod(meth, null)
方法拦截
- 实现GroovyInterceptor接口后,任何方法都会先执行invokeMethod方法,然后默认执行metaClass的invokeMethod方法,就可以正常向后走
- 使用MetaClass拦截方法,默认实现是ExpandoMetaClass
// 没实现接口,优先调用metaclass的invokeMethod方法
Class.metaClass.invokeMethod - {...}
方法注入
动态的向类中添加方法
- 分类 能够修改一个类的MetaClass,且修改仅在代码块作用域起作用,使用是有性能代价的
// 为String添加方法 tossn
// 创建StringUtil并添加静态方法tossn
class StringUtil {
def static tossn(self) {...} // 可以通过限制self类型表明为哪些类添加此方法,如果方法还有其他参数,直接加载self之后
}
// 应用
use(StringUtil) {
'xx'.tossn()
}
- ExpandoMetaClass 全局可用
// 注入对象方法
Integer.metaClass.days = {delegate ...} // 通过delegate获取当前对象
1.days()
// 注入静态方法
Integer.metaClass.'static'.iseven = {...}
// 注入构造器 向特殊属性constructor
Integer.metaClass.constructor << {} // << 表示添加构造器,= 表示覆盖构造器
// emc dsl方式 ExpandoMetaClass,使用闭包实现的
Integer.metaClass {
days = { ->
...
}
}
// 向实例中注入方法
def jack = new Person()
jack.metaClass = new ExpandoMetaClass()
- Mixin // todo
方法合成
方法合成与方法注入的区别
- 方法注入:编写时知道要注入的方法名称,动态的向类中添加行为
- 方法合成:调用时动态确定方法的行为,而是基于状态引入行为
相关方法
- methodMissing 实践:拦截、缓存、调用
通过实现methodMissing拦截对不存在的方法的调用
通过实现propertyMissing拦截对不存在属性的访问
- 使用ExpandoMetaClass合成,对于没有修改类权限,或者非pogo类
// 使用metaClass的methodMissing方法
Person.metaClass.methodMissing = {...}
// 对实例进行方法合成
EMC的方式
动态类
Expando是一个动态类,属性和方法可以动态指定
car = new Expando(name: xxx, value: xxx, turn: {...})
car.drive = {...}
car.turn()
car.drive()
构建DSL
DSL的两大特点:上下文驱动,使用流畅,动态语言很适合设计内部DSL,譬如Ruby的括号可选、冒号代替双引号等为DSL提供极大的支持;python中由于空白是有意义的,可能会成为障碍。Groovy的元编程以及动态类型对DSL帮助很大,但是括号处理以及冒号符号不及ruby方便,groovy适合DSL的一些特点:
- 动态类型、可选类型
- 动态加载、操作和执行脚本
- 分类和ExpandoMetaClass,可以操纵类
- 闭包的上下文支持
- 操作符重载
- 生成器以及灵活的括号
命令链以及括号机制
方法调用可以不使用括号
print('hello') --> print 'hello'
// 闭包作为参数可以放在括号外面
test() {} // 这个不是函数定义,是将闭包的参数放到外面来执行
闭包与DSL
使用delegate为闭包代理上下文
def getPizza(closure) {
Pizza p = new Pizza()
closure.delegate = p // 为delegate设置代理对象p,闭包内就可以使用该对象上下文的方法
closure()
}
getPizza {
setPissa "pizza" // 使用Pizza对象的setPissa方法
}
方法拦截与DSL
def orderInfo = [:]
def methodMissing(String name, args) { // 为该脚本定义missing方法,执行是发现脚本中没有的方法就执行它,方法会被记录
orderInfo[name] = args
}
def acceptOrder(closure) {
closure.delegate = this // 将闭包delegate设置为本脚本对象
closure() // 执行后会将所有方法名存到orderInfo中
print orderInfo
}
// 执行DSL脚本 ,用户的DSL放在这边,上面是技术提供的DSL函数
acceptOrder {
setOrder xxx
}
上面两个脚本一般是分开的,可以参考下面的方式将两个脚本合起来
def dslDef = new File(xx).text
def dsl = new File(yy).text
def script = '''
${dslDef}
${dsl}
'''
new GroovyShell().evaluate(script)
括号的变通方案
如果括号没有参数,是不能直接把括号去掉的
def total() {...}
total // 不能直接使用,实际上访问的是属性
// 可以通过get方法实现这个属性
def getTotal() {...}
total // 可以访问total属性
分类,增加一些连接词使dsl更顺畅
2.ago // 计算2天前,但是2.days.ago会更顺畅可以为Integer增加days方法加强连接
class DateUtils {
static getDays(Integer self) { self }
}
use(DateUtils) {2 days ago}
ExpandoMetaClass与DSL
还是解决上面那个问题,可以通过metaClass使得全局有效
Integer.metaClass = {
getDays = {
delegate // 返回当前闭包的delegate
}
}
字符串
字面量与表达式
// 定义一个string
'string'
// 显示定义一个char
'c' as char
// 表达式 使用\转义$
"this is \$${str}"
多行字符串
''' 使用三个单引号创建多行GString
字符串的便捷方法
- 'ls -a'.execute().text 方法创建了一个Processor对象可以直接执行系统方法
- 使用 -= + 等等实现字符串中的增删
正则表达式
Groovy通过操作符重载是的正则更加容易,通过创建RegExp 或者 /reg/,通过 = ==~ 表达式表示匹配动作,表达式会返回matcher
集合类
list
// 声明
list = [1,2,3,4]
// 取值,第一个元素,最后一个元素,range对象取一个数组
list[0], list[-1], list[2..5]
// 内部迭代器 each
list.each { print it }
// 迭代并收集结果 collect
list.collect { it * 2 }
// 查找并返回第一个匹配的对象 find, 查找并返回所有匹配对象 findAll,闭包内应该是一个判断表达式
list.find { it == 2 } list.findAll { it == 2 }
// 将集合元素连成一个句子 join
list.join(",")
map
// 声明
map = ['a': 'a', 'b': 'b']
// 访问,属性访问
map['a'] map.a
// 属性访问的一些坑,譬如特殊字符c++,对于键名,没有特殊字符的键名可以省略''
map.c++ 错误 --> map.'c++'
// 内部迭代 each,一个参数使用entry,也可以使用两个参数分别代表key value
map.each { entry -> entry.key entry.value }
// 迭代并返回集合 collect
map.collect { key, value -> key }
// find findAll 返回map
map.find { key, value -> key.size() > 3 }
// any map中是否存在满足条件的元素,every是否都满足条件
map.any { key, value -> key =~ '[^A-Za-z]' } map.every { key, value -> xxx }
// groupBy
实践
groovy与java集成
背景:Java中有个Routebuilder抽象类,需要在Groovy中实现类的抽象方法configure
要注意的一点是:groovy中各种闭包作用于类的方式,是没有办法改变java类的,是通过metaclass实现的,譬如在groovy使用闭包实现了configure接口,通过GroovyClassLoader或者GroovyShell加载到java中,java中使用groovy传过来的configure接口是没有被实现的,因为java不会使用groovy那种调用类的方式
groovy能实现这种动态效果,是因为groovy的调用方式,而不是因为修改了java类,所以groovy中的动态方式在groovy中是可用的,但是java中使用普通类调用时是没有那种效果的。正确的姿势是:
// 在java中实现configure接口
// 在接口中通过groovy脚本进行调用,并将当前对象传递到绑定
Binding binding = new Binding();
binding.setProperty("currentObj", this);
GroovyShell groovyShell = new GroovyShell(binding);
File script = new File("xxx.groovy");
groovyShell.evaluate(script);
// 脚本中的闭包要设置到当前的对象,以便使用当前对象的方法
def cloure = {
println('exe closure')
from('hahaha')
}
cloure.delegate = currentObj
cloure()
- context设计模式:提供不同的层次的类定义函数,将所有类中都要操作的对象提取出来,组合成一个context对象,一遍各个函数中使用
在java中实现groovy Dsl的一个思路
- 在Java中实现DSL基类,继承Script,其中定义在groovy脚本中需要的各种函数(DSL)
- CompilerConfiguration设置该DSL为ScriptBaseclass
- GroovyShell parse 脚本,并获得DSL脚本对象
- 执行脚本
其他
在对象中获取类
getClass()