历时半年多的途游平台中心SDK重构工作基本完成,终于能轻松一段时间了,闲下来几天觉得甚是无聊,决定好好看看gradle这个东西,没准还能把途游目前用的这套ant的脚本给换了。

一、GRADLE的优势在哪

  • Polyglot Builds 多语言支持
  • Tool Integrations 整合的工具
  • Robust Dependency Management 强大的依赖管理
  • Powerful Concise Logic 简洁的逻辑
  • High Performance Builds 高性能构建
  • Build Reporting 构建报告

二、GRADLE学习路线

  1. Groovy basic
  2. Gradle basic
  3. Tasks
  4. Task inputs/outputs
  5. Plugins
  6. Java plugin
  7. Android plugin

三、GROOVY基础

Gradle基于groovy,groovy基于Java。所以还是很有必要把groovy搞清楚。

Groovy是Java平台上设计的面向对象编程语言。 这门动态语言拥有类似Python、Ruby和Smalltalk中的一些特性,可以作为Java平台的脚本语言使用。Groovy基于JVM,会先编译成Java class,然后启动虚拟机来执行。实际上JVM只是执行Java字节码,并不知道这是groovy的代码。

3.1、Groovy开发环境

我这里用的是sdkman来下载groovy

$ sdk install groovy version	//安装对应version的groovy

安装之后,可以新建一个hello.groovy输入

println "hello groovy!"

然后执行

$ groovy hello.groovy

你会得到输出

hello groovy!

3.2、Groovy介绍

  • Groovy官方给出的与Java的差异
  • Groovy中可以不用分号结尾
  • Groovy支持定义变量的时候不指定其类型,而是用def定义变量
  • 函数中参数的类型也可以不指定
  • 函数中可以不使用return xxx来设置xxx为函数返回值。如果不使用return语句的话,则函数里最后一句代码的执行结果被设置成返回值。如果函数定义时候指明了返回值类型的话,函数中则必须返回正确的数据类型。 示例:
def abc = "abc"
def x = 1
def getSomething(){
  "getSomething return value" //如果这是最后一行代码,则返回类型为String
  1000 //如果这是最后一行代码,则返回类型为Integer
}
  • 单引号’‘中的内容严格对应Java中的String,不对$符号进行转义。
  • 双引号”“的内容,如果字符中有$号的话,则它会$表达式先求值。
  • 三个引号’'’something’'’中的字符串支持随意换行。
  • 可以不带括号调用方法

示例:

def str = "string object"
println "I am a $str"	// 输出 I am a string object
println 'I am a $str'	// 输出 I am a $str

def lines = '''line1
	line2
	line3
	line4'''		// 其中line2~line4前面的空白符也是会被记录的

def doSomePrint(var1){
	println var1
}

doSomePrint "this is the content should be print"
// 输出 this is the content should be print

3.3 数据类型

3.3.1 基本数据类型

与Java一样,不过在groovy中都对应的是包装类

3.3.2 List

  • 创建
def list = [5, 6, 7, 8]
assert list.get(2) == 7
assert list[2] == 7
assert list instanceof java.util.List

def emptyList = []
assert emptyList.size() == 0
emptyList.add(5)
assert emptyList.size() == 1 // 其实每一个表达式都是创建了一个 `java.util.List`的实现

def list1 = ['a', 'b', 'c']
def list2 = new ArrayList<String>(list1)	// 创建一个和list1一样数据的列表

assert list2 == list1 	// ==在这里是用来检查每一个对应元素是否相等

def list3 = list1.clone()	
// clone()的效果一样,注意此处clone出来的列表是一个完全独立的对象,不受原列表的影响
assert list3 == list1
  • 还可以被当做布尔表达式
assert ![]	// 一个空的列表被当做布尔表达式的时候值为false

//除上之外的所有列表中,如果列表中不为空,都是为true
assert [1] && ['a'] && [0] && [0.0] && [false] && [null]
  • 遍历

一般来说,都是调用列表的方法eacheachWithIndex来遍历列表项

[1, 2, 3].each {
    println "Item: $it" // it在这里是一个隐式的参数,代表当前的列表项
}
['a', 'b', 'c'].eachWithIndex { it, i -> // it是当前列表项, 同时i是当前项在列表中的位置
    println "$i: $it"
}

当然,如果想要在遍历的时候,对列表项进行一些转换操作的话,可以用collect

assert [1, 2, 3].collect { it * 2 } == [2, 4, 6]

// 可以用更短的语法
assert [1, 2, 3]*.multiply(2) == [1, 2, 3].collect { it.multiply(2) }

def list = [0]
// 也可以把collect中转换过的列表项插入到一个list中去
assert [1, 2, 3].collect(list) { it * 2 } == [0, 2, 4, 6]
assert list == [0, 2, 4, 6]
  • 操作

