看懂 Gradle

Gradle 的脚本第一眼看上去很难把它和一门编程语言联系起来. 但其实 Gradle 的脚本是基于 Groovy 语言的. 只不过 Gradle 利用了很多 Groovy 的特性, 或者说是语法糖. 今天我们就来分析下下面这段 Gradle 脚本.

apply plugin: 'java'

task 'myTask' {
    group = 'myTasks'
    description 'Prints $description and ${group}'
} << {
    println "$description"
    println "$group"
}

简单的语法糖

Groovy 支持很多比较常见的语法糖, 比如语句后面的分号可以省略.

字符串插值

Groovy 中字符串可以使用单引号和双引号. 区别在于双引号是支持插值的:

def name = 'Guillaume' // a plain string
def greeting = "Hello ${name}"

assert greeting.toString() == 'Hello Guillaume'

我们可以使用 ${}$ 来表示插值. ${}{} 中可以是任何表达式, 而 $ 的后面只可以接带 . 的表达式:

def sum = "The sum of 2 and 3 equals ${2 + 3}"
assert sum.toString() == 'The sum of 2 and 3 equals 5'

def sum12 = "The sum of 1 and 2 is equal to ${def a = 1; def b = 2; a + b}"
assert sum12.toString() == 'The sum of 1 and 2 is equal to 3'

def person = [name: 'Guillaume', age: 36]
assert "$person.name is $person.age years old" == 'Guillaume is 36 years old'

对于 $ 只有带点的表达式是可以的, 所以 "$number.toString()" 会被解释成 "${number.toString}()"

字面表示

Groovy 中对于常见的数据结构都有字面表示方法:

def list = [1, 4, 6, 9]

// by default, keys are Strings, no need to quote them
// you can wrap keys with () like [(variableStateAcronym): stateName] to insert a variable or object as a key.
def map = [CA: 'California', 'MI': 'Michigan']

def range = 10..20
def pattern = ~/fo*/

Groovy 函数支持 named argument, 但其实 Groovy 会把他们转换成 Map 然后传给函数. 所以函数声明的参数的类型是个 Map:

apply(plugin: 'java') // 等于 apply([plugin: 'java'])

def foo(Map m, s) { // Map 需要放到第一个
    println m
    println s
}

foo(name: 'John', 'ugly', age: 24) // 等于 foo([name: 'John', age: 24], 'ugly')

括号

Groovy 允许我们在 top-level 语句中省略掉函数调用的括号. 例如:

println "Hello"
method a, b // 注意中间的逗号

vs:

println("Hello")
method(a, b)

但是对于没有参数的函数的调用是不能省略括号的. 同时, 不是 top-level 语句的函数调用的括号也是不能省略的. 下面两种情况都是不可以的省略括号的:

def foo(n) { n }
def bar() { println 'bar' }

// 下面的语句都不是合法的函数调用
println foo 1 // won't work
def m = foo 1
bar

Command Chains

写代码时我们经常会使用链式调用 (a(b).c(d)), 利用 Groovy 可以省略括号的特性, 我们可以非常简洁的进行链式调用 (a b c d), 不单单括号去掉了连中间的 . 都省略了.

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

如果链式调用中的某个方法没有参数, 那么这个括号是不可以省略的:

// equivalent to: select(all).unique().from(names)
select all unique() from names

如果链式调用中的元素的个数是奇数, 那么最后一个元素将会是获取属性:

// equivalent to: take(3).cookies
// and also this: take(3).getCookies()
take 3 cookies

所以文章最开头的脚本, 补上括号后是这样的:

apply([plugin: 'java'])

task('myTask') {
    group = 'myTasks'
    description('Prints $description and $group')
} << {
    println("$description")
    println("$group")
}

等一下, task('myTask')<< 后面的 {} 是什么鬼? 他们是我们下面要介绍的 Closure.

Closure

Closure 其实就是匿名函数. 语法定义是这样的:

{ [closureParameters -> ] statements }

下面是一些合法的 Closure 定义:

{ item++ }                                          

{ -> item++ }                                       

{ println it }                                      

{ it -> println it }                                

{ name -> println name }                            

{ String x, int y ->                                
    println "hey ${x} the value is ${y}"
}

{ reader ->                                         
    def line = reader.readLine()
    line.trim()
}

