Maven和Gradle對比

黃博文發表於2016-02-23

Java世界中主要有三大構建工具:Ant、Maven和Gradle。經過幾年的發展,Ant幾乎銷聲匿跡、Maven也日薄西山,而Gradle的發展則如日中天。筆者有幸見證了Maven的沒落和Gradle的興起。Maven的主要功能主要分為5點,分別是依賴管理系統、多模組構建、一致的專案結構、一致的構建模型和外掛機制。我們可以從這五個方面來分析一下Gradle比起Maven的先進之處。

依賴管理系統

Maven為Java世界引入了一個新的依賴管理系統。在Java世界中,可以用groupId、artifactId、version組成的Coordination(座標)唯一標識一個依賴。任何基於Maven構建的專案自身也必須定義這三項屬性,生成的包可以是Jar包,也可以是war包或者ear包。一個典型的依賴引用如下所示:

1
2
3
4
5
6
7
8
9
10
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
</dependency>

從上面可以看出當引用一個依賴時,version可以省略掉,這樣在獲取依賴時會選擇最新的版本。而儲存這些元件的倉庫有遠端倉庫和本地倉庫之分。遠端倉庫可以使用世界公用的central倉庫,也可以使用Apache Nexus自建私有倉庫;本地倉庫則在本地計算機上。通過Maven安裝目錄下的settings.xml檔案可以配置本地倉庫的路徑,以及採用的遠端倉庫的地址。

Gradle在設計的時候基本沿用了Maven的這套依賴管理體系。不過它在引用依賴時還是進行了一些改進。首先引用依賴方面變得非常簡潔。

1
2
3
4
dependencies {
    compile 'org.hibernate:hibernate-core:3.6.7.Final'
    testCompile junit:junit:4.+'
}

第二,Maven和Gradle對依賴項的scope有所不同。在Maven世界中,一個依賴項有6種scope,分別是complie(預設)、provided、runtime、test、system、import。而grade將其簡化為了4種,compile、runtime、testCompile、testRuntime。那麼如果想在gradle使用類似於provided的scope怎麼辦?彆著急,由於gradle語言的強大表現力,我們可以輕鬆編寫程式碼來實現類似於provided scope的概念(例如How to use provided scope for jar file in Gradle build?)。

第三點是Gradle支援動態的版本依賴。在版本號後面使用+號的方式可以實現動態的版本管理。

第四點是在解決依賴衝突方面Gradle的實現機制更加明確。使用Maven和Gradle進行依賴管理時都採用的是傳遞性依賴;而如果多個依賴項指向同一個依賴項的不同版本時就會引起依賴衝突。而Maven處理這種依賴關係往往是噩夢一般的存在。而Gradle在解決依賴衝突方面相對來說比較明確。在Chapter 23. Dependency Management 中的23.2.3章節詳細解讀了gradle是如何處理版本衝突的。

多模組構建

在SOA和微服務的浪潮下,將一個專案分解為多個模組已經是很通用的一種方式。在Maven中需要定義個parent POM作為一組module的聚合POM。在該POM中可以使用<modules>標籤來定義一組子模組。parent POM不會有什麼實際構建產出。而parent POM中的build配置以及依賴配置都會自動繼承給子module。

而Gradle也支援多模組構建。而在parent的build.gradle中可以使用allprojects和subprojects程式碼塊來分別定義裡面的配置是應用於所有專案還是子專案。對於子模組的定義是放置在setttings.gradle檔案中的。在gradle的設計當中,每個模組都是Project的物件例項。而在parent build.gradle中通過allprojects或subprojects可以對這些物件進行各種操作。這無疑比Maven要靈活的多。

比如在parent的build.gradle中有以下程式碼:

1
2
3
allprojects {
    task hello << { task -> println "I'm $task.project.name" }
}

執行命令gradle -q hello會依次列印出父module以及各個submodule的專案名稱。這種強大的能力能讓gradle對各個模組具有更強的定製化。

一致的專案結構

在Ant時代大家建立Java專案目錄時比較隨意,然後通過Ant配置指定哪些屬於source,那些屬於testSource等。而Maven在設計之初的理念就是Conversion over configuration(約定大於配置)。其制定了一套專案目錄結構作為標準的Java專案結構。一個典型的Maven專案結構如下:

Maven和Gradle對比