过滤和查找

assert [1, 2, 3].find { it > 1 } == 2	// 找到列表中第一个匹配项
assert [1, 2, 3].findAll { it > 1 } == [2, 3]  // 找到列表中所有匹配项
assert ['a', 'b', 'c', 'd', 'e'].findIndexOf {  // 找到第一个匹配项的index
    it in ['c', 'e', 'g']
} == 2

assert ['a', 'b', 'c', 'd', 'c'].indexOf('c') == 2  // 返回第一个匹配项的index
assert ['a', 'b', 'c', 'd', 'c'].indexOf('z') == -1 // index == -1 意味着列表中没有这项
assert ['a', 'b', 'c', 'd', 'c'].lastIndexOf('c') == 4  // 返回最后一个匹配项的index

assert [1, 2, 3].every { it < 5 }  // 如果所有项都匹配则返回true
assert ![1, 2, 3].every { it < 3 }
assert [1, 2, 3].any { it > 2 }  // 如果有任何一项匹配则返回true
assert ![1, 2, 3].any { it > 3 }

assert [1, 2, 3, 4, 5, 6].sum() == 21  // 求和
assert ['a', 'b', 'c', 'd', 'e'].sum {
    it == 'a' ? 1 : it == 'b' ? 2 : it == 'c' ? 3 : it == 'd' ? 4 : it == 'e' ? 5 : 0
    // 自定义求和,有点像collect的感觉,显然列表项经过了一次转换之后再求的和
} == 15
assert ['a', 'b', 'c', 'd', 'e'].sum { ((char) it) - ((char) 'a') } == 10
assert ['a', 'b', 'c', 'd', 'e'].sum() == 'abcde'
assert [['a', 'b'], ['c', 'd']].sum() == ['a', 'b', 'c', 'd']

// sum还可以有一个初始值
assert [].sum(1000) == 1000
assert [1, 2, 3].sum(1000) == 1006

assert [1, 2, 3].join('-') == '1-2-3'  // 字符串拼接
assert [1, 2, 3].inject('counting: ') {
    str, item -> str + item  // 这里的str应该是结果,item是列表项,做的是字符串累加的操作
} == 'counting: 123'
assert [1, 2, 3].inject(0) { count, item ->
    count + item  // 此处是数字累加
} == 6

傻瓜式的最大最小值

def list = [9, 4, 2, 10, 5]
assert list.max() == 10
assert list.min() == 2

// 就像所有的东西都可以比较一样,对于单个字符也是可以的
assert ['x', 'y', 'a', 'z'].min() == 'a'

// 还可以用一个闭包来自定义排序行为
def list2 = ['abc', 'z', 'xyzuvw', 'Hello', '321']
assert list2.max { it.size() } == 'xyzuvw'
assert list2.min { it.size() } == 'z'

用一个闭包来自定义排序行为眼熟吧,这个有点像Java里面的Comparable,其实你还真可以定义一个Comparator用在这里

Comparator mc = { a, b -> a == b ? 0 : (a < b ? -1 : 1) }

def list = [7, 4, 9, -6, -1, 11, 2, 3, -9, 5, -13]
assert list.max(mc) == 11
assert list.min(mc) == -13

Comparator mc2 = { a, b -> a == b ? 0 : (Math.abs(a) < Math.abs(b)) ? -1 : 1 }

assert list.max(mc2) == -13
assert list.min(mc2) == -1

assert list.max { a, b -> a.equals(b) ? 0 : Math.abs(a) < Math.abs(b) ? -1 : 1 } == -13
assert list.min { a, b -> a.equals(b) ? 0 : Math.abs(a) < Math.abs(b) ? -1 : 1 } == -1

插入和删除

可以用[]定义一个空的列表,并且用 « 操作符来插入单个元素,但这仅仅是最简单的插入,还有好多方式可以实现

def list = []
assert list.empty

list << 5
assert list.size() == 1

// 在这里所有的插入操作都是在[1, 2]这个列表中进行的
// 由此也可见[4, 5]被当成了一个元素插进去了,说明 << 只能一次插入单个元素
assert ([1, 2] << 3 << [4, 5] << 6) == [1, 2, 3, [4, 5], 6]

// 可以用 leftShift 代替 <<
assert ([1, 2, 3] << 4) == ([1, 2, 3].leftShift(4))

assert [1, 2] + 3 + [4, 5] + 6 == [1, 2, 3, 4, 5, 6]
// 等同于调用plus()方法
assert [1, 2].plus(3).plus([4, 5]).plus(6) == [1, 2, 3, 4, 5, 6]

def a = [1, 2, 3]
a += 4      // 这会创建一个新的列表并且将新创建的列表分配到a这个变量头上
a += [5, 6]
assert a == [1, 2, 3, 4, 5, 6]

