由于Groovy支持脚本,因此首先需要明确Groovy提供了一个抽象类groovy.lang.Script,最终这个脚本会被编译为一个继承于groovy.lang.Script的类。定义的脚本是一个文件,最终编译的这个类名即是脚本文件的名称。

闭包

接下来来了解一下关于Groovy的闭包。在Groovy中闭包是一个代码块儿,可以被用作参数、返回值或分配给一个变量。

闭包的定义如如下语法:

{ [closureParameters -> ] statements }

[closureParameters -> ]是可选的,是用逗号隔开的一个闭包参数列表,与方法的参数列表类似,因此参数也是支持默认参数、可变参数等。如果指定了参数则符号->是必须的,用来分割闭包体和参数列表。另外,闭包中总是会有一个隐含的参数名it,所以如果闭包中没有定义参数列表,在调用时仍然可以传递一个参数。这样如果需要定义一个闭包不接受任何参数,可以仅仅指定符号->没有参数列表。

闭包实质是groovy.lang.Closure的一个实列,因此在分配一个闭包给变量时这个变量可以明确指定类型groovy.lang.Closure。闭包的调用可以想通常的方法一样调用,而且闭包有一个隐含的方法call(),因此也可以直接调用闭包的call()方法。

委派策略(Delegation strategy)

闭包基本属性

这部份就是这篇文章需要讲述的重点,也是这部份内容让Groovy的闭包更强大能设计出更好的DSL。
委派策略是建立在thisownerdelegate之上的,因此先理解这三个概念。

this

this引用的是定义闭包所在的这个类,比如如下一个脚本:

# filename=test.groovy
def out = {
println("The this is ${this.class.typeName}")
def inner = {
println("The this in inner closure is ${this.class.typeName}")
}
inner()
}

out()

执行的输出:

The this is test
The this in inner closure is test

根据Groovy的实现知道脚本文件最终会编译为一个以脚本文件名为名称的类,可以看到无论是脚本内的闭包还是这个闭包嵌套的闭包this都指向的是test这个类。再看一个列子:

# filename=test.groovy
class InnerClass {
class InnerInner {
def out = {
println("The this is ${this.class.typeName}}")
def inner = {
println("The this in inner closure is ${this.class.typeName}")
}
inner()
}
}
}

def ii = new InnerClass.InnerInner()
ii.out()

输出:

The this is InnerClass$InnerInner
The this in inner closure is InnerClass$InnerInner

可以看到在一个内部类中定义一个闭包,this指向的这个内部类,也就是说this指向的是围绕这个闭包定义的这个类。调用getThisObject()方法会返回围绕这个闭包定义的这个类,等于使用this

owner

owner指向的则是围绕闭包定义的这个对象(并不是仅仅指类的一个实列),在部分情况下它与this相同,而在一些情况下又和this不同。闭包内部可以嵌套闭包,想象在一个类中E定义一个闭包A,在A中再定义一个闭包B。那么这个时候A中ownerthis都是这个类E,而闭包B中的this仍然指向类E,owner则指向的是闭包A。它可能是一个类也可能是一个闭包,还是用this中的列子,稍微修改一下:

class InnerClass {
class InnerInner {
def out = {
println("${this.class.typeName}") // output: InnerClass$InnerInner
println("${owner.class.typeName}") // output: InnerClass$InnerInner
def inner = {
println("${this.class.typeName}") // output: InnerClass$InnerInner
println("${owner.class.typeName}") // output: InnerClass$InnerInner$_closure1
}
inner()
}
}
}

def ii = new InnerClass.InnerInner()
ii.out()

如上面的代码块儿,输出已经在其中注释。可以看到在闭包outthisowner都是内部类。在闭包innerthis仍然指向的是内部类,由于闭包inner定义在闭包out中,因此它的owner指向的是闭包对象。如同this,Groovy也提供了一个方法getOwner(),它与owner相同。

delegate

