Gradle 詳解
Gradle是當前非常“勁爆”的構建工具。本篇文章就是專為講解Gradle而來。介紹Gradle之前,先說點題外話。
一、題外話
說實話,我在索尼工作的時候,就見過Gradle。但是當時我一直不知道這是什麼東西。而且索尼工具組的工程師還將其和Android Studio索尼版一起推送,偶一看就更沒興趣了。為什麼那個時候如此不待見Gradle呢?因為我此前一直是做ROM開發。在這個層面上,我們用make,mm或者mmm就可以了。而且,編譯耗時對我們來說也不是啥痛點,因為用組內吊炸天的神機伺服器完整編譯索尼的image也要耗費1個小時左右。所以,那個時侯Gradle完全不是我們的菜。
現在,搞APP開發居多,編譯/打包等問題立即就成痛點了。比如:
- 一個APP有多個版本,Release版、Debug版、Test版。甚至針對不同APP Store都有不同的版本。在以前ROM的環境下,雖然可以配置Android.mk,但是需要依賴整個Android原始碼,而且還不能完全做到滿足條件,很多事情需要手動搞。一個app如果涉及到多個開發者,手動操作必然會帶來混亂。
- library工程我們需要編譯成jar包,然後釋出給其他開發者使用。以前是用eclipse的export,做一堆選擇。要是能自動編譯成jar包就爽了。
上述問題對絕大部分APP開發者而言都不陌生,而 Gradle
二、閒言構建
構建,叫build也好,叫make也行。反正就是根據輸入資訊然後幹一堆事情,最後得到幾個產出物(Artifact)。
最最簡單的構建工具就是make了。make就是根據Makefile檔案中寫的規則,執行對應的命令,然後得到目標產物。
日常生活中,和構建最類似的一個場景就是做菜。輸入各種食材,然後按固定的工序,最後得到一盤菜。當然,做同樣一道菜,由於需求不同,做出來的東西也不盡相同。比如,宮保雞丁這道菜,回民要求不能放大油、口淡的要求少放鹽和各種油、辣不怕的男女漢子們可以要求多放辣子....總之,做菜包含固定的工序,但是對於不同條件或需求,需要做不同的處理。
在Gradle爆紅之前,常用的構建工具是ANT,然後又進化到Maven。ANT和Maven這兩個工具其實也還算方便,現在還有很多地方在使用。但是二者都有一些缺點,所以讓更懶得人覺得不是那麼方便。比如,Maven編譯規則是用XML來編寫的。XML雖然通俗易懂,但是很難在xml中描述if{某條件成立,編譯某檔案}/else{編譯其他檔案} 這樣有不同條件的任務。
怎麼解決?怎麼解決好?對程式設計師而言,自然是程式設計解決,但是有幾個小要求:
- 這種“程式設計”不要搞得和程式設計師理解的程式設計那樣複雜。寥寥幾筆,輕輕鬆鬆把要做的事情描述出來就最好不過。所以,Gradle選擇了Groovy。Groovy基於Java並拓展了Java。 Java程式設計師可以無縫切換到使用Groovy開發程式。 Groovy說白了就是把寫Java程式變得像寫指令碼一樣簡單。寫完就可以執行,Groovy內部會將其編譯成Java class然後啟動虛擬機器來執行。當然,這些底層的渣活不需要你管。
- 除了可以用很靈活的語言來寫構建規則外,Gradle另外一個特點就是它是一種DSL,即Domain Specific Language ,領域相關語言。什麼是DSL,說白了它是某個行業中的行話。還是不明白?徐克導演得《智取威虎山》中就有很典型的DSL使用描述,比如:
土匪:蘑菇,你哪路?什麼價?(什麼人?到哪裡去?)
楊子榮:哈!想啥來啥,想吃奶來了媽媽,想孃家的人,孩子他舅舅來了。(找同行)
楊子榮:拜見三爺!
土匪:天王蓋地虎!(你好大的膽!敢來氣你的祖宗?)
楊子榮:寶塔鎮河妖!(要是那樣,叫我從山上摔死,掉河裡淹死。)
土匪:野雞悶頭鑽,哪能上天王山!(你不是正牌的。)
楊子榮:地上有的是米,喂呀,有根底!(老子是正牌的,老牌的。)
Gradle中也有類似的行話,比如sourceSets代表原始檔的集合等..... 太多了,記不住 。以後我們都會接觸到這些行話。那麼,對使用者而言,這些行話的好處是什麼呢?這就是:
一句行話可以包含很多意思,而且在這個行當裡的人一聽就懂,不用解釋。另外,基於行話,我們甚至可以建立一個模板,使用者只要往這個模板裡填必須要填的內容,Gradle就可以非常漂亮得完成工作,得到想要的東西。
這就和現在的智慧炒菜機器似的,只要選擇菜譜,把食材準備好,剩下的事情就不用你操心了。吃貨們對這種做菜方式肯定是以反感為主,太沒有特色了。但是程式設計師對Gradle類似做法卻熱烈擁抱。
到此,大家應該明白要真正學會Gradle恐怕是離不開下面兩個基礎知識:
- Groovy,由於它基於Java,所以我們僅介紹Java之外的東西。瞭解Groovy語言是掌握Gradle的基礎。
- Gradle作為一個工具,它的行話和它“為人處事”的原則。
三、Groovy介紹
Groovy是一種動態語言。這種語言比較有特點,它和Java一樣,也運行於Java虛擬機器中。恩??對頭,簡單粗暴點兒看,你可以認為Groovy擴充套件了Java語言。比如,Groovy對自己的定義就是:Groovy是在 java平臺上的、 具有像Python, Ruby 和 Smalltalk 語言特性的靈活動態語言, Groovy保證了這些特性像 Java語法一樣被 Java開發者使用。
除了語言和Java相通外,Groovy有時候又像一種指令碼語言。前文也提到過,當我執行Groovy指令碼時,Groovy會先將其編譯成Java類位元組碼,然後通過Jvm來執行這個Java類。圖1展示了Java、Groovy和Jvm之間的關係。
實際上,由於Groovy Code在真正執行的時候已經變成了Java位元組碼,所以JVM根本不知道自己執行的是Groovy程式碼 。
下面我們將介紹Groovy。由於此文的主要目的是Gradle,所以我們不會過多討論Groovy中細枝末節的東西,而是把知識點集中在以後和Gradle打交道時一些常用的地方上。
3.1 Groovy開發環境
在學習本節的時候,最好部署一下Groovy開發環境。根據 Groovy官網 的介紹,部署Groovy開發環境非常簡單,在Ubuntu或者cygwin之類的地方:
- curl -s get.gvmtool.net | bash
- source "$HOME/.gvm/bin/gvm-init.sh"
- gvm install groovy
- 執行完最後一步,Groovy就下載並安裝了。
然後,建立一個test.groovy檔案,裡邊只有一行程式碼:
println "hello groovy"
- 執行groovy test.groovy,輸出結果如圖2所示:
親們,必須要完成上面的操作啊。做完後,有什麼感覺和體會?
最大的感覺可能就是groovy和shell指令碼,或者python好類似。
另外,除了可以直接使用JDK之外,Groovy還有一套 GDK 。
說實話,看了這麼多家API文件,還是Google的Android API文件做得好。其頁面中右上角有一個搜尋欄,在裡邊輸入一些關鍵字,瞬間就能列出候選類,相關文件,方便得不得了啊.....
3.2 一些前提知識
為了後面講述方面,這裡先介紹一些前提知識。初期接觸可能有些彆扭,看習慣就好了。
- Groovy註釋標記和Java一樣,支援 // 或者 /**/
- Groovy語句可以不用分號結尾。Groovy為了儘量減少程式碼的輸入,確實煞費苦心
- Groovy中支援動態型別,即 定義變數的時候可以不指定其型別 。Groovy中,變數定義可以使用關鍵字def。 注意,雖然def不是必須的,但是為了程式碼清晰,建議還是使用def關鍵字
def variable1 = 1 //可以不使用分號結尾
def varable2 = "I am a person"
def int x = 1 //變數定義時,也可以直接指定型別
- 函式定義時,引數的型別也可以不指定。比如
String testFunction(arg1,arg2){//無需指定引數型別 ... }
- 除了變數定義可以不指定型別外,Groovy中函式的返回值也可以是無型別的。比如:
//無型別的函式定義,必須使用def關鍵字
def nonReturnTypeFunc(){
last_line //最後一行程式碼的執行結果就是本函式的返回值
}
//如果指定了函式返回型別,則可不必加def關鍵字來定義函式
String getString(){
return "I am a string"
}
其實,所謂的無返回型別的函式,我估計內部都是按返回Object型別來處理的。畢竟,Groovy是基於Java的,而且最終會轉成Java Code執行在JVM上
- 函式返回值:Groovy的函式裡,可以不使用return xxx來設定xxx為函式返回值。如果不使用return語句的話,則函式裡最後一句程式碼的執行結果被設定成返回值。比如
//下面這個函式的返回值是字串"getSomething return value"
def getSomething(){
"getSomething return value" //如果這是最後一行程式碼,則返回型別為String
1000 //如果這是最後一行程式碼,則返回型別為Integer
}
注意,如果函式定義時候指明瞭返回值型別的話,函式中則必須返回正確的資料型別,否則執行時報錯。如果使用了動態型別的話,你就可以返回任何型別了。
- Groovy對字串支援相當強大,充分吸收了一些指令碼語言的優點:
1 單引號''中的內容嚴格對應Java中的String,不對$符號進行轉義
def singleQuote='I am $ dolloar' //輸出就是I am $ dolloar
2 雙引號""的內容則和指令碼語言的處理有點像,如果字元中有$號的話,則它會 $表示式 先求值。
def doubleQuoteWithoutDollar = "I am one dollar" //輸出 I am one dollar
def x = 1
def doubleQuoteWithDollar = "I am $x dolloar" //輸出I am 1 dolloar
3 三個引號'''xxx'''中的字串支援隨意換行 比如
def multieLines = ''' begin line 1 line 2 end '''
- 最後,除了每行程式碼不用加分號外,Groovy中函式呼叫的時候還可以不加括號。比如:
println("test") ---> println "test"
注意,雖然寫程式碼的時候,對於函式呼叫可以不帶括號,但是Groovy經常把屬性和函式呼叫混淆。比如
def getSomething(){ "hello" }
getSomething() //如果不加括號的話,Groovy會誤認為getSomething是一個變數。
所以,呼叫函式要不要帶括號,我個人意見是如果這個函式是Groovy API或者Gradle API中比較常用的,比如println,就可以不帶括號。否則還是帶括號。Groovy自己也沒有太好的辦法解決這個問題,只能 兵來將擋水來土掩 了。
好了,瞭解上面一些基礎知識後,我們再介紹點深入的內容。
3.3 Groovy中的資料型別
Groovy中的資料型別我們就介紹兩種和Java不太一樣的:
- 一個是Java中的基本資料型別。
- 另外一個是Groovy中的容器類。
- 最後一個非常重要的是閉包。
放心,這裡介紹的東西都很簡單
3.3.1 基本資料型別
作為動態語言,Groovy世界中的所有事物都是物件。所以, int,boolean這些Java中的基本資料型別,在Groovy程式碼中其實對應的是它們的包裝資料型別。比如int對應為Integer,boolean對應為Boolean。 比如下圖中的程式碼執行結果:
圖4 int實際上是Integer
3.3.2 容器類
Groovy中的容器類很簡單,就三種:
- List:連結串列,其底層對應Java中的List介面,一般用ArrayList作為真正的實現類。
- Map:鍵-值表,其底層對應Java中的LinkedHashMap。
- Range:範圍,它其實是List的一種拓展。
對容器而言,我們最重要的是瞭解它們的用法。下面是一些簡單的例子:
1. List類
變數定義:List變數由[]定義,比如
def aList = [5,'string',true] //List由[]定義,其元素可以是任何物件
變數存取:可以直接通過索引存取,而且不用擔心索引越界。如果索引超過當前連結串列長度,List會自動
往該索引新增元素
assert aList[1] == 'string'
assert aList[5] == null //第6個元素為空
aList[100] = 100 //設定第101個元素的值為10
assert aList[100] == 100
那麼,aList到現在為止有多少個元素呢?
println aList.size ===>結果是101
2. Map類
容器變數定義
變數定義:Map變數由[:]定義,比如
def aMap = ['key1':'value1','key2':true]
Map由[:]定義,注意其中的冒號。冒號左邊是key,右邊是Value。key必須是字串,value可以是任何物件。另外,key可以用''或""包起來,也可以不用引號包起來。比如
def aNewMap = [key1:"value",key2:true] //其中的key1和key2預設被
處理成字串"key1"和"key2"
不過Key要是不使用引號包起來的話,也會帶來一定混淆,比如
def key1="wowo"
def aConfusedMap=[key1:"who am i?"]
aConfuseMap中的key1到底是"key1"還是變數key1的值“wowo”?顯然,答案是字串"key1"。如果要是"wowo"的話,則aConfusedMap的定義必須設定成:
def aConfusedMap=[(key1):"who am i?"]
Map中元素的存取更加方便,它支援多種方法:
println aMap.keyName <==這種表達方法好像key就是aMap的一個成員變數一樣
println aMap['keyName'] <==這種表達方法更傳統一點
aMap.anotherkey = "i am map" <==為map新增新元素
3. Range類
Range是Groovy對List的一種拓展,變數定義和大體的使用方法如下:
def aRange = 1..5 <==Range型別的變數 由begin值+兩個點+end值表示
左邊這個aRange包含1,2,3,4,5這5個值
如果不想包含最後一個元素,則
def aRangeWithoutEnd = 1..<5 <==包含1,2,3,4這4個元素
println aRange.from
println aRange.to
3.3.4 Groovy API的一些祕笈
前面講這些東西,主要是讓大家瞭解Groovy的語法。實際上在coding的時候,是離不開SDK的。由於Groovy是動態語言,所以要使用它的SDK也需要掌握一些小訣竅。
Groovy的API文件位於 http://www.groovy-lang.org/api.html
以上文介紹的Range為例,我們該如何更好得使用它呢?
- 先定位到Range類。它位於groovy.lang包中:
有了API文件,你就可以放心呼叫其中的函數了。 不過,不過,不過 :我們剛才程式碼中用到了Range.from/to屬性值,但翻看Range API文件的時候,其實並沒有這兩個成員變數。圖6是Range的方法
文件中並沒有說明Range有from和to這兩個屬性,但是卻有getFrom和getTo這兩個函式。What happened? 原來:
根據Groovy的原則,如果一個類中有名為xxyyzz這樣的屬性(其實就是成員變數),Groovy會自動為它新增getXxyyzz和setXxyyzz兩個函式,用於獲取和設定xxyyzz屬性值。
注意,get和set後第一個字母是大寫的
所以,當你看到Range中有getFrom和getTo這兩個函式時候,就得知道潛規則下,Range有from和to這兩個屬性。當然,由於它們不可以被外界設定,所以沒有公開setFrom和setTo函式。
3.4 閉包
3.4.1 閉包的樣子
閉包,英文叫Closure,是Groovy中非常重要的一個數據型別或者說一種概念了。閉包的歷史來源,種種好處我就不說了。我們直接看怎麼使用它!
閉包,是一種資料型別,它代表了一段可執行的程式碼。其外形如下:
def aClosure = {//閉包是一段程式碼,所以需要用花括號括起來..
Stringparam1, int param2 -> //這個箭頭很關鍵。箭頭前面是引數定義,箭頭後面是程式碼
println"this is code" //這是程式碼,最後一句是返回值,
//也可以使用return,和Groovy中普通函式一樣
}
簡而言之,Closure的定義格式是:
def xxx = {paramters -> code} //或者
def xxx = {無引數,純code} 這種case不需要->符號
說實話,從C/C++語言的角度看,閉包和函式指標很像 。閉包定義好後,要呼叫它的方法就是:
閉包物件.call(引數) 或者更像函式指標呼叫的方法:
閉包物件(引數)
比如:
aClosure.call("this is string",100) 或者
aClosure("this is string", 100)
上面就是一個閉包的定義和使用。在閉包中,還需要注意一點:
如果閉包沒定義引數的話,則隱含有一個引數,這個引數名字叫it,和this的作用類似。it代表閉包的引數。
比如:
def greeting = { "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
等同於:
def greeting = { it -> "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
但是,如果在閉包定義時,採用下面這種寫法,則表示閉包沒有引數!
def noParamClosure = { -> true }
這個時候,我們就不能給noParamClosure傳引數了!
noParamClosure ("test") <==報錯喔!
3.4.2 Closure使用中的注意點
1. 省略圓括號
閉包在Groovy中大量使用,比如很多類都定義了一些函式,這些函式最後一個引數都是一個閉包。比如:
public static <T> List<T> each(List<T> self, Closure closure)
上面這個函式表示針對List的每一個元素都會呼叫closure做一些處理。這裡的closure,就有點回調函式的感覺。但是,在使用這個each函式的時候,我們傳遞一個怎樣的Closure進去呢?比如:
def iamList = [1,2,3,4,5] //定義一個List
iamList.each{ //呼叫它的each,這段程式碼的格式看不懂了吧?each是個函式,圓括號去哪了?
println it
}
上面程式碼有兩個知識點:
- each函式呼叫的圓括號不見了 !原來,Groovy中,當函式的最後一個引數是閉包的話,可以省略圓括號。比如
def testClosure(int a1,String b1, Closure closure){
//do something
closure() //呼叫閉包
}
那麼呼叫的時候,就可以免括號!
testClosure (4, "test", {
println "i am in closure"
} ) //紅色的括號可以不寫..
注意,這個特點非常關鍵,因為以後在Gradle中經常會出現圖7這樣的程式碼:
經常碰見圖7這樣的沒有圓括號的程式碼。省略圓括號雖然使得程式碼簡潔,看起來更像指令碼語言,但是它這經常會讓我confuse(不知道其他人是否有同感),以doLast為例,完整的程式碼應該按下面這種寫法:
doLast({ println 'Hello world!' })
有了圓括號,你會知道 doLast只是把一個Closure物件傳了進去。很明顯,它不代表這段指令碼解析到doLast的時候就會呼叫println 'Hello world!' 。
但是把圓括號去掉後,就感覺好像println 'Hello world!'立即就會被呼叫一樣!
2. 如何確定Closure的引數
另外一個比較讓人頭疼的地方是,Closure的引數該怎麼搞?還是剛才的each函式:
public static <T> List<T> each(List<T> self, Closure closure)
如何使用它呢?比如:
def iamList = [1,2,3,4,5] //定義一個List變數
iamList.each{ //呼叫它的each函式,只要傳入一個Closure就可以了。
println it
}
看起來很輕鬆,其實:
- 對於each所需要的Closure,它的引數是什麼?有多少個引數?返回值是什麼?
我們能寫成下面這樣嗎?
iamList.each{String name,int x ->
return x
} //執行的時候肯定報錯!
所以,Closure雖然很方便,但是它一定會和使用它的上下文有極強的關聯。要不,作為類似回撥這樣的東西,我如何知道呼叫者傳遞什麼引數給Closure呢?
此問題如何破解?只能通過查詢API文件才能瞭解上下文語義。比如下圖8:
圖8中:
- each函式說明中,將給指定的closure傳遞Set中的每一個item。所以,closure的引數只有一個。
- findAll中, 絕對抓瞎 了。一個是沒說明往Closure裡傳什麼。另外沒說明Closure的返回值是什麼.....。
對Map的findAll而言,Closure可以有兩個引數。findAll會將Key和Value分別傳進去。並且,Closure返回true,表示該元素是自己想要的。返回false表示該元素不是自己要找的 。示意程式碼如圖9所示:
Closure的使用有點坑,很大程度上依賴於你對API的熟悉程度,所以最初階段,SDK查詢是少不了的。
3.5 指令碼類、檔案I/O和XML操作
最後,我們來看一下Groovy中比較高階的用法。
3.5.1 指令碼類
1. 指令碼中import其他類
Groovy中可以像Java那樣寫package,然後寫類。比如在資料夾com/cmbc/groovy/目錄中放一個檔案,叫Test.groovy,如圖10所示:
你看,圖10中的Test.groovy和Java類就很相似了。當然,如果不宣告public/private等訪問許可權的話,Groovy中類及其變數預設都是public的。
現在,我們在測試的根目錄下建立一個test.groovy檔案。其程式碼如下所示:
你看,test.groovy先import了com.cmbc.groovy.Test類,然後建立了一個Test型別的物件,接著呼叫它的print函式。
這兩個groovy檔案的目錄結構如圖12所示:
在groovy中,系統自帶會載入當前目錄/子目錄下的xxx.groovy檔案。所以,當執行groovy test.groovy的時候,test.groovy import的Test類能被自動搜尋並載入到。
2. 指令碼到底是什麼
Java中,我們最熟悉的是類。但是我們在Java的一個原始碼檔案中,不能不寫class(interface或者其他....),而Groovy可以像寫指令碼一樣,把要做的事情都寫在xxx.groovy中,而且可以通過groovy xxx.groovy直接執行這個指令碼。這到底是怎麼搞的?
既然是基於Java的,Groovy會先把xxx.groovy中的內容轉換成一個Java類。比如:
test.groovy的程式碼是:
println 'Groovy world!'
Groovy把它轉換成這樣的Java類:
執行 groovyc -d classes test.groovy
groovyc 是groovy的編譯命令,-d classes用於將編譯得到的class檔案拷貝到classes資料夾下
圖13是test.groovy指令碼轉換得到的java class。用jd-gui反編譯它的程式碼:
圖13中:
- test.groovy被轉換成了一個test類,它從script派生。
- 每一個指令碼都會生成一個static main函式。這樣,當我們groovy test.groovy的時候,其實就是用java去執行這個main函式
- 指令碼中的所有程式碼都會放到run函式中 。比如,println 'Groovy world',這句程式碼實際上是包含在run函式裡的。
- 如果指令碼中定義了函式,則函式會被定義在test類中。
groovyc 是一個比較好的命令,讀者要掌握它的用法。然後利用jd-gui來檢視對應class的Java原始碼。
3. 指令碼中的變數和作用域
前面說了,xxx.groovy只要不是和Java那樣的class,那麼它就是一個指令碼。而且指令碼的程式碼其實都會被放到run函式中去執行。那麼,在Groovy的指令碼中,很重要的一點就是指令碼中定義的 變數和它的作用域 。舉例:
def x = 1 <==注意,這個x有def(或者指明型別,比如 int x = 1)
def printx(){
println x
}
printx() <==報錯,說x找不到
為什麼?繼續來看反編譯後的class檔案。
圖14中:
- printx被定義成test類的成員函式
- def x = 1 ,這句話是在run中建立的。所以,x=1從程式碼上看好像是在整個指令碼中定義的,但實際上printx訪問不了它。printx是test成員函式,除非x也被定義成test的成員函式,否則printx不能訪問它。
那麼,如何使得printx能訪問x呢?很簡單,定義的時候不要加型別和def。即:
x = 1 <==注意,去掉def或者型別
def printx(){
println x
}
printx() <==OK
這次Java原始碼又變成什麼樣了呢?
圖15中,x也沒有被定義成test的成員函式,而是在run的執行過程中,將x作為一個屬性新增到test例項物件中了。然後在printx中,先獲取這個屬性。
注意,Groovy的文件說 x = 1這種定義將使得x變成test的成員變數,但從反編譯情況看,這是不對的.....
雖然printx可以訪問x變量了,但是假如有其他指令碼卻無法訪問x變數。因為它不是test的成員變數。
比如,我在測試目錄下建立一個新的名為test1.groovy。這個test1將訪問test.groovy中定義的printx函式:
這種方法使得我們可以將程式碼分成模組來編寫, 比如將公共的功能放到test.groovy中,然後使用公共功能的程式碼放到test1.groovy中 。
執行groovy test1.groovy,報錯。說x找不到。這是因為x是在test的run函式動態加進去的。怎麼辦?
import groovy.transform.Field; //必須要先import
@Field x = 1 <==在x前面加上@Field標註,這樣,x就徹徹底底是test的成員變量了。
檢視編譯後的test.class檔案,得到:
這個時候,test.groovy中的x就成了test類的成員函數了。如此,我們可以在script中定義那些需要輸出給外部指令碼或類使用的變量了!
3.5.2 檔案I/O操作
本節介紹下Groovy的檔案I/O操作。直接來看例子吧,雖然比Java看起來簡單,但要理解起來其實比較難。尤其是當你要自己查SDK並編寫程式碼的時候。
整體說來,Groovy的I/O操作是在原有Java I/O操作上進行了更為簡單方便的封裝,並且使用Closure來簡化程式碼編寫。主要封裝瞭如下一些了類:
1. 讀檔案
Groovy中,檔案讀操作簡單到令人髮指:
def targetFile = new File(檔名) <==File物件還是要建立的。
然後開啟http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/File.html
看看Groovy定義的API:
1 讀該檔案中的每一行:eachLine的唯一引數是一個Closure。Closure的引數是檔案每一行的內容
其內部實現肯定是Groovy開啟這個檔案,然後讀取檔案