// * 在这里感觉是把列表里面的子列表拍平了,也正是flatten的意思
assert [*[1, 2, 3]] == [1, 2, 3]
assert [1, [2, 3, [4, 5], 6], 7, [8, 9]].flatten() == [1, 2, 3, 4, 5, 6, 7, 8, 9]
// 但是要注意到 * 只对当前的列表生效,而flatten会递归查找列表中的子列表
assert [1, *[222, 333, [444, 555]], 678] == [1, 222, 333, [444, 555], 678]
assert [1, *[222, 333, [444, 555]], 678].flatten() == [1, 222, 333, 444, 555, 678]

// 类似于Java的一些插入方法
def list = [1, 2]
list.add(3)
list.addAll([5, 4])
assert list == [1, 2, 3, 5, 4]

list = [1, 2]
list.add(1, 3) // add 3 just before index 1
assert list == [1, 3, 2]

list.addAll(2, [5, 4]) //add [5,4] just before index 2
assert list == [1, 3, 5, 4, 2]

list = ['a', 'b', 'z', 'e', 'u', 'v', 'g']
list[8] = 'x' // 这个[]操作符在列表长度不够的时候,会自动增加列表长度
// 如果需要的话,null值也是可以被插入的
assert list == ['a', 'b', 'z', 'e', 'u', 'v', 'g', null, 'x']

然而需要注意的是 + 操作符会新创建对象,和 « 相比,会存在性能上的问题

GDK中也提供了不少用于移除列表项的方法

assert ['a','b','c','b','b'] - 'c' == ['a','b','b','b']
assert ['a','b','c','b','b'] - 'b' == ['a','c']
assert ['a','b','c','b','b'] - ['b','c'] == ['a']

def list = [1,2,3,4,3,2,1]
list -= 3  // 这会移除列表中所有的3,并返回一个新创建的列表
assert list == [1,2,4,2,1]
assert ( list -= [2,4] ) == [1,1]

def list = [1,2,3,4,5,6,2,2,1]
assert list.remove(2) == 3  // 移除第三项并返回
assert list == [1,2,4,5,6,2,2,1]

def list= ['a','b','c','b','b']
assert list.remove('c')  // 移除'c',成功则返回true
assert list.remove('b')  // 移除第一个'b', 成功则返回true
assert list == ['a','b','b']

def list= ['a',2,'c',4]
list.clear()  // 清空列表项可以用clear()
assert list == []

集合

assert 'a' in ['a','b','c']  // 列表中存在'a'则返回true
assert ['a','b','c'].contains('a')  // 等同于Java中的contains
assert [1,3,4].containsAll([1,4])  // containsAll会检查所有元素是否存在

assert [1,2,3,3,3,3,4,5].count(3) == 4  // 计数(等于3的元素个数)
assert [1,2,3,3,3,3,4,5].count {
    it%2==0  // 自定义计数,匹配条件则累计
} == 2

assert [1,2,4,6,8,10,12].intersect([1,3,6,9,12]) == [1,6,12]  // 交集

assert [1,2,3].disjoint( [4,6,9] )  // 是否存在交集
assert ![1,2,3].disjoint( [2,4,6] )

排序

列表是有序的,GDK提供了一套用于方便排序的东西

assert [6, 3, 9, 2, 7, 1, 5].sort() == [1, 2, 3, 5, 6, 7, 9]

def list = ['abc', 'z', 'xyzuvw', 'Hello', '321']
assert list.sort {
    it.size()
} == ['z', 'abc', '321', 'Hello', 'xyzuvw']

def list2 = [7, 4, -6, -1, 11, 2, 3, -9, 5, -13]
list2.sort { a, b -> 
	a == b ? 0 : Math.abs(a) < Math.abs(b) ? -1 : 1 
}
assert list2 == [-1, 2, 3, 4, 5, -6, 7, -9, 11, -13]

Comparator mc = { a, b -> a == b ? 0 : Math.abs(a) < Math.abs(b) ? -1 : 1 }

// JDK 8+ 可以这么玩
// list2.sort(mc)
// assert list2 == [-1, 2, 3, 4, 5, -6, 7, -9, 11, -13]
// 否则你只能用`Collections.sort`
def list3 = [6, -3, 9, 2, -7, 1, 5]

Collections.sort(list3)
assert list3 == [-7, -3, 1, 2, 5, 6, 9]

Collections.sort(list3, mc)
assert list3 == [1, 2, -3, 5, 6, -7, 9]

复制

assert [1, 2, 3] * 3 == [1, 2, 3, 1, 2, 3, 1, 2, 3]
assert [1, 2, 3].multiply(2) == [1, 2, 3, 1, 2, 3]
assert Collections.nCopies(3, 'b') == ['b', 'b', 'b']