如果函数的最后一个参数是 Closure, 那么这个 Closure 是可以拿到括号外面的.

所以 task('myTask', {}) 可以写成 task('myTask') {}.

Delegate

Closure 是有自己的上下文 (Context) 的, 大多数语言的 Closure 的上下文就是 Closure 定义的时候所在的上下文. Groovy 的 Closure 比较不同的地方在于, 它可以重新定义 Closure 的上下文即 Delegate.

def myVar = 'Hello World!'
def myClosure = { println myVar }

class MyClass {
    def myVar = 'Hello from MyClass!'
}

MyClass m = new MyClass()
myClosure.setDelegate(m)
myClosure() // 输出: Hello from MyClass!

下面脚本中的 Closure 就被 Gradle 重新设置了上下文, 所以我们才可以使用 groupdescription.

task 'myTask' {
    group = 'myTasks'
    description 'Prints $description and ${group}'
}

隐式 Getter / Setter

在 Groovy 中访问和修改属性都是会默认调用 Getter 和 Setter 方法的. 所以 group = 'myTasks' 等价于 setGroup('myTasks'). 可以使用 obj.@prop 来跳过 Getter 和 Setter 方法.

不过 description '...' 可不等价于 setDescription('...'), 而是等价于我们之前说的方法调用 description('...'), 可是查文档我们会发现 description 是个属性啊! 这是因为 Gradle 默认会为一些属性添加一个同名的方法. 所以我们可以用 group = '...' 也可以用 group '...'.

符号重载

Groovy 是支持符号重载的:

Operator Method
a + b a.plus(b)
a - b a.minus(b)
a * b a.multiply(b)
a ** b a.power(b)
a / b a.div(b)
a % b a.mod(b)
a | b a.or(b)
a & b a.and(b)
a ^ b a.xor(b)
a++ or ++a a.next()
a-- or --a a.previous()
a[b] a.getAt(b)
a[b] = c a.putAt(b, c)
a << b a.leftShift(b)
a >> b a.rightShift(b)
a >>> b a.rightShiftUnsigned(b)
switch(a) { case(b) : } b.isCase(a)
if(a) a.asBoolean()
~a a.bitwiseNegate()
-a a.negative()
+a a.positive()
a as b a.asType(b)
a == b a.equals(b)
a != b ! a.equals(b)
a <=> b a.compareTo(b)
a > b a.compareTo(b) > 0
a >= b a.compareTo(b) >= 0
a < b a.compareTo(b) < 0
a <= b a.compareTo(b) <= 0

所以最终我们最开始的 Gradle 脚本就变成了这样:

apply([plugin: 'java']);

task('myTask', {
    setGroup('myTasks');
    description('Prints $description and $group');
}).leftShif({
    println("$description");
    println("$group");
})

这样是不是熟悉多了?

Compilation customizers

通常我们定义一个 task 会这样写 task myTask {}, myTask 外面是没有引号的. 这样的话按照我们上面的说法这就会等价于 task(myTask) {}, 可是 Gradle 并没有为我们定义 myTask 哇.

其实这是 Gradle 利用了 Groovy 的 Compilation customizers. 显然, Gradle 要执行我们写的 build.gradle, 必然会在代码中调用 Groovy 提供的接口来编译运行脚本, Groovy 比较灵活的地方在于, 我们可以配置 Groovy 的编译器:

import org.codehaus.groovy.control.CompilerConfiguration

// create a configuration
def config = new CompilerConfiguration()
// tweak the configuration
config.addCompilationCustomizers(...)
// run your script
def shell = new GroovyShell(config)
shell.evaluate(script)

我们上面的 myTask 就是 Gradle 利用 CompilationCustomizer 修改了 AST (Abstract Syntax Tree), 类似 Java 的字节码修改. 这个比较进阶, 可以参考下面给出的链接进一步了解.

总结

知道了这些 Groovy 的特性, 应该就能看懂 Gradle 的脚本了. 我们也可以发现 Groovy 是一门非常灵活的语言, 所以这也使得 Groovy 非常适合作为 DSL (Domain-Specific Languages) 的开发.

Gradle tip #2: understanding syntax

看懂Gradle脚本(1)- Groovy语言的Map语法糖

Groovy Style Guide

Groovy Domain-Specific Languages

Groovy Closure

Groovy Syntax

Groovy Operators