Gradle也沿用了這一標準的目錄結構。如果你在Gradle專案中使用了標準的Maven專案結構的話,那麼在Gradle中也無需進行多餘的配置,只需在檔案中包含apply plugin:'java',系統會自動識別source、resource、test srouce、 test resource等相應資源。不過Gradle作為JVM上的構建工具,也同時支援groovy、scala等原始碼的構建,甚至支援Java、groovy、scala語言的混合構建。雖然Maven通過一些外掛(比如maven-scala-plugin)也能達到相同目的,但配置方面顯然Gradle要更優雅一些。

一致的構建模型

為了解決Ant中對專案構建活動缺乏標準化的問題,Maven特意設定了標準的專案構建週期,其預設的構建週期如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<phases>
  <phase>validate</phase>
  <phase>initialize</phase>
  <phase>generate-sources</phase>
  <phase>process-sources</phase>
  <phase>generate-resources</phase>
  <phase>process-resources</phase>
  <phase>compile</phase>
  <phase>process-classes</phase>
  <phase>generate-test-sources</phase>
  <phase>process-test-sources</phase>
  <phase>generate-test-resources</phase>
  <phase>process-test-resources</phase>
  <phase>test-compile</phase>
  <phase>process-test-classes</phase>
  <phase>test</phase>
  <phase>prepare-package</phase>
  <phase>package</phase>
  <phase>pre-integration-test</phase>
  <phase>integration-test</phase>
  <phase>post-integration-test</phase>
  <phase>verify</phase>
  <phase>install</phase>
  <phase>deploy</phase>
</phases>

而這種構建週期也是Maven最為人詬病的地方。因為Maven將專案的構建週期限制的太死,你無法在構建週期中新增新的phase,只能將外掛繫結到已有的phase上。而現在專案的構建過程變得越來越複雜,而且多樣化,顯然Maven對這種複雜度缺少足夠的應變能力。比如你想在專案構建過程中進行一項壓縮所有javascript的任務,那麼就要繫結到Maven的現有的某個phase上,而顯然貌似放在哪個phase都不太合適。而且這些phase都是序列的,整個執行下來是一條線,這也限制了Maven的構建效率。而Gradle在構建模型上則非常靈活。在Gradle世界裡可以輕鬆建立一個task,並隨時通過depends語法建立與已有task的依賴關係。甚至對於Java專案的構建來說,Gradle是通過名為java的外掛來包含了一個對Java專案的構建週期,這等於Gradle本身直接與專案構建週期是解耦的。

外掛機制

Maven和Gradle設計時都採用了外掛機制。但顯然Gradle更勝一籌。主要原因在於Maven是基於XML進行配置。所以其配置語法太受限於XML。即使實現很小的功能都需要設計一個外掛,建立其與XML配置的關聯。比如想在Maven中執行一條shell命令,其配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>exec-maven-plugin</artifactId>
  <version>1.2</version>
  <executions>
    <execution>
      <id>drop DB => db_name</id>
      <phase>pre-integration-test</phase>
      <goals>
        <goal>exec</goal>
      </goals>
      <configuration>
        <executable>curl</executable>
        <arguments>
          <argument>-s</argument>
          <argument>-S</argument>
          <argument>-X</argument>
          <argument>DELETE</argument>
          <argument>http://${db.server}:${db.port}/db_name</argument>
        </arguments>
      </configuration>
    </execution>
  </executions>
</plugin>

而在Gradle中則一切變得非常簡單。

1
2
3
task dropDB(type: Exec) {
 commandLine curl,-s,s,-x,DELETE,"http://${db.server}:{db.port}/db_name"
}

在建立自定義外掛方面,Maven和Gradle的機制都差不多,都是繼承自外掛基類,然後實現要求的方法。這裡就不展開說明。


從以上五個方面可以看出Maven和Gradle的主要差異。Maven的設計核心Convention Over Configuration被Gradle更加發揚光大,而Gradle的配置即程式碼又超越了Maven。在Gradle中任何配置都可以作為程式碼被執行的,我們也可以隨時使用已有的Ant指令碼(Ant task是Gradle中的一等公民)、Java類庫、Groovy類庫來輔助完成構建任務的編寫。

這種採用本身語言實現的DSL對本身語言專案進行構建管理的例子比比皆是。比如Rake和Ruby、Grunt和JavaScript、Sbt和Ruby…..而Gradle之所以使用Groovy語言實現,是因為Groovy比Java語言更具表現力,其語法特性更豐富,又兼具函式式的特點。這幾年興起的語言(比如Scala、Go、Swift)都屬於強型別的語言,兼具物件導向和函式式的特點。

最後想說的Gradle的命令列比Maven的要強大的多。以前寫過一篇文章專門講述了Gradle的命令列操作,詳情請見Gradle命令列黑魔法

相關文章