// Collections.nCopies和List.multiply不一样的地方在于Collections.nCopies中的复制的内容被整个当做了一项
assert Collections.nCopies(2, [1, 2]) == [[1, 2], [1, 2]] // 而不是[1,2,1,2]

3.3.3 Map

  • 创建

Map的创建很简单: [:]

def map = [name: 'Gromit', likes: 'cheese', id: 1234]
assert map.get('name') == 'Gromit'
assert map.get('id') == 1234
assert map['name'] == 'Gromit'
assert map['id'] == 1234
assert map instanceof java.util.Map

def emptyMap = [:]
assert emptyMap.size() == 0
emptyMap.put("foo", 5)
assert emptyMap.size() == 1
assert emptyMap.get("foo") == 5

需要注意的一点是,Map的键默认为String,举个栗子,默认情况下[a:1]['a':1]是相等的,在这种情况下,如果你想把变量a的值当做key,你需要用圆括号把a括起来

def a = 'Bob'
def ages = [a: 43]
assert ages['Bob'] == null  // Bob这个key是找不到的
assert ages['a'] == 43  // 反倒是a可以被找到

ages = [(a): 43]  // 现在用圆括号把a括起来
assert ages['Bob'] == 43   // 然后就可以了

// 需要注意的是Map的clone方法clone出来的对象会受到原对象的影响,这与list的表现是不一样的,如下所示:
def map = [
    simple : 123,
    complex: [a: 1, b: 2]
]
def map2 = map.clone()
assert map2.get('simple') == map.get('simple')
assert map2.get('complex') == map.get('complex')
map2.get('complex').put('c', 3)
assert map.get('complex').get('c') == 3
  • 属性标记

凡是String类型的key,在groovy中都可以以属性的方式去取值或赋值,如下:

def map = [name: 'Gromit', likes: 'cheese', id: 1234]
assert map.name == 'Gromit'  // 等同于map.get('Gromit')
assert map.id == 1234

def emptyMap = [:]
assert emptyMap.size() == 0
emptyMap.foo = 5
assert emptyMap.size() == 1
assert emptyMap.foo == 5

// 有几个需要注意的地方
map = [1      : 'a',
       (true) : 'p',
       (false): 'q',
       (null) : 'x',
       'null' : 'z']
assert map.containsKey(1) // 1 is not an identifier so used as is
assert map.true == null
assert map.false == null
assert map.get(true) == 'p'
assert map.get(false) == 'q'
assert map.null == 'z'
assert map.get(null) == 'x'
  • 遍历

在GDK中常用来迭代或遍历Map的两个方法是eacheachWithIndex,值得注意的是,在迭代遍历Map的时候,其中的键值对是有序的,和它被创建的时候的顺序保持一致

def map = [
    Bob  : 42,
    Alice: 54,
    Max  : 33
]

// `entry` is a map entry
map.each { entry ->
    println "Name: $entry.key Age: $entry.value"
}

// `entry` is a map entry, `i` the index in the map
map.eachWithIndex { entry, i ->
    println "$i - Name: $entry.key Age: $entry.value"
}

// Alternatively you can use key and value directly
map.each { key, value ->
    println "Name: $key Age: $value"
}

// Key, value and i as the index in the map
map.eachWithIndex { key, value, i ->
    println "$i - Name: $key Age: $value"
}
  • 其他操作

插入移除

插入用putputAll,移除用removeclear

def defaults = [1: 'a', 2: 'b', 3: 'c', 4: 'd']
def overrides = [2: 'z', 5: 'x', 13: 'x']

def result = new LinkedHashMap(defaults)
result.put(15, 't')
result[17] = 'u'
result.putAll(overrides)
assert result == [1: 'a', 2: 'z', 3: 'c', 4: 'd', 5: 'x', 13: 'x', 15: 't', 17: 'u']

def m = [1:'a', 2:'b']
m.remove('2')
assert m.get('2') == null
m.clear()
assert m == [:]

键、值、对

def map = [1:'a', 2:'b', 3:'c']

def entries = map.entrySet()
entries.each { entry ->
  assert entry.key in [1,2,3]
  assert entry.value in ['a','b','c']
}

def keys = map.keySet()
assert keys == [1,2,3] as Set

查找、过滤

其实和list都差不多,看下面例子:

def people = [
    1: [name:'Bob', age: 32, gender: 'M'],
    2: [name:'Johnny', age: 36, gender: 'M'],
    3: [name:'Claire', age: 21, gender: 'F'],
    4: [name:'Amy', age: 54, gender:'F']
]

def bob = people.find { it.value.name == 'Bob' } // 查找单个条目
def females = people.findAll { it.value.gender == 'F' }

// both return entries, but you can use collect to retrieve the ages for example
def ageOfBob = bob.value.age
def agesOfFemales = females.collect {
    it.value.age
}

