1. 程式人生 > >深入瞭解gradle和maven的區別

深入瞭解gradle和maven的區別

[toc] 深入瞭解gradle和maven的區別 # 簡介 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。 ![](https://img-blog.csdnimg.cn/20201028104251422.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_25,color_8F8F8F,t_70) 以上圖為例,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的比較: ![](https://img-blog.csdnimg.cn/20201028205153551.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_25,color_8F8F8F,t_70) * 使用gradle和maven構建小專案(10個模組,每個模組50個原始檔和50個測試檔案)的比較: ![](https://img-blog.csdnimg.cn/2020102820530754.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_25,color_8F8F8F,t_70) * 使用gradle和maven構建大專案(500個模組,每個模組100個原始檔和100個測試檔案)的比較: ![](https://img-blog.csdnimg.cn/20201028205402145.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_25,color_8F8F8F,t_70) 可以看到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: ~~~java 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 和版本號。兩者本質上是一樣的,只是形式不同,我們看一個轉換的例子: ~~~xml log4j log4j 1.2.12 ~~~ 上是一個maven的例子,我們看下gradle的例子怎寫: ~~~java 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檔案: ~~~java 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版本號覆蓋其他匯入的版本號: ~~~java 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的倉庫: ~~~java repositories { mavenCentral() } ~~~ 我們還可以直接指定maven倉庫的地址: ~~~java repositories { maven { url "http://repo.mycompany.com/maven2" } } ~~~ 如果你想使用maven本地的倉庫,則可以這樣使用: ~~~java 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: ~~~java 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會使用最新的版本),就需要用到版本降級了。 ~~~java 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一個依賴: ~~~java dependencies { implementation('commons-beanutils:commons-beanutils:1.9.4') { exclude group: 'commons-collections', module: 'commons-collections' } } ~~~ ## 多模組專案 maven中可以建立多模組專案: ~~~xml simple-weather
simple-webapp
~~~ 我們可以在gradle中做同樣的事情settings.gradle: ~~~java rootProject.name = 'simple-multi-module' include 'simple-weather', 'simple-webapp' ~~~ ## profile和屬性 maven中可以使用profile來區別不同的環境,在gradle中,我們可以定義好不同的profile檔案,然後通過指令碼來載入他們: build.gradle: ~~~java if (!hasProperty('buildProfile')) ext.buildProfile = 'default' apply from: "profile-${buildProfile}.gradle" task greeting { doLast { println message } } ~~~ profile-default.gradle: ~~~java ext.message = 'foobar' ~~~ profile-test.gradle: ~~~java ext.message = 'testing 1 2 3' ~~~ 我們可以這樣來執行: ~~~java >
gradle greeting foobar > gradle -PbuildProfile=test greeting testing 1 2 3 ~~~ ## 資源處理 在maven中有一個process-resources階段,可以執行resources:resources用來進行resource檔案的拷貝操作。 在Gradle中的Java plugin的processResources task也可以做相同的事情。 比如我可以執行copy任務: ~~~java task copyReport(type: Copy) { from file("$buildDir/reports/my-report.pdf") into file("$buildDir/toArchive") } ~~~ 更加複雜的拷貝: ~~~java task copyPdfReportsForArchiving(type: Copy) { from "$buildDir/reports" include "*.pdf" into "$buildDir/toArchive" } ~~~ 當然拷貝還有更加複雜的應用。這裡就不詳細講解了。 > 本文已收錄於 [http://www.flydean.com/gradle-vs-maven/ ](http://www.flydean.com/gradle-vs-maven/ ) > > 最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現! > > 歡迎關注我的公眾號:「程式那些事」,懂技術,更