【语言学习】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()

访问XML,数据库以及excel