assert ageOfBob == 32
assert agesOfFemales == [21,54]

// but you could also use a key/pair value as the parameters of the closures
def agesOfMales = people.findAll { id, person ->
    person.gender == 'M'
}.collect { id, person ->
    person.age
}
assert agesOfMales == [32, 36]

// `every` returns true if all entries match the predicate
assert people.every { id, person ->
    person.age > 18
}

// `any` returns true if any entry matches the predicate

assert people.any { id, person ->
    person.age == 54
}

分组

在GDK中可以用一些标准来对一个list进行分组转化成一个map

assert ['a', 7, 'b', [2, 3]].groupBy {
    it.class
} == [(String)   : ['a', 'b'],
      (Integer)  : [7],
      (ArrayList): [[2, 3]]
]

assert [
        [name: 'Clark', city: 'London'], [name: 'Sharma', city: 'London'],
        [name: 'Maradona', city: 'LA'], [name: 'Zhang', city: 'HK'],
        [name: 'Ali', city: 'HK'], [name: 'Liu', city: 'HK'],
].groupBy { it.city } == [
        London: [[name: 'Clark', city: 'London'],
                 [name: 'Sharma', city: 'London']],
        LA    : [[name: 'Maradona', city: 'LA']],
        HK    : [[name: 'Zhang', city: 'HK'],
                 [name: 'Ali', city: 'HK'],
                 [name: 'Liu', city: 'HK']],
]

3.3.4 Range

Range这个东西其实就是一个序列化的列表,可以用..定义一个全包含的序列,而..<则是包含第一个然后不包含最后一个,如下所示:

// 全包含
def range = 5..8
assert range.size() == 4
assert range.get(2) == 7
assert range[2] == 7
assert range instanceof java.util.List
assert range.contains(5)
assert range.contains(8)

// 半开半闭
range = 5..<8
assert range.size() == 3
assert range.get(2) == 7
assert range[2] == 7
assert range instanceof java.util.List
assert range.contains(5)
assert !range.contains(8)

// 用from和to来取头尾值
range = 1..10
assert range.from == 1
assert range.to == 10

其实如果一个对象实现了java.lang.Comparable用来比较,然后还实现了next()previous()两个方法用于获取上一个/下一个对象的话,都是可以使用Range的:

// an inclusive range
def range = 'a'..'d'
assert range.size() == 4
assert range.get(2) == 'c'
assert range[2] == 'c'
assert range instanceof java.util.List
assert range.contains('a')
assert range.contains('d')
assert !range.contains('e')

// 可以for循环遍历
for (i in 1..10) {
    println "Hello ${i}"
}
// 也可以用groovy风格的each
(1..10).each { i ->
    println "Hello ${i}"
}
// 还可以用在switch中
switch (years) {
    case 1..10: interestRate = 0.076; break;
    case 11..25: interestRate = 0.052; break;
    default: interestRate = 0.037;
}

3.3.4 Collection语法糖

  • GPath 支持

由于listmap的属性标记支持,让groovy在对于网状结构的集合时,可以这么玩:

def listOfMaps = [['a': 11, 'b': 12], ['a': 21, 'b': 22]]
assert listOfMaps.a == [11, 21] //GPath
assert listOfMaps*.a == [11, 21] //*.

listOfMaps = [['a': 11, 'b': 12], ['a': 21, 'b': 22], null]
assert listOfMaps*.a == [11, 21, null] // 这么写是包括null值的
assert listOfMaps*.a == listOfMaps.collect { it?.a } // 等价写法
// 但是这样写的话不会包含null值
assert listOfMaps.a == [11,21]
  • *操作符

其实这个操作符应该在是做了类似于putAll操作

assert [ 'z': 900, *: ['a': 100, 'b': 200], 'a': 300] == ['a': 300, 'b': 200, 'z': 900]
// 可以理解为把map拍平了放进去,也就是putAll,应该不是递归的
assert [*: [3: 3, *: [5: 5]], 7: 7] == [3: 3, 5: 5, 7: 7]

def f = { [1: 'u', 2: 'v', 3: 'w'] }
assert [*: f(), 10: 'zz'] == [1: 'u', 10: 'zz', 2: 'v', 3: 'w']
// 还可以这么玩
f = { map -> map.c }
assert f(*: ['a': 10, 'b': 20, 'c': 30], 'e': 50) == 30

f = { m, i, j, k -> [m, i, j, k] }  // 这里接收四个参数,并组合到一个集合里
// 命名未命名混合着来也行,但是未命名的会被排在后面
assert f('e': 100, *[4, 5], *: ['a': 10, 'b': 20, 'c': 30], 6) ==[["e": 100, "b": 20, "c": 30, "a": 10], 4, 5, 6]
  • *.操作符

这个简写的操作符可以让你访问集合中所有的项的属性或者方法

