簡介
gradle和maven都可以用來建構java程式,甚至在某些情況下,兩者還可以互相轉換,那麼他們兩個的共同點和不同點是什麼?我們如何在項目中選擇使用哪種技術呢?一起來看看吧。
gradle和maven的比較
雖然gradle和maven都可以作為java程式的建構工具。但是兩者還是有很大的不同之處的。我們可以從下面幾個方面來進行分析。
可擴充性
Google選擇gradle作為android的建構工具不是沒有理由的,其中一個非常重要的原因就是因為gradle夠靈活。一方面是因為gradle使用的是groovy或者kotlin語言作為腳本的編寫語言,這樣極大的提高了腳本的靈活性,但是其本質上的原因是gradle的基礎架構能夠支援這種靈活性。
你可以使用gradle來建構native的C/C++程式,甚至擴充到任何語言的建構。
相對而言,maven的靈活性就差一些,并且自定義起來也比較麻煩,但是maven的項目比較容易看懂,并且上手簡單。
是以如果你的項目沒有太多自定義建構需求的話還是推薦使用maven,但是如果有自定義的建構需求,那麼還是投入gradle的懷抱吧。
性能比較
雖然現在大家的機子性能都比較強勁,好像在做項目建構的時候性能的優勢并不是那麼的迫切,但是對于大型項目來說,一次建構可能會需要很長的時間,尤其對于自動化建構和CI的環境來說,當然希望這個建構是越快越好。
Gradle和Maven都支援并行的項目建構和依賴解析。但是gradle的三個特點讓gradle可以跑的比maven快上一點:
- 增量建構
gradle為了提升建構的效率,提出了增量建構的概念,為了實作增量建構,gradle将每一個task都分成了三部分,分别是input輸入,任務本身和output輸出。下圖是一個典型的java編譯的task。

