【認識 Gradle】(1)講古的時間 Apache Ant
- gradle-series
在開發 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 組成的,它的根節點必需是 ,在它之內就能任意寫下各種的 ,以及其他代表 Task 或 DataType 的 XML 元素。Task 代表的通常是一個動作,例如 會呼叫 Java Compiler 進行編譯,DataType 是 Ant 提供你描述檔案與路徑的集合的工具,例如: 與 。Task、DataType、Property 通常是會用在一起的,因為編譯活動,就是對一些路徑內的檔案做一些處理。
在 Ant Build File 內,使用 來定義一些路徑與檔案的位置是相當常見的,範例中就定義了三個變數(在此篇文章 property 即為變數的意思),代表編譯時需要的不同路徑。它們都是替 Java Compiler 準備的,src.dir 是原始碼的位置,lib.dir 是函式庫的路徑,build.dir 是編譯產出的目錄:
<property name="src.dir" value="src" />
<property name="lib.dir" value="libs" />
<property name="build.dir" value="build" />
接著是一個命名為 build 的 target。在 Ant 內,target 代表一系列動作的描述,我們定義 build target 來描述 build 這個活動該做哪些事情。在這個範本,共有二個 task 被呼叫。mkdir task 用來建立 build.dir 變數所設定的目錄。javac task 用來編譯原始碼,它如何你自行呼叫 javac 外部指令一般皆需要提供它必要的參數。javac task 除了原始碼位置與輸出位置之外,要讓編譯順利完成,最重要的工作就是設定 classpath:
<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
type 是使用 Ant 編譯 Java 專案最常用到的 DataType。首次接觸的人,看到這可能會有個疑惑,為何我知道它是 DataType 而不是 Task?一個簡單的分法,用來描述檔案、路徑集合的都是 DataType,提供動作的視為 Task。另一個分辨的方法:DataType 常會配合 fileset 使用,只要看到配合相關動作的 XML 標籤,都能輔助你判別:
<!-- classpath 與 fileset DataType 方便開發者處理路徑與檔案列表的問題 -->
<classpath>
<fileset dir="${lib.dir}">
<include name="**/*.jar" />
</fileset>
</classpath>
Ant 提供開發者 DataType 的機制有個明顯的優點,那就是使用者不用再考慮路徑會因作業系統的不同而需要改變寫法。例如該使用 \ 還是 / 當作路徑分隔字元,該使用 ; 還是 : 串接多組路徑(試想一下,若你要呼叫 javac 外部指令,那個 classpath 依不同的系統該怎麼填)。而針對開發環境與執行環境不同作業系統的情況,那就是要改變編譯成品的內容。它就不是 DataType 能抽向化的部分了,開發者應該使用 property 指定不同情況的設定檔。
最後總結一下概念速寫:讀者現在已經知道 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,它們又要做到什麼功能?可以回想一下平時開發工作:
- 準備編譯環境
- 編譯原始碼
- 將專案打包成 jar 檔
- 將專案打包成 jar 檔與相依的 library
- 將專案打包成 jar 檔含相依的 library 與部署需要的相關檔案
- 重置編譯結果 PS. 為簡化說明,我們忽略單元測試的步驟。
規劃起來可分別對應成下列 target:
- prepare (建立目錄與必要檔案的複製)
- build
- jar (相依 build)
- jar-deps (相依 jar,但多了複製 library 的動作)
- dist (相依 jar-deps,但多了複製設定檔與啟動 script 的動作)
- clean
將上面的需求寫成 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:
qty:GradleHowToAnt qrtt1$ tree resources/
resources/
├── log4j.properties
├── run.bat
└── run.sh
它的提供了正式部署時需的設定檔。以 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。