官方的解释是delegate相当于方法调用这个闭包时的一个对象,或者每当消息接收者未定义时的属性解析。对比闭包的thisowner涉及到是一个闭包的scope(作用于),delegate则是一个闭包会使用到的一个用户定义的对象。默认情况下delegate被设置与owner相同。

通用也会有一个方法getDelegate()来获取这个引用的对象,等于使用delegate。同样拿上面的代码实列:

class InnerClass {
class InnerInner {
def out = {
println("${this.class.typeName}") // output: InnerClass$InnerInner
println("${owner.class.typeName}") // output: InnerClass$InnerInner
println("${delegate.class.typeName}") // output: InnerClass$InnerInner
def inner = {
println("${this.class.typeName}") // output: InnerClass$InnerInner
println("${owner.class.typeName}") // output: InnerClass$InnerInner$_closure1
println("${delegate.class.typeName}") // output: InnerClass$InnerInner$_closure1
}
inner()
}
}
}

可以看到在闭包out与嵌套闭包innerdelegateowner引用的是相同的对象。在上面说道默认delegate设置与owner相同,也就是说delegate是可以被改变的,也是因为这个促使闭包变得更强大。用官方文档中的一个实列来看看delegate

class Person {
String name
}
class Thing {
String name
}

def p = new Person(name: 'Norman')
def t = new Thing(name: 'Teapot')

def upperCasedName = { println(delegate.name.toUpperCase()) }

upperCasedName.delegate = p
upperCasedName() // output: NORMAN
upperCasedName.delegate = t
upperCasedName() // output: TEAPOT

在上面的代码块儿中首先定义PersonThing两个类,并且都用一个属性叫name。之后分别创建两个类的实列,接着定义了一个闭包。根据之前的解释知道这个代码块儿在一个脚本中test.groovy,此时定义的闭包delegateowner是一样的,都是编译后的类test。在闭包中引用了属性name,但并未定义。后面分别通过改变delegate并调用闭包,可以看到闭包中调用成功,并且引用的name属性值也分别是两次改变delegate指向的实列的属性值。也就是说通过赋值给闭包隐藏的delegate属性,即可以动态的修改delegate的指向,而且在闭包中可以引用delegate指向对象的成员。实际上在闭包中引用一个涉及到delegate指向对象的成员时,并不需要一定使用delegate前缀,因此上面代码块儿中闭包定义成def upperCasedName = { println(name.toUpperCase()) }也是没有问题的。

Delegation strategy

在理解了闭包的这三个概念之后,就可以看看闭包的委托策略。闭包中访问一个属性没有明确设置接收者对象(指thisownerdelegate),这个时候委托策略就会被涉及到。在上节中的例子,闭包中调用name属性且加了接受者对象前缀delegate.,这个时候不会涉及到Delegation strategy。若去掉delegate.前缀,则会按照Delegation strategy来解析属性或方法调用。如官方的例子:

class Person {
String name
}
def p = new Person(name:'Igor')
def cl = { name.toUpperCase() }
cl.delegate = p
assert cl() == 'IGOR'

在闭包cl中name没有明确指定接收者,而被直接使用。通过改变这个闭包的delegate指向Person类的实列p,这个闭包方法执行成功。也就是闭包中name属性的解析对于他的delegate对象是透明的。在这里以可以看到引用delegate对象中的属性并不需要delegate.前缀。闭包的这种属性的解析的方式和方法调用(闭包可以调用delegate的方法)非常强大。

可以看到name并没有明确的指定接收者而是直接引用,这个时候就会按照策略解析。首先是owner,这个例子中都在一个脚本文件中,因此ownerthis是一样的,都没有这个属性。最后就会从delegate中解析,上面指定闭包的cldelegate指向对象p,p中包含name属性。 解析策略包含下面几种:

  • Closure.OWNER_FIRST就是默认的策略,这个默认策略并不说会忽略闭包自己或this中的定义的属性,只有闭包自己或this都没有定义访问的属性。而且根据thisowner的关系可以知道它俩有时候是一样的。这个策略优先从owner中解析,owner中存在会被调用否则会使用delegate
  • Closure.DELEGATE_FIRST这个策略和上面的策略相反;
  • Closure.OWNER_ONLY这个策略从字面上就可以理解仅仅会从owner中解析,delegate会被忽略;
  • Closure.DELEGATE_ONLY这个策略通上一条策略相反;
  • Closure.TO_SELF这个策略则用于有高级元编程开发需要的开发者,可以实现自己的策略;比如想要实现仅仅在closure类自身中的解析;

