【認識 Gradle】(6)Java 專案與 Build Script 客製化
【認識 Gradle】(5)Gradle Task 觀念導讀 << 前情 前二篇是針對 Gradle Script 識讀的概念進行較細節的介紹。Gradle 的生態圈是以 Java 語言為主的專案,例如 Java 應用程式、Java Web 專案或 Android 專案較為常見。 許多的 Open Source 專案也由 Maven 轉移至 Gradle。由學習使用 Gradle 輔助 Java 專案的開發是個很好上手的起點,使用 Gradle 建置 Java 專案可參考官手冊: 相信有在 follow 的讀者發現,我們給予的材料都是依官方手冊為主的。除了已購入 Gradle 書籍的讀者之外,我想官方手冊已經有充足的資訊讓 Gradle 使用者解決日常開發的需要。而此系列教學的目標在於多解說一些概念,讓讀者能讀懂官方手冊。這些概念是散落於手冊許多部分的,或是非 Gradle 專屬的領域的(像是 Groovy 的用法)。 觀念的傳達試著讓讀者將這些資訊黏起來,讓它們串在一起。我們希望學習者面對新的使用需求或問題時,能優先運用這些觀念與手冊的資訊找到解法,使用搜尋引擎碰運氣才是最後的手段。 Java 專案讀者可參考在 Gradle 起手式 使用的範例。它是一個經典的 Hello World 專案。基本結構、原始碼、設定檔與 /* 引用 java plugin 獲得編譯 java 專案相關的 task $ */ apply plugin: 'java' /* 引用 application plugin 獲得執行 java 專案相關的 task $ */ apply plugin:'application' /* 執行 application plugin 用到的參數 $ */ mainClassName = "tw.com.codedata.HelloWorld" /* 設定 maven repository server $ */ repositories { mavenCentral() } /* 宣告專案的相依函式庫 $ */ dependencies { compile group: 'commons-logging', name: 'commons-logging', version: '1.1.1' compile group: 'log4j', name: 'log4j', version: '1.2.16' } 這簡單的範例是個起點,但它離實際運用的仍有距離。一個實際在運作的專案來,可不是程式碼可以編成一個 jar 檔就行。還要依不同的目標產生不同的編譯產出。例如:依部署環境不同使用不同的設定檔,像是在測試環境使用的 DB Server 位置、相關設定檔內容或是快取存放位置都有修改的必要。這些事過去你可能是用 Ant 執行或手工修改設定檔。這樣的工作應該依賴編譯工具,並加上檢核的步驟避免人為錯誤。 /* 引用 java plugin 獲得編譯 java 專案相關的 task $ */ apply plugin: 'java' 我們再度回到起點,使用 apply 委派 Project 物件將 Java Plugin 加進這個專案。這回課程的主要目標是講述日常開發的事物,我們不會過度深入 Gradle 內部實作的細節,所以讀者只要知道 SourceSets新的變數最廣為人知的為 sourceSets,這也是多數想要客製化 Java 專案的學習者最先認識的變數。在 Gradle 的手冊中將這類別變數稱為 Convention Properties,透過 Gradle 的機制將它由 Plugin 內部曝露在 Build Script 可以取用的範圍,所以變更它即能影響 Plugin 的行為。sourceSets 是其中一個變數,我們可以用它來改變 Project Layout。 qty:gradleLab qrtt1$ tree . . ├── build.gradle └── src ├── main │ ├── java │ └── resources └── test ├── java └── resources 以上面的例子來看,它是典型的 Java 專案的 Project Layout,這慣例如同 Maven 使用的 Project Layout 一致。不過有許多理由,我們不想要用這樣的 Project Layout,例如舊的專案,特別是已經透過版本系統管理的舊專案,不是因為『需求變更』而變更路徑的情況使得版本記錄不連貫,這使得一些原本在路一個路徑下的記錄轉移到不同的路徑,使得在語意上屬於同個檔案的歷史有著二代的記錄。 當然還有其他同樣理直氣壯的理由,例如:Gradle 只是作為輔助的編譯工具,同時還並存著 Ant 或 Maven 讓其他尚未接納 Gradle 作為編譯工具的同事使用。不管什麼理由,你需要改變它成這個樣子: qty:gradleLab qrtt1$ tree . ├── build.gradle ├── config │ └── log4j.properties └── src └── tw └── com └── codedata └── HelloWorld.java src 即為原先 apply plugin: 'java' repositories { mavenCentral() } dependencies { compile group: 'commons-logging', name: 'commons-logging', version: '1.1.1' compile group: 'log4j', name: 'log4j', version: '1.2.16' } sourceSets { main { java { srcDir 'src' } resources { srcDir 'config' } } } 修改好後,編譯居然也能動了,實在很神奇。回想一下前幾回提過的觀念,它只是在呼叫 delegate 方法。我們稍為改寫一下 Build Script: println sourceSets.class sourceSets { println main.class main { println java.class java { srcDir 'src' } println resources.class resources { srcDir 'config' } } } 執行 gradle 會印出: class org.gradle.api.internal.tasks.DefaultSourceSetContainer_Decorated class org.gradle.api.internal.tasks.DefaultSourceSet_Decorated class org.gradle.api.internal.file.DefaultSourceDirectorySet class org.gradle.api.internal.file.DefaultSourceDirectorySet 所以,我們可以知道:
查詢 Javadoc 可以明顯地發現, 另一個 sourceSets 常用的功能是排除某些檔案,不要讓它被編譯或被複製。在 SourceSet Javadoc 上有這樣的例子: apply plugin: 'java' sourceSets { main { java { exclude 'some/unwanted/package/**' } } } 這個排除的設定由『源頭』就被封鎖,它間接影響到產生 IDE 設定的 plugin。以 eclipse plugin 來說,它會在 classpath 內排除它。對於 Java 原始碼來說,會看到它標示成不同的 Icon,如果是一般的設定檔就比較看不出來。舉個例來說,如果將所有 .properties 為副檔名的檔案在 sourceSets 內排除: sourceSets { main { java { srcDir 'src' exclude '**/*.properties' } resources { srcDir 'config' } } } 產生出來的 .classpath 會包含: <classpathentry excluding="**/*.properties" kind="src" path="src"/> 而這類的設定檔本身沒有特別的圖示標明它是否為『作用中』的狀態,比較不容易察覺吃不到設定檔產生的錯誤。所以,在 sourceSets 內使用排除檔案或目錄的功能要特別小心。若只是針對輸出的 jar 檔要排除檔案,可以在其他的流程處理。 Java Tasks當你的專案引用 java plugin 後,它就會增加上述的 task。這圖是取自 Chapter 23. The Java Plugin。Java Plugin 建構出如圖中所示的 Task 相依關係,當你試著呼叫 qty:gradleLab qrtt1$ gradle build :compileJava :processResources :classes :jar :assemble :compileTestJava UP-TO-DATE :processTestResources UP-TO-DATE :testClasses UP-TO-DATE :test UP-TO-DATE :check UP-TO-DATE :build BUILD SUCCESSFUL 執行 每一個 task 都有方法修改它預設的行為,以 要客製化既有 task 的途徑有二種,一種是利用 task 的 configure closure,一種是利用 Plugin 提供的 Convention Properites。 在 【認識 Gradle】(5)Gradle Task 觀念導讀 時,我們已經介紹過 configure closure,它用法很直覺就是替 task 寫個 closure 罷了,像在前一段提到的除排特定檔案,我們可以選擇在打包 jar 時進行: jar { exclude '**/log4j.properties' } 編譯出的檔案內就不會出現 log4j.properties: qty:gradleLab qrtt1$ unzip -l build/libs/gradleLab.jar Archive: build/libs/gradleLab.jar Length Date Time Name -------- ---- ---- ---- 0 12-21-13 13:10 META-INF/ 25 12-21-13 13:10 META-INF/MANIFEST.MF 0 12-20-13 18:00 tw/ 0 12-20-13 18:00 tw/com/ 0 12-20-13 18:00 tw/com/codedata/ 750 12-20-13 18:00 tw/com/codedata/HelloWorld.class -------- ------- 775 6 files 在 library 內排除特定設定檔是相當常見的功能,我們會希望 library 使用者明確指定他應該準備的設定檔。這種細節可以避免忘了放檔案,程式能動但動的不如期望的情況發生。 另一種的客製化就是透過 Plugin 事先準備好的 Convention Properties 來做,例如在 Java 專案內設定編譯選項: sourceCompatibility=1.4 targetCompatibility=1.5 指定原始碼、目的碼版本,在一些舊專案特別有用,像是一些舊專案把 compileJava.options.encoding='UTF-8' 除了 compileJava 外,還有 compileTestJava 也同樣能設定,加上 javadoc 也可以設成需要的編碼。這可以透過 groovy 的 spread operator 寫成比較騷包的型式: [compileJava, compileTestJava, javadoc]*.options*.encoding = 'UTF-8' 若你的 Build Script 更複雜,有更多組的 sourceSet 的 Java Compiler 需要綁定,直接寫 task 名稱去設定就可能有所遺漏。這樣可以改成以 task 型別的方法去修改參數,任何 task 有繼承自 Compile 的 task 就修改它的 options 設定為 UTF-8: tasks.withType(Compile) { options.encoding='UTF-8' } Packaging日常開發的活動,不只單純把 code 寫出來,將專案打包成可以發佈的型式為一個重要的環節。實際的專案它會有許多組的設定檔,以下目錄節構是模擬出來的一種情境: qty:gradleLab qrtt1$ tree . ├── build.gradle ├── config │ └── log4j.properties ├── scripts │ └── run.sh ├── profile_dev │ └── log4j.properties ├── profile_production │ └── log4j.properties ├── scripts └── src └── tw └── com └── codedata └── HelloWorld.java 新的目錄結構,多出三個目錄:
現在有個目標是適當地打包編譯後結果,有幾個可能的打包型式。透過指定 profileDir 參數選擇打包時要包含的檔案: gradle pack -PprofileDir=profile_dev gradle pack -PprofileDir=profile_dev 或是設定成不需要指定參數的,改由支援不同的 task 來打包: gradle pack_dev gradle pack_production 也可以設計成一個 task 自動依 profile 目錄打包出對應的版本: gradle pack 上述的型式都是可行的方案,不過實際用起來越簡單越好。因為你馬上會發現,身為作者的自己常被詢問該加什麼參數。至於打包的型式,常見的是 ZIP 檔,如果是 Web 專案那就會是 WAR 檔(其實還是 ZIP 啊),或者輸出一個目錄方便透過 rsync 或 s3sync 之類的檔案工具,僅同步有變更的檔案。明白『需求』後,以自動偵測 profile 目錄來進行打包為目標,增加 在實作上我們可以分別實作 packdev task 與 packproduction task,再用一個 pack 建立相依關係: task pack(dependsOn: [pack_dev, pack_production]) Gradle 是建立在 DSL 之上的編譯工具,那就能適時地發揮它作為 Programming Language 的威力,我們這回試著寫成 官方文件 Build Script Basics 內提過的 Dynamic tasks: file(".").eachDir { profile -> /* 掃瞄目前的目錄,找出名稱含 profile_ 的目錄 */ if (profile.name =~ /profile_/) { /* 取出 profile_ 後的字作為 task 識別名稱 */ def key = profile.name - "profile_" /* 建立新的 tack 命名為 pack_ 開頭的 task 名稱 */ task "pack_${key}"(dependsOn:clean, group:'Package', type:Zip) { /* 指定 ZIP 檔的主檔名 */ baseName = project.name + "_" + key /* 指定要存入的 ZIP Entries */ into (project.name + '/libs') { /* 包含被設進 compile 分類的 dependencies */ from configurations.compile /* 包含 jar task 產生的檔案 */ from jar.outputs.files } into (project.name) { /* 包含 scripts 目錄下的檔案 */ from file("./scripts").listFiles() /* 包含目前 profile 路徑的檔案 */ from profile.listFiles() } } } } /* 建立一個 pack task 相依於其他 pack_ 開頭命名的 task */ task pack(dependsOn: tasks.matching { it.name =~ /pack_/} ) 透過動態的方式產生 task,對未來新增環境時的擴充相當容易,在程式有良好實作參數化的情況下,只要加新的 profile_ 目錄就行了,較少有機會需要再改變 Build Script 的設計。 課程回顧這次的課程提到運用 Gradle 開發 Java 專案常見的客製化情境,現實的情況可能會更加複雜,但掌握專案 Build Script 客製化的幾個管道,就能得心應手:
上述三者為簡單、快速的客製化途徑。聰明的讀者可能立馬想到『還有實作 plugin 吧!』,這確實是個途徑之一,不過本質上也是把上述三個基本方式以 plugin 的型式實作,而 plugin 實作得考慮更多的問題,後續會再有獨立的篇幅討論。 |
pcbill
01/29
文中
compileJava.options.encoding='UTF-8'
的設定是不是多了 code tag?