
【認識 Gradle】(1)講古的時間 Apache Ant
在開發 Java 應用程式的過程中,早前常見的自動化編譯工具為 Ant 與 Maven。Gradle 是後起之秀,已經越來越多 Open Source 專案由 Maven 轉向 Gradle(更早之前是由 Ant 轉向 Maven)。由於環境的轉變(或稱情勢所逼),現今的 Java 開發者越來越有學習使用 Gradle 輔助專案開發的必要。 依慣例(老梗),我們會使用 Hello World 進行示範: import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class HelloWorld { static Log logger = LogFactory.getLog(HelloWorld.class); public static void main(String[] args) { logger.info("Hello World"); } } 除了程式碼本身 Commons Logging 配合 Log4j 使用,所以在專案內還需要有這二個 Library 與設定檔: log4j.rootLogger=info, stdout, R log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n log4j.appender.R=org.apache.log4j.RollingFileAppender log4j.appender.R.File=codedata.log log4j.appender.R.Append=true log4j.appender.R.layout=org.apache.log4j.PatternLayout log4j.appender.R.layout.ConversionPattern=%p %t [%d{yy/MM/dd HH:mm:ss:SSS}] %c - %m%n 此系列教學,最終的目標是講解如何使用 Gradle 在日常開發活動。不免俗地需要介紹一些在 Gradle 之前推出的工具。在剛開始的幾篇,我們會簡單地介紹 Ant 與 Maven 的基本概念,但不會過於深入細節,只是需向未有經驗的讀者展示一些他們過去沒來得及認識的工具。 Ant 概念速寫Ant 在設計上很簡單,提供一組 XML 標籤,讓你定義一個『專案(project)』內要提供哪些『目標(target)』,在目標內需要描述有哪些『任務(task)』需要被執行。簡而言之,Ant 讓你用 XML 的方式描述一些自動化編譯的事項,就如同撰寫程式語言一般。說到程式語言不外乎,語法、語意與函式,若將上述概念轉換成 Ant 的領域,則為 XML、DataType、Properties、Task。 首先,我們的專案結構如下: 為了將 Hello World 編譯成功,我寫了下面的 Ant Build File: <?xml version="1.0" encoding="UTF-8"?> <project name="project" default="build"> <!-- 透過 property 定義變數,在編譯過程引用。這些變數也能在呼叫 Ant 時被覆寫 例如:透過指定不同的設定檔路徑來區分『開發環境』、『測試環境』與『正式環境』的部署 --> <property name="src.dir" value="src" /> <property name="lib.dir" value="libs" /> <property name="build.dir" value="build" /> <target name="build"> <!-- 建立 build 目錄放置 javac 產出的 .class 檔 --> <mkdir dir="${build.dir}" /> <!-- 呼叫 javac task,並指定 src 目錄為 source code 的目錄, build 目錄為輸出 .class 的目錄 --> <javac srcdir="${src.dir}" destdir="${build.dir}"> <!-- classpath 與 fileset DataType 方便開發者處理路徑與檔案列表的問題 --> <classpath> <fileset dir="${lib.dir}"> <include name="**/*.jar" /> </fileset> </classpath> </javac> </target> </project> 對初次看 Ant Build File 的開發者,有可能無法一下子找到它的脈絡,我們可以將它抽象成這樣的結構: Ant Build File 是以 XML 組成的,它的根節點必需是 在 Ant Build File 內,使用 <property name="src.dir" value="src" /> <property name="lib.dir" value="libs" /> <property name="build.dir" value="build" /> 接著是一個命名為 build 的 target。在 Ant 內,target 代表一系列動作的描述,我們定義 build target 來描述 <target name="build"> <!-- 建立 build 目錄放置 javac 產出的 .class 檔 --> <mkdir dir="${build.dir}" /> <!-- 呼叫 javac task,並指定 src 目錄為 source code 的目錄, build 目錄為輸出 .class 的目錄 --> <javac srcdir="${src.dir}" destdir="${build.dir}"> <!-- classpath 與 fileset DataType 方便開發者處理路徑與檔案列表的問題 --> <classpath> <fileset dir="${lib.dir}"> <include name="**/*.jar" /> </fileset> </classpath> </javac> </target>
<!-- classpath 與 fileset DataType 方便開發者處理路徑與檔案列表的問題 --> <classpath> <fileset dir="${lib.dir}"> <include name="**/*.jar" /> </fileset> </classpath> Ant 提供開發者 DataType 的機制有個明顯的優點,那就是使用者不用再考慮路徑會因作業系統的不同而需要改變寫法。例如該使用 最後總結一下概念速寫:讀者現在已經知道 Ant Build File 的基本結構,並對於 task、property、data type 稍有概念。這個範例的功能只是將 HelloWorld 配合 Logger Library 編譯出來,放到 build.dir 變數指定的路徑上。由於我們設定 project 預設執行的 target 是 build,直接下 ant 指令就可以完成編譯工作: qty:GradleHowToAnt qrtt1$ ant Buildfile: /Users/qrtt1/workspace/GradleHowToAnt/build.xml build: [mkdir] Created dir: /Users/qrtt1/workspace/GradleHowToAnt/build [javac] /Users/qrtt1/workspace/GradleHowToAnt/build.xml:22: warning: 'includeantruntime' was not set, defaulting to build.sysclasspath=last; set to false for repeatable builds [javac] Compiling 1 source file to /Users/qrtt1/workspace/GradleHowToAnt/build BUILD SUCCESSFUL Total time: 1 second qty:GradleHowToAnt qrtt1$ ls build HelloWorld.class Ant 與 Java 專案Ant 的設計相當簡單直覺,加上容易擴充。開發者可以方便地引用第三方 Library 來增加新的 task 或 data type。寫起來雖然是用 XML 在描述編譯工作的行為,但實際上可以把它當作 scripting language 來寫,他也有提供條件判斷,也能在 target 內呼叫其他的 target,還支援引用其他 Ant Build File。聽起來是相當自由,且容易上手的。 若是站在管理程式專案的角度來說,較希望同組人馬會有相近的慣例需要遵循,就如同一組人寫 Java 被要求需使用相同的 coding style 一般。依據經驗與慣例,使用 Ant 作為編譯工具的 Java 專案應該包含哪些 Task,它們又要做到什麼功能?可以回想一下平時開發工作:
PS. 為簡化說明,我們忽略單元測試的步驟。 規劃起來可分別對應成下列 target:
將上面的需求寫成 Ant Build File 大致為: <?xml version="1.0" encoding="UTF-8"?> <project name="helloworld" default="build"> <property name="src.dir" value="src" /> <property name="lib.dir" value="libs" /> <property name="resource.dir" value="resources" /> <property name="build.dir" value="build" /> <property name="dist.dir" value="dist" /> <!-- 刪除 build.dir 與 dist.dir --> <target name="clean"> <delete dir="${build.dir}" /> <delete dir="${dist.dir}" /> </target> <!-- 建立 build.dir 與 dist.dir 與複製相關設定檔 --> <target name="prepare" depends="clean"> <mkdir dir="${build.dir}" /> <mkdir dir="${dist.dir}" /> <mkdir dir="${build.dir}/libs" /> <mkdir dir="${build.dir}/all" /> </target> <target name="build" depends="prepare"> <javac srcdir="${src.dir}" destdir="${build.dir}" debug="true"> <classpath> <fileset dir="${lib.dir}"> <include name="**/*.jar" /> </fileset> </classpath> </javac> </target> <!-- 將專案的編譯結果打包成 jar --> <target name="jar" depends="build"> <jar destfile="${dist.dir}/libs/${ant.project.name}.jar"> <fileset dir="${build.dir}" /> </jar> </target> <!-- 複製相關的 library 與專案的 jar 和設定檔至 dist.dir 目錄 --> <target name="jar-deps" depends="jar"> <copy todir="${dist.dir}/libs"> <fileset dir="${lib.dir}"> <include name="**/*.jar" /> </fileset> </copy> </target> <!-- 將最終的檔案進行 zip 打包,並含入特定環境的設定檔或程式啟動 script --> <target name="dist" depends="jar-deps"> <zip destfile="${ant.project.name}-all.zip"> <zipfileset dir="${dist.dir}"> <exclude name="all" /> </zipfileset> <zipfileset dir="${resource.dir}" /> </zip> </target> </project> 為了讓它看起來完整些,我加了一個 resources 目錄,裡面放了最終打包需要的設定檔與啟動 script:
它的提供了正式部署時需的設定檔。以 log4j.properties 為例,正式環境的 log level 的需求與開發可能不同,這個版本將整體的 level 調成 warning 才顯示,但針對 HelloWorld 是 debug 的: log4j.rootLogger=warn, stdout, R log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout # Pattern to output the caller's file name and line number. log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n log4j.appender.R=org.apache.log4j.RollingFileAppender log4j.appender.R.File=codedata.log log4j.appender.R.Append=true log4j.appender.R.layout=org.apache.log4j.PatternLayout log4j.appender.R.layout.ConversionPattern=%p %t [%d{yy/MM/dd HH:mm:ss:SSS}] %c - %m%n log4j.logger.HelloWorld=debug 除了一般設定檔之外,通常還會有啟動的 script,例如 run.bat: cd /d %~dp0 java -cp .;libs/* HelloWorld Ant 標準化經驗的影響上述的安排,其實試圖對自動化編譯的工作進行標準化的過程。在同一個團隊的人就會習慣有 jar target 與 dist target 可以使用。同樣的慣例,在 Java Web Application 專案可能是 war target。 除了 target 之外,漸漸會開始規範目錄的名稱,例如 src 目錄放專案原始碼位置,test 放測試碼的位置,libs 就是放第三方函式庫的位置,libs.test 放測試用函式庫的位置。編譯的順序也會成為範圍,先編譯 src 目錄再編譯 test 目錄,一旦有人在 src 內呼叫了 test 目錄的 library 就會讓編譯動作失敗。同樣的規範在 classpath 的設定也是如此,編譯 src 目錄時不會包含 libs.test 內的函式庫,所以透過編譯順序的約規可以防制誤用不該用的類別,以防在正式部署時出現相依性錯誤的問題。 基於這些開發上實戰經驗與知識的累積:針對特定類型專案,需要有共通的編譯行為與功能,對於後來 Maven 的產生是有相當影響的。在 What is Maven 也提到 Maven 這個字代表著『知識的累積』,標準化的建構流程即為這些的集合,同時帶入了相依管理的概念。 相依管理的需求來自於將大量的 JARs 放入版本控制系統後產生的挫折感,不同專案會有許多重複的、相同的 JARs,但都送進了版本控制系統,讓版本控制系統越來越顯得笨重,而跨區域開發者更苦了遠端的開發者。他們需要花更多的時間在下載 JARs。有了相依管理的工具,只需要描述相依哪些函式庫,不用真的將它連同原始碼放入版本控制系統。 標準化過的 Maven 優點是一致性與極便利的相依性管理,不便的是擴充起來不像 Ant 這般自由。以 Ant 為編譯工具為主的社群,意識到新工具帶來的 trade off,才會有後繼的 Ant Ivy,補足 Ant 相依性管理的弱點。Ant Ivy 也成為除了 Maven 之外,最常被使用的 Java 編譯工具內的相依性管理機制,例如 sbt, build tool for Scala 與 Gradle 內都是使用 Ant Ivy。 |