assert [1, 3, 5] == ['a', 'few', 'words']*.size()

class Person {
    String name
    int age
}
def persons = [new Person(name:'Hugo', age:17), new Person(name:'Sandra',age:19)]
assert [17, 19] == persons*.age
  • 下标操作符

你可以用下标来操作list map array,但是有意思的是,在groovy中String被当做一个特别的集合

def text = 'nice cheese gromit!'
def x = text[2]

assert x == 'c'
assert x.class == String

def sub = text[5..10]
assert sub == 'cheese'

def list = [10, 11, 12, 13]
def answer = list[2,3]
assert answer == [12,13]

还有就是你可以用Range去获取一个特定的子列表

list = ['a','x','x','d']
list[1..2] = ['b','c']
assert list == ['a','b','c','d']

值得注意的是,负的下标也是可以的,而负的下标的意义在于用于从集合的最后开始获取元素

text = "nice cheese gromit!"
x = text[-1]
assert x == "!"

def name = text[-7..-2]
assert name == "gromit"

在这种情况下,如果你用的是反向的Range的话(类似于3..1),则结果也会是反的 :)

text = "nice cheese gromit!"
name = text[3..1]
assert name == "eci"

3.3.5 与集合相关的其他玩意

这里已经写了不少了,但是还有好多是没有列出来的,具体可以查看GDK的API

四、Groovy的I/O操作

4.1 读文件

对于文本文件来说,读文件内容简直是简单到爆

new File(baseDir, 'haiku.txt').eachLine { line ->
    println line
}

Groovy在File中加入了eachLine方法,并且有好多个重载可以用

new File(baseDir, 'haiku.txt').eachLine { line, nb ->
    println "Line $nb: $line"
}

不论因为什么原因导致在eachLine里面抛出了异常,groovy保证对应的文件I/O会被关闭,这对于所有的groovy添加的I/O操作方法里面都是一致的

def count = 0, MAXSIZE = 3
new File(baseDir,"haiku.txt").withReader { reader ->
    while (reader.readLine()) {
        if (++count > MAXSIZE) {
            throw new RuntimeException('Haiku should only have 3 verses')
        }
    }
}

如果你要把文件中的每一行作为一项,整个文件转换成一个list,可以这么写:

def list = new File(dir, "a.txt").collect{ it }

或者用as把文本文件内容转换成一个String[]数组

def list = new File(dir, 'a.txt') as String[]

如果你要的是byte[]的话,也很简单:

byte[] contents = file.bytes

又或者说是InputStream

def is = new File(dir, 'a.txt').newInputStream()
// do something ...
is.close()  // 注意在这种方式下,需要主动关闭流

new File(dir, 'a.txt').withInputStream { stream ->
    // do something ...
}  // 而在这种情况下,流会自动关闭

4.2 写文件

写文件也不难,可以用Writer

new File(dir, 'a.txt').withWriter('utf-8') { writer ->
    writer.writeLine 'Into the ancient pond'
    writer.writeLine 'A frog jumps'
    writer.writeLine 'Water’s sound!'
}

还有一种更简单的,直接用<<操作符

new File(baseDir,'haiku.txt') << '''Into the ancient pond
A frog jumps
Water’s sound!'''

如果是byte[]也不是问题:

file.bytes = [66,22,11]

流也是支持的:

def os = new File(baseDir,'data.bin').newOutputStream()
// do something ...
os.close()  // 记得要关闭

new File(baseDir,'data.bin').withOutputStream { stream ->
    // do something ...
}

4.3 文件树

如果需要在脚本里面对一些特定文件做处理,groovy也提供了一些快捷的方法

dir.eachFile { file ->                      
    println file.name  // 所有文件
}
dir.eachFileMatch(~/.*\.txt/) { file ->     
    println file.name  // 所有匹配这个正则的文件
}

如果需要往更深的目录递归去找文件的话,可以用eachFileRecurse

dir.eachFileRecurse { file ->                      
    println file.name  // 包括文件和文件夹
}

dir.eachFileRecurse(FileType.FILES) { file ->      
    println file.name  // 仅包括文件
}

对于更复杂的情况的话,还可以用traverse根据你的条件来过滤出你想要的文件:

dir.traverse { file ->
    if (file.directory && file.name=='bin') {
        FileVisitResult.TERMINATE  // 结束遍历
    } else {
        println file.name
        FileVisitResult.CONTINUE  // 输出文件名并继续
    }
}

4.4 数据和对象

在Java里面用java.io.DataOutputStreamjava.io.DataInputStream来序列化和反序列化是很常见的,在GDK中,这种操作会变得更简单:

boolean b = true
String message = 'Hello from Groovy'
// 写入文件
file.withDataOutputStream { out ->
    out.writeBoolean(b)
    out.writeUTF(message)
}
// 读取
file.withDataInputStream { input ->
    assert input.readBoolean() == b
    assert input.readUTF() == message
}