通过赋值上述不同的策略给闭包的resolveStrategy属性来修改闭包的委派策略。有一点是闭包自身定义的属性一定会被使用,其他情况只要没有明确指定接收者,都会按照这个策略来解析。如下面一个列子:

class Person {
String name
def outer = {
def inner = {
String name = "owner"
println(this.name.toUpperCase())
}
inner()
}

String toString() {
outer()
}
}

def p = new Person(name: 'Norman')
p.toString() // output: NORMAN, if remove this. in closure inner then output: OWNER

@Field def name = "self"
def upperCasedName = {
println(name.toUpperCase())
}

upperCasedName.delegate = p
upperCasedName() // output: SELF
upperCasedName.resolveStrategy = Closure.DELEGATE_FIRST
upperCasedName() // output: NORMAN

上面的例子中在Person类中的嵌套闭包innerthisowner是不同的。例子中明确指定了this.,这个时候不会涉及到Delegation strategy结果是this对象Person的实列的成员name。因为这时没有涉及到Delegation strategy,无论怎么修改resolveStrategy,结果都不会变。若移除this.前缀,根据前面的策略,闭包outer是闭包innerowner,所以输出也变成OWNER

下面的部分直接在脚本中定义一个闭包upperCasedName,使用注解@Field定义一个变量name,这个变量最终会是脚本类的成员。在闭包中引用变量name,没有明确指定的前缀接收者。在改变闭包upperCasedNameresolveStrategy的前后分别调用这个闭包,可以看到输出在未改变前是onwer的属性,改变策虑Closure.DELEGATE_FIRST后则引用的Person实列的name属性。

应用

Groovy支持在调用方法时省略参数列表的括号,加之上面讲到的强大的闭包Delegation strategy,可以比较容易的设计开发一个DSL。最近在使用Jenkins 2 Pipeline构建一个交付系统,Pipeline中支持使用Shared Libraries扩展默认的功能,自定义更多的DSL结构。

在Shared Libraries创建一个脚本,脚本中定义一个call()方法,这样就可以直接在Pipeline脚本中通过脚本名称调用。这个方法的参数可以是一个闭包,这样可以使用Delegation strategy定义个纯配置的结构:

vars/approve.groovy

def call(body) {
def config = [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = config
body()

def proceedMessage = config.message ?: "Deploy this build?"
def timeoutTime = config.timeoutTime ?: 5
def timeoutUnit = config.timeoutUnit ?: TimeoutUnit.MINUTES.name()
timeout(time: timeoutTime, unit: timeoutUnit) {
input id: 'Proceed', message: "\n${proceedMessage}"
}
}

在这个脚本中实现了call()方法,并且使用一个闭包body作为他的参数。在call()的实现中修改了闭包参数bodyresolveStrategyClosure.DELEGATE_FIRST,并且使delegate指向一个Map实列,这样就可以使用Map中的值。

然后在Jenkinsfile脚本中调用这个结构:

approve {
timeoutTime = 1
timeoutUnit = 'HOURS'
}

依托与Groovy可以省略方法调用参数的括号,这个实际上是:

approve( {
timeoutTime = 1
timeoutUnit = 'HOURS'
})

实际上Groovy的这个省略方法调用参数括号还允许支持“command chain”:

// equivalent to: turn(left).then(right)
turn left then right

// equivalent to: paint(wall).with(red, green).and(yellow)
paint wall with red, green and yellow

上面的两个列子就是官方文档中给出的,可以看到省略了方法参数括号后的链式调用语法。这个可以看看文档都比较全面。