【認識 Gradle】(2)講古的時間 Apache Maven
- gradle-series
Apache Maven 是被廣泛使用的 Java 自動化編譯工具,它的相依性管理功能帶來極大的便利。這也是多數 Java 開發者對它最有印象的部分。繼上一篇談到的 Maven 被創造出來的動機有二個主要原因:
- Java 專案的標準化
- Java 專案函式庫管理的問題
直接看看具體的例子,範例如同前一篇的 Hello World,但編譯工具轉換成 Maven。它的做法與先有原始碼再撰寫 Ant Build File 描述編譯方式的順序不太同。Maven 專案有提供專案『樣版』的機制,它會建立好專案目錄結構、範例檔與 Maven 設定檔 pom.xml
。讓我們試試 Maven 風的專案建立機制吧!
使用 Maven Archetype plugin 建立新專案:
mvn archetype:generate \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DinteractiveMode=false \
-DgroupId=tw.com.codedata -DartifactId=helloworld
執行完畢能在目前的路徑下發現與 artifactId 相同的目錄,它的結構如下:
qty:gradle-maven qrtt1$ tree helloworld/
helloworld/
├── pom.xml
└── src
├── main
│ └── java
│ └── tw
│ └── com
│ └── codedata
│ └── App.java
└── test
└── java
└── tw
└── com
└── codedata
└── AppTest.java
11 directories, 3 files
若再觀察一下,在平行的目錄結構 src/main/java
與 src/test/java
下的 package 即為 groupdId。這是利用 maven-archetype-quickstart 樣版建立出來的結果,大多數的專案樣版也是這樣的結構。針對 Java 專案來說,它有 4 個常用的目錄:
- src/main/java 放置專案原始碼
- src/test/java 放置單元測試用原始碼
- src/main/resources 放置設定檔,例如 log4j.properties
- src/test/resources 放置測試用設定檔,如同測試程式本身不會被打包進 jar
上述的 4 個『慣例』路徑並不是 ArchType plugin 決定的,它只是一個程式碼產生器,透過事先撰寫的 Prototype 與簡單的變數代換建立出來使用者期待的專案骨構。實際決定這些內容的是 pom.xml
,它就是 Maven 專案主要的設定檔,決定了該專案的設定、編譯行為。以下為 helloworld 專案的 pom.xml
內容:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>tw.com.codedata</groupId>
<artifactId>helloworld</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>helloworld</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
剛才說明 pom.xml
替我們決定了許多事情,現在看到實際的檔案又非常的精簡。這是為什麼呢?這得提一下 Maven 的設計哲學 Convention Over Configuration。Maven 在實作各種設定檔原型或 plugin 時已經決定許多『慣例』並將它們作為『預設值』,就像你使用應用軟體一般,它有它預設的行為。你並不需要第一次使用時,回答一連串的問題來決定各種細節的設定。
Maven 的 pom.xml
之所以那麼簡短是因為在 Maven 核心函式內的 super-pom.xml
內已經事先填好了預設資訊。開發者在自己專案內的 pom.xml
設定可以覆寫舊有設定或附加新的設定或行為在原先 super-pom.xml
內沒有定義的事項。
Maven 有提供指令查詢現在 pom.xml
的有效設定:
mvn help:effective-pom
以下節錄 build 部分的相關段落,可以看先前談到的 4 個目錄:
<build>
<sourceDirectory>/Users/qrtt1/Downloads/gradle-maven/helloworld/src/main/java</sourceDirectory>
<scriptSourceDirectory>/Users/qrtt1/Downloads/gradle-maven/helloworld/src/main/scripts</scriptSourceDirectory>
<testSourceDirectory>/Users/qrtt1/Downloads/gradle-maven/helloworld/src/test/java</testSourceDirectory>
<outputDirectory>/Users/qrtt1/Downloads/gradle-maven/helloworld/target/classes</outputDirectory>
<testOutputDirectory>/Users/qrtt1/Downloads/gradle-maven/helloworld/target/test-classes</testOutputDirectory>
<resources>
<resource>
<directory>/Users/qrtt1/Downloads/gradle-maven/helloworld/src/main/resources</directory>
</resource>
</resources>
<testResources>
<testResource>
<directory>/Users/qrtt1/Downloads/gradle-maven/helloworld/src/test/resources</directory>
</testResource>
</testResources>
<directory>/Users/qrtt1/Downloads/gradle-maven/helloworld/target</directory>
<finalName>helloworld-1.0-SNAPSHOT</finalName>
...(以下省略)...
相依性管理
使用 code generator 產生完專案後,繼續回到 Hello World 範例。先將程式 HelloWorld.java 與 log4j.properties 放到適當的位置:
qty:helloworld qrtt1$ tree
.
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── tw
│ │ └── com
│ │ └── codedata
│ │ └── HelloWorld.java
│ └── resources
│ └── log4j.properties
└── test
└── java
└── tw
└── com
└── codedata
12 directories, 3 files
將多餘的範例程式刪除,並替 HelloWorld.java 加上 package,並在 resources 目錄下放上 log4j.properties:
package tw.com.codedata;
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");
}
}
在滿足相依性前,我們先編譯試試(順便熟悉一下錯誤訊息),其實跟手工呼叫 Java Compiler 時看到的訊息是一樣的,只是它透過 Maven 的 logger 顯示出來。明顯地,它缺少了相依的函式庫:
qty:helloworld qrtt1$ mvn package
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building helloworld 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.5:resources (default-resources) @ helloworld ---
[debug] execute contextualize
[WARNING] Using platform encoding (Big5 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 1 resource
[INFO]
[INFO] --- maven-compiler-plugin:2.3.2:compile (default-compile) @ helloworld ---
[WARNING] File encoding has not been set, using platform encoding Big5, i.e. build is platform dependent!
[INFO] Compiling 1 source file to /Users/qrtt1/Downloads/gradle-maven/helloworld/target/classes
[INFO] -------------------------------------------------------------
[ERROR] COMPILATION ERROR :
[INFO] -------------------------------------------------------------
[ERROR] /Users/qrtt1/Downloads/gradle-maven/helloworld/src/main/java/tw/com/codedata/HelloWorld.java:[3,33] package org.apache.commons.logging does not exist
[ERROR] /Users/qrtt1/Downloads/gradle-maven/helloworld/src/main/java/tw/com/codedata/HelloWorld.java:[4,33] package org.apache.commons.logging does not exist
[ERROR] /Users/qrtt1/Downloads/gradle-maven/helloworld/src/main/java/tw/com/codedata/HelloWorld.java:[8,11] cannot find symbol
symbol : class Log
location: class tw.com.codedata.HelloWorld
[ERROR] /Users/qrtt1/Downloads/gradle-maven/helloworld/src/main/java/tw/com/codedata/HelloWorld.java:[8,24] cannot find symbol
symbol : variable LogFactory
location: class tw.com.codedata.HelloWorld
[INFO] 4 errors
[INFO] -------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.595s
[INFO] Finished at: Sun Oct 13 01:30:56 CST 2013
[INFO] Final Memory: 8M/81M
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:2.3.2:compile (default-compile) on project helloworld: Compilation failure: Compilation failure:
[ERROR] /Users/qrtt1/Downloads/gradle-maven/helloworld/src/main/java/tw/com/codedata/HelloWorld.java:[3,33] package org.apache.commons.logging does not exist
[ERROR] /Users/qrtt1/Downloads/gradle-maven/helloworld/src/main/java/tw/com/codedata/HelloWorld.java:[4,33] package org.apache.commons.logging does not exist
[ERROR] /Users/qrtt1/Downloads/gradle-maven/helloworld/src/main/java/tw/com/codedata/HelloWorld.java:[8,11] cannot find symbol
[ERROR] symbol : class Log
[ERROR] location: class tw.com.codedata.HelloWorld
[ERROR] /Users/qrtt1/Downloads/gradle-maven/helloworld/src/main/java/tw/com/codedata/HelloWorld.java:[8,24] cannot find symbol
[ERROR] symbol : variable LogFactory
[ERROR] location: class tw.com.codedata.HelloWorld
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
修改 pom.xml
加上二組 dependency 標籤,再次執行 mvn package 就能成功編譯:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>tw.com.codedata</groupId>
<artifactId>helloworld</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>helloworld</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.16</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
您可以使用 mvn exec:java
指定要執行的類別來執行 HelloWorld 範例:
qty:helloworld qrtt1$ mvn exec:java -Dexec.mainClass="tw.com.codedata.HelloWorld"
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building helloworld 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] >>> exec-maven-plugin:1.2.1:java (default-cli) @ helloworld >>>
[INFO]
[INFO] <<< exec-maven-plugin:1.2.1:java (default-cli) @ helloworld <<<
[INFO]
[INFO] --- exec-maven-plugin:1.2.1:java (default-cli) @ helloworld ---
INFO [tw.com.codedata.HelloWorld.main()] (HelloWorld.java:11) - Hello World
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.312s
[INFO] Finished at: Sun Oct 13 01:47:04 CST 2013
[INFO] Final Memory: 5M/81M
[INFO] ------------------------------------------------------------------------
透過『宣告』相依性的方法在替專案增加相依函式庫,將專案原始碼與函式庫分開處理,方便了版本控制系統的使用。編譯成果會被放在 target 目錄下,除了 target 目錄的內容,其他都是需要進版本控制系統的『資料』。而函式庫會由 Maven 透過 repostiory server 下載,並 cache 在使用者目錄下的 .m2 目錄:
qty:helloworld qrtt1$ tree
.
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── tw
│ │ │ └── com
│ │ │ └── codedata
│ │ │ └── HelloWorld.java
│ │ └── resources
│ │ └── log4j.properties
│ └── test
│ └── java
│ └── tw
│ └── com
│ └── codedata
└── target
├── classes
│ ├── log4j.properties
│ └── tw
│ └── com
│ └── codedata
│ └── HelloWorld.class
├── helloworld-1.0-SNAPSHOT.jar
├── maven-archiver
│ └── pom.properties
└── surefire
關於相依性管理還有許多細節是需要知曉的,但此文主要是針對 Maven 的概念與優點進行介紹,有興趣的讀者可以再對相關項目進行研究:
- 更細節地控制相依關係。例如:排除特定的遞移相依關依(禁止引用相依套件的相依套件)
- 查找需要的 Library
- 設定第 3 方 repository server
- 自建 repository server
- 上傳非 Maven Project 的 JARs 至自建的 repository server
談談 Maven 指令
在介紹 Maven 相依性管理過程,我們介紹了許多指令。這其實是一種模稜兩可的說法,先來看一下 mvn 的說明:
qty:~ qrtt1$ mvn -h
usage: mvn [options] [<goal(s)>] [<phase(s)>]
Options:
-am,--also-make If project list is specified, also
build projects required by the
list
-amd,--also-make-dependents If project list is specified, also
build projects that depend on
projects on the list
-B,--batch-mode Run in non-interactive (batch)
mode
-C,--strict-checksums Fail the build if checksums don't
match
-c,--lax-checksums Warn if checksums don't match
-cpu,--check-plugin-updates Ineffective, only kept for
backward compatibility
-D,--define <arg> Define a system property
...(以下省略)...
由說明可看到,除了參數之外,它可以指定 goal 與 phase。它們又分別代表什麼呢? 我先將之前執行過的指令區分一下,這些指令是 goal:
mvn archetype:generate
mvn help:effective-pom
mvn exec:java
而這個指令是 phase:
mvn package
曖昧的說法,都能稱為執行 Maven 指令。Maven 本身是由許多的 plugin 組成的,而 plugin 內會提供一個或多個功能,這些功能稱為 goal,對應至 Ant 則為 task。要執行 goal 需指出它是哪一個 plugin 並呼叫 goal 的名稱。你可以簡單地判斷,有『:』冒號出現的就是 goal,因為在冒號的前半部為 plugin 名稱,後半部為 goal。Maven plugin 預先建立了許多的 goal,如同 Ant 提供了相當數量的 task 讓開發者使用,單純提供『功能』讓開發者自由組裝不是 Maven 的主要目標,別忘了專案標準化目的。
Maven 以 Build Lifecycle 的概念來達到標準化的目的,先重貼一下在 Ant 介紹時,我們定下的專案編譯需求:
- 準備編譯環境
- 編譯原始碼
- 將專案打包成 jar 檔
- 將專案打包成 jar 檔與相依的 library
- 將專案打包成 jar 檔含相依的 library 與部署需要的相關檔案
- 重置編譯結果
就當它是某個團隊內的慣例,針對一個 Java 專案建立了這些明確的編譯階段(phase),這其實就是 Build Lifecycle。不同性質的專案有不同的編譯階段需要減增,或是同一個編譯階段但有著不同的工作內容。例如:獨立執行的 Java 專案與 Java Web 應用程式專案的『將專案打包成 jar 檔含相依的 library 與部署需要的相關檔案』會有很大的差別。Java Web 應用程式可能需要多處理靜態的網頁檔案。像是決定哪些檔案該複製到某些目錄,哪些是單機開發時需要,但部署時不需要包含的檔案。Maven 定義 Build Lifecycle 沒有針對特定專案,而是考慮各種可能將所有有機會用到的編譯階段給予命名,開發者可以在官網找到完整的 Lifecycle Reference 清單。
當開發者執行:
mvn package
會發生什麼事呢?首先 Maven 取得有效的 pom.xml 設定後,會先看 packaging 的值,它決定了這個 Maven 專案的 Build Lifecycle 有哪些工作要做:
<groupId>tw.com.codedata</groupId>
<artifactId>helloworld</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
我們借用 Maven by Example 內 Core Concepts 的圖解來說明:
當我們指定了 packaging type 後,它的功用就是替專案在各編譯階段綁定(binding)需要的 goal,例如在 package 編譯階段它綁定 jar plugin 的 jar goal。不過當下指令要求 Maven 執行 package 編譯階段的真正意涵的是『由第 1 個編譯階段,執行到 package 編譯階段結束』。以上圖為例,它會依執執行下列 goal:
- resources:resources
- compiler:compile
- resources:testResources
- compiler:testCompile
- surefire:test
- jar:jar
對於熟悉 Template Method 模式 的讀者,可能覺得這樣的設計很親切。它確實就像哪些預留空白實作的 Method,讓後既者決定是否要在那些空白的地方『掛』上實作。說到這,讀者應該能清楚 Build Lifecycle、goal、phase 三者的關係。這也是 Maven 對專案標準化的努力,而單純留下『他是個方便管理相依性的編譯工具』的印象在開發者腦海。
Maven 學習與使用經驗
這篇文章是作為 Gradle 學習系列的先備知識之一撰寫,焦點在 Maven 核心的概念,而非實戰時會用到的技巧。若因為這篇文章,讓讀者拋開了對 Maven 官方文件的焦慮感,想要進一步學習,相當推薦 Sonatype 出品的電子書 Maven by Example。以我自己來說,多數的概念理解是來自於這本書平易近人的解說。Sonatype 同時也發展了 Maven Repository Server,有商用版與社群版能選用。若沒有用到進階的功能(例如:LDAP)用社群版本應足以支持架設自有的 Maven Repository Server 的需求。
Maven 提供替專案提供標準化的工具與便利的相依性管理機制,與 Ant 提供建立編譯需求的各種 task 由開發者自行決定該怎麼建構專案,提供開發者哪些功能,並能配合 Ant Ivy 加上相依性管理的機制。這是不同的專案建置風格,二方各能做多接近彼此效果的成果,但千萬別想著要用 Ant 來做 Maven 比較容易達到的工作,或用 Maven 來做 Ant 比較容易達到的工具。Ant 具有客製化的彈性,Maven 要客製化的方便性,無法像 Ant 這般自由。
以 Maven 的使用來說,身邊開發者的抱怨主要有:
- 使者用對相依性管理的濫用:由於加相依性太方便了,隨著專案時間越長有越多的相依性套件加入。乏缺一個統一的人進行審閱,導致相依性混亂或不合理的巨大,例如:朋友常說『你看過像國泰樹一樣巨大的相依性嗎?』。這基本上是總壞習慣,若不改善,換成 Gradle 也不會有救。不過要改起來不難,Maven 有 dependency plugin 讓你列出相依關係,輔助你理解套件的相依性,剩下的就看專案的使用情況調整。
- 官方文件難以理解:對於核心概念理解不足的使用者,各個 plugin 文件像天書一般難懂。有時查了半天不知道自己該下什麼參數,或是不知道功能叫什麼名字。有些得靠書上的教學來學習,或團隊中傳承來的經驗補足。
- 學習修改預設行為的上手難度與自由度:這問題主因也是官網的組織方式不是那麼易懂。客製化 Maven 不外乎是為了『開發環境設定慣例』、『條件式編譯』、『個人開發習慣』這些需求。例如:Java Compiler 設定,期望它強制為 UTF-8 而無跟隨著系統設定走。你可以在 pom.xml 內覆寫 Java Compiler Plugin 的 configuration。有些效能能用 Maven Profile 機制達到,例如:對不同的環境使用不同的設定檔。有些事情無法做的,也得在讀了手冊才明白,如 Using Maven When You Can’t Use the Conventions
隨著時間沉澱雙方的走向有試著將對方的優點,在自身上補足,並漸漸消去自身的弱點。即使有所不濟,除了開發自己的 Maven Plugin,也能由 Maven Plugin 呼叫 Ant 或由 Ant Task 呼叫 Maven 來完成特殊的功能。
後續的文章將進入正題,談 Gradle 的使用。它有著 Lifecycle 的精神、相依性管理機制的便利與 DSL 的可讀性與容易撰寫。讓專案編譯工具的學習門檻較 Maven 低一些,又能保持 Ant 使用上的自由,但透過引用 plugin 來獲得需要專案編譯功能。這些特性,讓許多以 Java 為開發語言的開放原始碼專案,漸漸由 Maven 改用 Gradle。