简单来说,如果你想序列化一个对象,让他实现Serializable接口就行,然后你就可以用ObjectOutputStream来把他写入文件,并用ObjectInputStream来读取对应内容:

Person p = new Person(name:'Bob', age:76)
// 写入
file.withObjectOutputStream { out ->
    out.writeObject(p)
}
// 读取
file.withObjectInputStream { input ->
    def p2 = input.readObject()
    assert p2.name == p.name
    assert p2.age == p.age
}

4.5 命令行处理

Groovy提供了一个也是简单到爆的命令支持,把要执行的命令作为一个String并调用方法execute()就可以了:

// 在*nix环境下,你可以执行这个
def process = "ls -l".execute()  // 这里的process是一个java.lang.Process对象
println "Found text ${process.text}"  // 这里的text返回的是命令行的输出内容

其中返回的java.lang.Process对象,可以让你拿到其中的in/out/err流,还有返回值什么的,比如你可以这样:

def process = "ls -l".execute()             
process.in.eachLine { line ->               
    println line                            
}

值得注意的是,in代表的是命令行的标准输出流,而out是一个你可以往命令行里面写数据的输入流(标准输入)。

记住,有很多命令行是shell内置的,这个时候需要小心了。比如你想要在Windows机器上获取一个目录下的所有文件的list,然后像下面这么写就会出错:

def process = "dir".execute()
println "${process.text}"

你会看到报IOException,说无法运行程序dir,这是因为dir是内置于Windows的shell中的(cmd.exe),然后不能被单独运行,所以你只能这么写:

def process = "cmd /c dir".execute()
println "${process.text}"

因为这个命令行相关功能底层使用了java.lang.Process,所以这个类的缺陷也要考虑进去,在javadoc中对于这个类的说明是这样的:

Because some native platforms only provide limited buffer size for standard input and output streams, failure to promptly write the input stream or read the output stream of the subprocess may cause the subprocess to block, and even deadlock

意思说的是,因为有一些原生的平台对于标准输入输出流仅提供有限的缓冲区大小,当你在subprocess中直接去对输入输出流进行操作的失败,会导致subprocess阻塞,甚至卡死

因此,groovy提供一些额外的辅助方法来帮助你更好地操作命令行中的流,请看groovy是如何把命令行中的所有输出整合在一起的:

def p = "rm -f foo.tmp".execute([], tmpDir)
p.consumeProcessOutput()
p.waitFor()

consumeProcessOutput还有几个用StringBufferInputStreamOutputStream的重载

另外,还有一个pipeTo操作符(对应 | 用来重载的)用来把一个命令中的输出流导向另一个命令的输入流,如下所示:

// 管道操作
proc1 = 'ls'.execute()
proc2 = 'tr -d o'.execute()
proc3 = 'tr -d e'.execute()
proc4 = 'tr -d i'.execute()
proc1 | proc2 | proc3 | proc4
proc4.waitFor()
if (proc4.exitValue()) {
    println proc4.err.text
} else {
    println proc4.text
}
// 收集输出/错误
def sout = new StringBuilder()
def serr = new StringBuilder()
proc2 = 'tr -d o'.execute()
proc3 = 'tr -d e'.execute()
proc4 = 'tr -d i'.execute()
proc4.consumeProcessOutput(sout, serr)
proc2 | proc3 | proc4
[proc2, proc3].each { it.consumeProcessErrorStream(serr) }
proc2.withWriter { writer ->
    writer << 'testfile.groovy'
}
proc4.waitForOrKill(1000)
println "Standard output: $sout"
println "Standard error: $serr"

五、Groovy实用工具类

5.1 ConfigSlurper

ConfigSlurper是一个用于读取groovy脚本格式的配置文件,就像是Java中的*.properties文件,ConfigSlurper可以用.操作符,另外,它还可以识别闭包中的各种属性值,甚至是完全随意的对象:

// . 操作符  和  闭包中随意定义的属性
def config = new ConfigSlurper().parse('''
    app.date = new Date()
    app.age  = 42  		
    app {
        name = "Test${42}"
    }
''')

assert config.app.date instanceof Date
assert config.app.age == 42
assert config.app.name == 'Test42'

从下面的列子中可以看出,parse方法会返回一个groovy.util.ConfigObject实例,而这个ConfigObject的实例是一个特殊的java.util.Map的实现,要不返回对应的value,要不返回一个new出来的ConfigObject,不信你可以试试:

def config = new ConfigSlurper().parse('''
    app.date = new Date()
    app.age  = 42
    app.name = "Test${42}"
''')

assert config.test != null

此处的config.test并未指定,但是还是返回了一个新的ConfigObject

如果你想把.作为一个配置项的名字,可以通过单引号或者双引号做到:

def config = new ConfigSlurper().parse('''
    app."person.age"  = 42
''')

assert config.app."person.age" == 42

另外,ConfigSlurper还支持environments。而environments可以在闭包里面提供多种环境选择,然后再创建ConfigSlurper的时候,把对应的环境参数传过去就行:

def config = new ConfigSlurper('development').parse('''
  environments {
       development {
           app.port = 8080
       }
       test {
           app.port = 8082
       }
       production {
           app.port = 80
       }
  }
''')
assert config.app.port == 8080

还有就是ConfigSlurperenvironments的命名并不受限于常规的几种,完全依赖于ConfigSlurper的代码中定义了什么。

environments这个是内建就有的东西,但是还有一个东西叫registerConditionalBlock,可以用来注册其他的一些东西,比如:

def slurper = new ConfigSlurper()
slurper.registerConditionalBlock('myProject', 'developers')   

def config = slurper.parse('''
  sendMail = true
  myProject {
       developers {
           sendMail = false
       }
  }
''')
// 一旦注册了对应了block,ConfigSlurper就可以解析出来
assert !config.sendMail

为了Java整合需要,toProperties方法可以用来把一个ConfigObject转换成一个可以存储在一个*.properties文件中的java.util.Properties对象,并且其中的所有配置的value项会被转成String

def config = new ConfigSlurper().parse('''
    app.date = new Date()
    app.age  = 42
    app {
        name = "Test${42}"
    }
''')

def properties = config.toProperties()

assert properties."app.date" instanceof String
assert properties."app.age" == '42'
assert properties."app.name" == 'Test42'

5.2 Expando

Expando类可以用来创建一个动态的可扩展的对象。尽管叫这个名字,但是它底层用的并不是ExpandoMetaClass。每一个Expando对象相当于一个独立的、动态的可以在运行时扩展属性或方法的实例。

def expando = new Expando()
expando.name = 'John'

assert expando.name == 'John'

有一种特例是,当你把一个闭包动态注册为一个属性时,它可以当做一个方法一样被调用:

def expando = new Expando()
expando.toString = { -> 'John' }
expando.say = { String s -> "John says: ${s}" }

assert expando as String == 'John'
assert expando.say('Hi') == 'John says: Hi'

5.3 Observable list, map 和 set

Groovy还提供可被观察监听的list,map和set,每一种集合类型在add、remove等操作发生的时候都会触发java.beans.PropertyChangeEvent事件。要注意的是PropertyChangeEvent并不仅仅是作为一个信号,它里面还包含了确切的事件内容,比如属性名,比如新/老数据等。

针对于不同的事件类型,可能会发送更多种定制的PropertyChangeEvent事件,例如添加操作会产生事件ObservableList.ElementAddedEvent

def event   // 定义一个变量存储被PropertyChangeEventListener捕获的事件                    
def listener = {
    if (it instanceof ObservableList.ElementEvent)  {  // 这个是ObservableList.ElementAddedEvent的父类
        event = it
    }
} as PropertyChangeListener

def observable = [1, 2, 3] as ObservableList   // 创建一个可被监听的list
observable.addPropertyChangeListener(listener)   // 注册监听器

observable.add 42   // 触发添加事件

assert event instanceof ObservableList.ElementAddedEvent

def elementAddedEvent = event as ObservableList.ElementAddedEvent
assert elementAddedEvent.changeType == ObservableList.ChangeType.ADDED
assert elementAddedEvent.index == 3
assert elementAddedEvent.oldValue == null
assert elementAddedEvent.newValue == 42

需要意识到的是,一个添加的操作,最终是触发了两个事件,第一个是ObservableList.ElementAddedEvent:添加,第二个是PropertyChangeEvent:列表size变化事件。

而另一个有趣的是ObservableList.ElementClearedEvent事件。当多个项被移除的时候,比如说clear()操作,它能够把列表中移除的项收集起来:

def event
def listener = {
    if (it instanceof ObservableList.ElementEvent)  {
        event = it
    }
} as PropertyChangeListener

def observable = [1, 2, 3] as ObservableList
observable.addPropertyChangeListener(listener)

observable.clear()

assert event instanceof ObservableList.ElementClearedEvent

def elementClearedEvent = event as ObservableList.ElementClearedEvent
assert elementClearedEvent.values == [1, 2, 3]
assert observable.size() == 0

ObservableMapObservableSet这两个东西实际上和ObservableList是一致的

六、小结

基本上是把groovy官方的文档搬过来了,不过这样过了一遍之后,感觉脑子里面对groovy的印象加深很多,看到一些简单的groovy脚本也是能明白都干了些什么,也算是入了门,不过这里仅仅是一些基础的介绍,还有很多东西需要去学,推荐直接去看groovy官方文档库,挺有意思的。