以上圖為例,input就是目标jdk的版本,源代碼等,output就是編譯出來的class檔案。
增量建構的原理就是監控input的變化,隻有input發送變化了,才重新執行task任務,否則gradle認為可以重用之前的執行結果。
是以在編寫gradle的task的時候,需要指定task的輸入和輸出。
并且要注意隻有會對輸出結果産生變化的才能被稱為輸入,如果你定義了對初始結果完全無關的變量作為輸入,則這些變量的變化會導緻gradle重新執行task,導緻了不必要的性能的損耗。
還要注意不确定執行結果的任務,比如說同樣的輸入可能會得到不同的輸出結果,那麼這樣的任務将不能夠被配置為增量建構任務。
- 建構緩存
gradle可以重用同樣input的輸出作為緩存,大家可能會有疑問了,這個緩存和增量編譯不是一個意思嗎?
在同一個機子上是的,但是緩存可以跨機器共享.如果你是在一個CI服務的話,build cache将會非常有用。因為developer的build可以直接從CI伺服器上面拉取建構結果,非常的友善。
- Gradle守護程序
gradle會開啟一個守護程序來和各個build任務進行互動,優點就是不需要每次建構都初始化需要的元件和服務。
同時因為守護程序是一個一直運作的程序,除了可以避免每次JVM啟動的開銷之外,還可以緩存項目結構,檔案,task和其他的資訊,進而提升運作速度。
我們可以運作 gradle --status 來檢視正在運作的daemons程序。
從Gradle 3.0之後,daemons是預設開啟的,你可以使用 org.gradle.daemon=false 來禁止daemons。
我們可以通過下面的幾個圖來直覺的感受一下gradle和maven的性能比較:
- 使用gradle和maven建構 Apache Commons Lang 3的比較:
- 使用gradle和maven建構小項目(10個子產品,每個子產品50個源檔案和50個測試檔案)的比較:
- 使用gradle和maven建構大項目(500個子產品,每個子產品100個源檔案和100個測試檔案)的比較:
可以看到gradle性能的提升是非常明顯的。
依賴的差別
gralde和maven都可以本地緩存依賴檔案,并且都支援依賴檔案的并行下載下傳。
在maven中隻可以通過版本号來覆寫一個依賴項。而gradle更加靈活,你可以自定義依賴關系和替換規則,通過這些替換規則,gradle可以建構非常複雜的項目。
從maven遷移到gradle
因為maven出現的時間比較早,是以基本上所有的java項目都支援maven,但是并不是所有的項目都支援gradle。如果你有需要把maven項目遷移到gradle的想法,那麼就一起來看看吧。
根據我們之前的介紹,大家可以發現gradle和maven從本質上來說就是不同的,gradle通過task的DAG圖來組織任務,而maven則是通過attach到phases的goals來執行任務。
雖然兩者的建構有很大的不同,但是得益于gradle和maven相識的各種約定規則,從maven移植到gradle并不是那麼難。
要想從maven移植到gradle,首先要了解下maven的build生命周期,maven的生命周期包含了clean,compile,test,package,verify,install和deploy這幾個phase。
我們需要将maven的生命周期phase轉換為gradle的生命周期task。這裡需要使用到gradle的Base Plugin,Java Plugin和Maven Publish Plugin。
先看下怎麼引入這三個plugin:
plugins {
id 'base'
id 'java'
id 'maven-publish'
}
clean會被轉換成為clean task,compile會被轉換成為classes task,test會被轉換成為test task,package會被轉換成為assemble task,verify 會被轉換成為check task,install會被轉換成為 Maven Publish Plugin 中的publishToMavenLocal task,deploy 會被轉換成為Maven Publish Plugin 中的publish task。
有了這些task之間的對應關系,我們就可以嘗試進行maven到gradle的轉換了。
自動轉換
我們除了可以使用 gradle init 指令來建立一個gradle的架子之外,還可以使用這個指令來将maven項目轉換成為gradle項目,gradle init指令會去讀取pom檔案,并将其轉換成為gradle項目。
轉換依賴
gradle和maven的依賴都包含了group ID, artifact ID 和版本号。兩者本質上是一樣的,隻是形式不同,我們看一個轉換的例子:
<dependencies>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
</dependencies>
上是一個maven的例子,我們看下gradle的例子怎寫:
dependencies {
implementation 'log4j:log4j:1.2.12'
}
可以看到gradle比maven寫起來要簡單很多。
注意這裡的implementation實際上是由 Java Plugin 來實作的。
我們在maven的依賴中有時候還會用到scope選項,用來表示依賴的範圍,我們看下這些範圍該如何進行轉換:
- compile:
在gradle可以有兩種配置來替換compile,我們可以使用implementation或者api。
前者在任何使用Java Plugin的gradle中都可以使用,而api隻能在使用Java Library Plugin的項目中使用。
當然兩者是有差別的,如果你是建構應用程式或者webapp,那麼推薦使用implementation,如果你是在建構Java libraries,那麼推薦使用api。
- runtime:
可以替換成 runtimeOnly 。
- test:
gradle中的test分為兩種,一種是編譯test項目的時候需要,那麼可以使用testImplementation,一種是運作test項目的時候需要,那麼可以使用testRuntimeOnly。
- provided:
可以替換成為compileOnly。
- import:
在maven中,import經常用在dependencyManagement中,通常用來從一個pom檔案中導入依賴項,進而保證項目中依賴項目版本的一緻性。
在gradle中,可以使用 platform() 或者 enforcedPlatform() 來導入pom檔案:
dependencies {
implementation platform('org.springframework.boot:spring-boot-dependencies:1.5.8.RELEASE')
implementation 'com.google.code.gson:gson'
implementation 'dom4j:dom4j'
}
比如上面的例子中,我們導入了spring-boot-dependencies。因為這個pom中已經定義了依賴項的版本号,是以我們在後面引入gson的時候就不需要指定版本号了。
platform和enforcedPlatform的差別在于,enforcedPlatform會将導入的pom版本号覆寫其他導入的版本号:
dependencies {
// import a BOM. The versions used in this file will override any other version found in the graph
implementation enforcedPlatform('org.springframework.boot:spring-boot-dependencies:1.5.8.RELEASE')
// define dependencies without versions
implementation 'com.google.code.gson:gson'
implementation 'dom4j:dom4j'
// this version will be overridden by the one found in the BOM
implementation 'org.codehaus.groovy:groovy:1.8.6'
}
轉換repositories倉庫
gradle可以相容使用maven或者lvy的repository。gradle沒有預設的倉庫位址,是以你必須手動指定一個。
你可以在gradle使用maven的倉庫:
repositories {
mavenCentral()
}
我們還可以直接指定maven倉庫的位址:
repositories {
maven {
url "http://repo.mycompany.com/maven2"
}
}
如果你想使用maven本地的倉庫,則可以這樣使用:
repositories {
mavenLocal()
}
但是mavenLocal是不推薦使用的,為什麼呢?
mavenLocal隻是maven在本地的一個cache,它包含的内容并不完整。比如說一個本地的maven repository module可能隻包含了jar封包件,并沒有包含source或者javadoc檔案。那麼我們将不能夠在gradle中檢視這個module的源代碼,因為gradle會首先在maven本地的路徑中查找這個module。
并且本地的repository是不可信任的,因為裡面的内容可以輕易被修改,并沒有任何的驗證機制。
控制依賴的版本
如果同一個項目中對同一個子產品有不同版本的兩個依賴的話,預設情況下Gradle會在解析完DAG之後,選擇版本最高的那個依賴包。
但是這樣做并不一定就是正确的, 是以我們需要自定義依賴版本的功能。
首先就是上面我們提到的使用platform()和enforcedPlatform() 來導入BOM(packaging類型是POM的)檔案。
如果我們項目中依賴了某個module,而這個module又依賴了另外的module,我們叫做傳遞依賴。在這種情況下,如果我們希望控制傳遞依賴的版本,比如說将傳遞依賴的版本更新為一個新的版本,那麼可以使用dependency constraints:
dependencies {
implementation 'org.apache.httpcomponents:httpclient'
constraints {
implementation('org.apache.httpcomponents:httpclient:4.5.3') {
because 'previous versions have a bug impacting this application'
}
implementation('commons-codec:commons-codec:1.11') {
because 'version 1.9 pulled from httpclient has bugs affecting this application'
}
}
}
注意,dependency constraints隻對傳遞依賴有效,如果上面的例子中commons-codec并不是傳遞依賴,那麼将不會有任何影響。
同時 Dependency constraints需要Gradle Module Metadata的支援,也就是說隻有你的module是釋出在gradle中才支援這個特性,如果是釋出在maven或者ivy中是不支援的。
上面講的是傳遞依賴的版本更新。同樣是傳遞依賴,如果本項目也需要使用到這個傳遞依賴的module,但是需要使用到更低的版本(因為預設gradle會使用最新的版本),就需要用到版本降級了。
dependencies {
implementation 'org.apache.httpcomponents:httpclient:4.5.4'
implementation('commons-codec:commons-codec') {
version {
strictly '1.9'
}
}
}
我們可以在implementation中指定特定的version即可。
strictly表示的是強制比對特定的版本号,除了strictly之外,還有require,表示需要的版本号大于等于給定的版本号。prefer,如果沒有指定其他的版本号,那麼就使用prefer這個。reject,拒絕使用這個版本。
除此之外,你還可以使用Java Platform Plugin來指定特定的platform,進而限制版本号。
最後看一下如何exclude一個依賴:
dependencies {
implementation('commons-beanutils:commons-beanutils:1.9.4') {
exclude group: 'commons-collections', module: 'commons-collections'
}
}
多子產品項目
maven中可以建立多子產品項目:
<modules>
<module>simple-weather</module>
<module>simple-webapp</module>
</modules>
我們可以在gradle中做同樣的事情settings.gradle:
rootProject.name = 'simple-multi-module'
include 'simple-weather', 'simple-webapp'
profile和屬性
maven中可以使用profile來差別不同的環境,在gradle中,我們可以定義好不同的profile檔案,然後通過腳本來加載他們:
build.gradle:
if (!hasProperty('buildProfile')) ext.buildProfile = 'default'
apply from: "profile-${buildProfile}.gradle"
task greeting {
doLast {
println message
}
}
profile-default.gradle:
ext.message = 'foobar'
profile-test.gradle:
ext.message = 'testing 1 2 3'
我們可以這樣來運作:
> gradle greeting
foobar
> gradle -PbuildProfile=test greeting
testing 1 2 3
資源處理
在maven中有一個process-resources階段,可以執行resources:resources用來進行resource檔案的拷貝操作。
在Gradle中的Java plugin的processResources task也可以做相同的事情。
比如我可以執行copy任務:
task copyReport(type: Copy) {
from file("$buildDir/reports/my-report.pdf")
into file("$buildDir/toArchive")
}
更加複雜的拷貝:
task copyPdfReportsForArchiving(type: Copy) {
from "$buildDir/reports"
include "*.pdf"
into "$buildDir/toArchive"
}
當然拷貝還有更加複雜的應用。這裡就不詳細講解了。
本文已收錄于 http://www.flydean.com/gradle-vs-maven/最通俗的解讀,最深刻的幹貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!
歡迎關注我的公衆号:「程式那些事」,懂技術,更懂你!