【認識 Gradle】(2)講古的時間 Apache Maven
【認識 Gradle】(1)講古的時間 Apache Ant << 前情 Apache Maven 是被廣泛使用的 Java 自動化編譯工具,它的相依性管理功能帶來極大的便利。這也是多數 Java 開發者對它最有印象的部分。繼上一篇談到的 Maven 被創造出來的動機有二個主要原因:
直接看看具體的例子,範例如同前一篇的 Hello World,但編譯工具轉換成 Maven。它的做法與先有原始碼再撰寫 Ant Build File 描述編譯方式的順序不太同。Maven 專案有提供專案『樣版』的機制,它會建立好專案目錄結構、範例檔與 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 若再觀察一下,在平行的目錄結構
上述的 4 個『慣例』路徑並不是 ArchType plugin 決定的,它只是一個程式碼產生器,透過事先撰寫的 Prototype 與簡單的變數代換建立出來使用者期待的專案骨構。實際決定這些內容的是 <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 核心函式內的 Maven 有提供指令查詢現在 pom.xml 的有效設定: mvn help:effective-pom 以下節錄 <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 標籤,再次執行 <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> 您可以使用 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 在使用者目錄下的 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 的概念與優點進行介紹,有興趣的讀者可以再對相關項目進行研究:
談談 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。它們又分別代表什麼呢? 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 以
就當它是某個團隊內的慣例,針對一個 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,例如在
對於熟悉 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 的使用來說,身邊開發者的抱怨主要有:
隨著時間沉澱雙方的走向有試著將對方的優點,在自身上補足,並漸漸消去自身的弱點。即使有所不濟,除了開發自己的 Maven Plugin,也能由 Maven Plugin 呼叫 Ant 或由 Ant Task 呼叫 Maven 來完成特殊的功能。 後續的文章將進入正題,談 Gradle 的使用。它有著 Lifecycle 的精神、相依性管理機制的便利與 DSL 的可讀性與容易撰寫。讓專案編譯工具的學習門檻較 Maven 低一些,又能保持 Ant 使用上的自由,但透過引用 plugin 來獲得需要專案編譯功能。這些特性,讓許多以 Java 為開發語言的開放原始碼專案,漸漸由 Maven 改用 Gradle。 |