漫談依賴管理工具:從Maven,Gradle到Go
為什么要有依賴管理工具?
談依賴管理之前,我們先談?wù)劄槭裁匆幸蕾嚬芾砉ぞ哌@東西。
我們學(xué)了一種編程語言,然后寫了個“Hello World”,然后宣稱自己學(xué)了一門語言,這時候確實不需要關(guān)心依賴問題。
然而,當(dāng)你要寫一個稍微復(fù)雜點的應(yīng)用,那怕就是留言板這樣的,需要讀寫數(shù)據(jù)庫,就需要依賴數(shù)據(jù)庫驅(qū)動,就會遇到依賴管理的問題了。
再進一步,你寫了一個庫,想共享給別人使用,更需要了解依賴管理的問題。
當(dāng)然,如果項目足夠簡單,你可以直接將依賴方的源碼放置在自己的項目中,或者將依賴庫的二進制文件(比如jar,dll)放置在項目的lib里。要提供給別人呢?把二進制包提供下載或者給別人傳過去。依賴管理工具出現(xiàn)之前大多數(shù)都是這樣搞的。
但如果再復(fù)雜些,依賴庫本身也有依賴怎么弄呢?將依賴壓縮打包,然后放個readme幫助文件說明下,貌似也可以工作。
那如果你的項目依賴了好幾個,乃至幾十個庫,而各庫又有依賴,依賴也有自己的依賴,怎么辦?怎么檢測庫的依賴是否有版本沖突?以后升級的時候怎么辦?怎么判斷l(xiāng)ib目錄下的某個文件是否被依賴了?
到這一步必須要承認(rèn)需要有個依賴管理工具了,無論你使用任何語言。我們大約也清楚了依賴管理要做些什么。假設(shè)還沒有依賴管理工具,我們自己要設(shè)計一個,如何入手?
1.要有一種依賴庫的命名規(guī)則,或者叫坐標(biāo)(Coordinates)的定義規(guī)則,可以通過坐標(biāo)準(zhǔn)確找到依賴的庫。
2.要有對應(yīng)的配置文件規(guī)則,來描述和定義依賴。
3.要有中心倉庫保存這些依賴庫,以及依賴庫的元數(shù)據(jù)(metadata),供使用方拉取。
4.還需要一個本地工具去解析這個配置文件,實現(xiàn)依賴的拉取。
以上其實就是各依賴管理工具的核心要素。
聊聊Maven
Maven誕生于2004年(來源維基),查詢了下,應(yīng)該是各語言的依賴管理工具中早的。Ruby的gem也是2004年出現(xiàn)的,但gem離完備的依賴管理工具還差些,直到Ruby的bundler出現(xiàn)。Python的pip出現(xiàn)的更晚。
Maven的習(xí)慣是通過 groupID(一般是組織的域名倒寫,遵循Java package的命名習(xí)慣)+ artifactId(庫本身的名稱) + version(版本)來定義坐標(biāo),通過xml來做配置文件,提供了中心倉庫(repo.maven.org)以及本地工具(mvn)?! ?/p>
- 依賴定義:
- <dependency>
- <groupId>com.google.guava</groupId>
- <artifactId>guava</artifactId>
- <version>18.0</version>
- </dependency>
- repo定義:
- <repository>
- <id>repo.default</id>
- <name>Internal Release Repository</name>
- <url>http://repo.xxxxxx.com/nexus/content/repositories/releases</url>
- <releases>
- <enabled>true</enabled>
- <updatePolicy>interval:60</updatePolicy>
- <checksumPolicy>warn</checksumPolicy>
- </releases>
- <snapshots>
- <enabled>false</enabled>
- <updatePolicy>always</updatePolicy>
- <checksumPolicy>warn</checksumPolicy>
- </snapshots>
- </repository>
同時,為了避免依賴沖突的問題,Maven的依賴配置提供了exclude配置機制,用于阻斷部分庫的傳遞依賴。
Ruby的gem,Node的npm,Python的pip,iOS的CocoaPods都類似,只是配置文件語法和坐標(biāo)命名規(guī)則有些差異。
至此,看起來Maven很簡單??!為啥許多人會覺得Maven復(fù)雜呢?
主要在于以下兩點:
1.Java這樣需要編譯的語言,發(fā)布的庫是二進制版本的jar包,發(fā)布前需要有編譯的流程,而依賴和編譯是緊密相關(guān)的。不像Ruby,Node這樣的腳本語言,將源碼和配置文件扔到倉庫就可以。
2.Maven并沒有將自己單純的定義為依賴管理工具,而是項目管理工具,它將項目的整個生命周期都囊括進去了。
第二點也是Ant+ivy和Maven思路上的區(qū)別,ivy認(rèn)為已經(jīng)有Ant這樣的編譯打包工具了,只需在上面做個插件解決依賴問題即可,而Maven認(rèn)為Ant本身也有改進的地方,所以一并改造了。
Maven的改進的核心思路是:Convention Over Configuration
即“約定大于配置”。既然大多數(shù)人習(xí)慣都把源碼目錄命名為src,那就約定好都用這個目錄,不用專門去配置。同樣,clean,compile,package等也約定好,不需要專門定義Ant task。這樣既簡化了配置文件,同時也降低了學(xué)習(xí)成本。一個Ant定義的項目,你需要閱讀幫助文件或者查看build.xml文件才能了解如何編譯打包,而Maven定義的項目直接運行“mvn package”即可。
Java語言發(fā)明的比較早,初期這種思想還不普及,所以Java本身沒有對項目的規(guī)范,而新的語言基本都吸收了這個思想,對項目都做了約定和規(guī)范。比如Go語言,如果用C/C++可能需要定義復(fù)雜的Makefile來定義編譯的規(guī)則,以及如何運行測試用例,而在Go中,這些都是約定好的。
Maven定義為項目管理工具,包含了項目從源碼到發(fā)布的整個生命周期:
- validate → generate-sources → process-sources
- → generate-resources → process-resources → compile
- → process-classes → generate-test-sources
- → process-test-sources → generate-test-resources
- → process-test-resources → test-compile
- → test → prepare-package → package
- → pre-integration-test → integration-test
- → post-integration-test → verify → install → deploy
既然包含了這么多功能和階段,所以Maven引入了插件機制,Maven本身的編輯打包等功能都是用插件來實現(xiàn)的,也允許用戶自己定義插件。
同時涉及構(gòu)建生命周期的不同的階段,依賴也需要確定是編譯依賴?測試依賴?運行時依賴?于是依賴多了scope的定義。
如果僅僅是這樣把Maven理解成標(biāo)準(zhǔn)化的Ant+ivy+可擴展的插件框架即可?但現(xiàn)實世界的項目往往更復(fù)雜。
我們有了function用于組合代碼塊邏輯,有了object用于組合一組方法,有了package,namespace用于組合一組相關(guān)對象,但其實還需要有更高一個層次的組合定義 —– module,或者叫子項目。同一個項目下,不同的源碼目錄可能需要編譯打包成不同的二進制文件,這些module共同構(gòu)成了一個整體的項目。這個其實和源碼管理習(xí)慣有關(guān)系,是每個獨立的module作為單獨的源碼倉庫呢?還是將相關(guān)的module全部放在一起?從降低溝通成本的角度考慮,還是應(yīng)該通過一個大的倉庫組織。
于是Maven引入了module的概念,同一個項目下可以有多個module,每個module有單獨的pom文件來定義,但為了避免重復(fù),Maven的pom文件支持parent機制,子項目的pom文件繼承parent pom的基本配置。可以說,module的機制將Maven的復(fù)雜度又提升了一個層次,很多人遇到Maven的坑多栽到這里了。
這里介紹一個Maven多項目版本管理的***實踐:
1.父項目中配置版本號,子項目中不要顯示配置版本號,直接繼承父項目的版本號。
2.子項目之間的依賴通過${project.version}引用,不要明確配置版本號。
3.發(fā)布新版的時候,同時發(fā)布所有子項目,即便是該子項目未做變更。
4.***通過Maven的release插件發(fā)布,避免手動修改版本號導(dǎo)致的不一致問題。
即便是這樣,Maven的多項目版本管理經(jīng)常也會遇到問題。主要是因為Maven的子項目之間的依賴也沿用的是第三方庫依賴的配置方式,需要指定子項目的版本號。另外子項目的parent需要顯式配置,也需要明確指定parent的版本號。一旦這些版本號出現(xiàn)錯誤,***就會導(dǎo)致各種詭異的問題。
Maven的release插件使用也比較復(fù)雜,該插件其實做幾個事情:
1.先構(gòu)建一遍項目,確認(rèn)項目可以正常構(gòu)建。
2.修改pom文件的版本號到正式版,然后提交到源碼倉庫并打tag。
3.將該tag的源碼檢出,再構(gòu)建一次,這次構(gòu)建的jar包的版本是正式版的,將jar包上傳到Maven倉庫。
4.遞增版本號,修改pom文件的版本號到SNAPSHOT,再次提交到源碼倉庫。
這個過程中,由于要構(gòu)建兩次,提交兩次源碼倉庫,上傳一次jar包,任何一步出錯都會導(dǎo)致release失敗,所以使用比較復(fù)雜。
到此,Maven的核心概念都分析完了,其他的都是插件機制上的一些擴展。大家也應(yīng)該明白了Maven之所以***變這么復(fù)雜的原因。
但無論如何,Maven基本上是項目管理工具的標(biāo)桿了,有的語言直接通過擴展插件來用Maven管理,比如C++,C#(NMaven),或者做了移植Byldan(C#),不過貌似都是不太成功,估計主要原因應(yīng)該是Maven是用Java寫的,有社區(qū)隔膜。
Gradle對Maven的改進
聊了Maven的思路和優(yōu)勢,那Maven的缺點呢?這個我們和Gradle一起聊聊。Gradle就是在Maven的基礎(chǔ)上進行的改進。優(yōu)勢主要體現(xiàn)在以下方面:
1.配置語言
Maven使用的是XML,受限于XML的表達能力以及XML本身的冗余,會使pom.xml文件顯得冗長而笨重。而Gradle是基于Groovy定義的一種DSL語言,簡潔并且表達能力強大。在Maven中,任何擴展都需要通過Maven插件實現(xiàn),但Gradle的配置文件本身就是一種語言,可以直接依賴任意Java庫,可以直接在build.gradle文件中像Ant一樣定義task,比Ant的表達能力更強(Ant本身也是XML定義的)。
Gradle的配置文件中可以直接獲取到Project對象以及環(huán)境變量,可以通過程序?qū)uild過程進行更細(xì)致的自定義控制,這個功能對于復(fù)雜的項目來說非常有用。
2.項目自包含(Self Provisioning Build Environment)
用戶下載了一個Maven定義的項目,如果沒用過Maven,還需要下載Maven工具包,了解Maven。但Gradle可以給項目生成一個 gradlew腳本,用戶直接運行g(shù)radlew腳本即可,該腳本會自動檢測本地是否已經(jīng)有Gradle,沒有則從網(wǎng)絡(luò)下載,對用戶透明(當(dāng)然,國內(nèi)網(wǎng)絡(luò)下***還是自己先下載好)。
對倉庫的配置,Maven提供了一個本地的settings.xml配置文件,用于定義私有倉庫以及倉庫密碼這樣敏感的不應(yīng)該放源碼倉庫里的文件。但這樣帶來的不便就是這些信息項目中沒有自包含,所以Gradle干掉了這種本地配置的機制,所有的定義都在項目里。私有倉庫密碼這樣的可以放在項目下的 gradle.properties文件里不提交上去,通過其他方式分享給內(nèi)部成員。這點可能各有優(yōu)劣。
3.任務(wù)依賴以及執(zhí)行機制
Maven的構(gòu)建生命周期的每一步都是預(yù)定義好的(參看前文),插件任務(wù)只能在預(yù)留的生命周期中的某個階段切入,雖然Maven的生命周期階段考慮很充分,但有時候也不能滿足需求。Maven會嚴(yán)格按照生命周期的階段從開始線性執(zhí)行任務(wù),而Gradle則使用了Directed Acyclic Graph來檢測任務(wù)的依賴關(guān)系,決定哪些任務(wù)可以并行執(zhí)行,這樣使任務(wù)的定義以及執(zhí)行都更靈活。
4.依賴管理更為靈活
Maven對依賴管理比較嚴(yán)格,依賴必須是源碼倉庫的坐標(biāo)。雖然也支持system scope的本地路徑配置,但還是有許多不方便之處(system scope的依賴,打包的時候不包含進來)。如果世界上所有的庫都通過Maven發(fā)布,當(dāng)然沒有問題,但現(xiàn)實往往不是這樣的。這里要吐槽一下國內(nèi)的各大廠發(fā)布的sdk之類的庫,幾乎都不提供倉庫地址,就給個壓縮包放一堆jar包進來,讓用戶自己搞定依賴管理問題。而Gradle在這方面比較靈活,比如支持:
- compile fileTree(dir: 'libs', include: '*.jar')
這樣的配置規(guī)則。
另外由于Gradle本身是一種語言,可以用編程的方式來管理依賴。比如大多數(shù)子項目都依賴某個庫,除了個別幾個,就可以這樣寫:
- configure(subprojects.findAll {it.name != 'xxx1’ && it.name != ‘xxx2’}) {
- dependencies {
- compile("com.google.guava:guava:18.0”)
- }
- }
5.子項目以及動態(tài)依賴機制
動態(tài)依賴主要是用來解決幾個互相依賴的庫都在快速開發(fā)期間的依賴問題,不能每次地層庫修改發(fā)布新版本,上層庫都要修改依賴配置文件,所以需要動態(tài)設(shè)置依賴***版本。
Maven的解決方案是SNAPSHOT機制,子項目之間也是通過這個機制來實現(xiàn)依賴的。遇到的問題我們前面也分析了。
Gradle的雖然也兼容Maven倉庫的SNAPSHOT機制,但它自己的版本管理機制上,并沒有引入SNAPSHOT機制。它的依賴支持4.x,2.+這樣的配置規(guī)則,實現(xiàn)動態(tài)依賴(注:Maven也支持類似的規(guī)則,參看 Dependency Version Requirement Specification)。而子項目之間的依賴采用特殊的依賴配置,和第三方庫的配置規(guī)則有區(qū)別。它直接使用:
- compile project(“:subpoject-name”);
這樣的配置,無需配置版本號,明確指定是子項目,避免Maven的子項目依賴帶來的版本號問題。子項目的配置中也不需要顯示配置父項目,只需要父項目單向中配置子項目列表即可。
同時Gradle的release機制也更為靈活,支持release到各種倉庫(包括Maven倉庫),但不控制release過程中的版本號生成,修改源碼倉庫等步驟,留給用戶自己通過手動或者CI工具,或者腳本去解決。
關(guān)于Gradle相對Maven的改進這里主要列舉這幾點,其他的可以參看Gradle官方的比較表格:maven_vs_gradle,這里不再詳述。
Go語言的多項目以及依賴管理問題
***再談?wù)凣o語言的多項目以及依賴管理問題。Go官方對這兩方面并未做約定或者提供工具,于是只能各自想辦法解決。多項目問題一般就是回歸到了 Makefile+腳本的解決方案,比如kubernetes。依賴管理,開源社區(qū)多用Godeps,kubernetes用的也是這個。Godeps通過源碼倉庫路徑以及源碼tag來確定庫的坐標(biāo),只管理依賴,有點像ivy,不關(guān)心構(gòu)建過程。Godepes會將依賴庫的依賴也添加到當(dāng)前項目的依賴配置中,不是動態(tài)的依賴傳遞機制。沒有scope,不區(qū)分是否是單元測試的依賴。一個倉庫只支持一個配置,沒有子項目概念,項目大了管理就比較復(fù)雜。另外它對傳遞依賴以及版本沖突的問題當(dāng)前還是沒有解決太好(有一些相關(guān)Issue)。
一個語言的多項目以及依賴管理方案對這個語言的生態(tài)發(fā)展有很大的影響,Java發(fā)展到現(xiàn)在,Maven以及Gradle功不可沒,所以感覺Go官方應(yīng)該對這兩方面有所作為。Go語言遲遲沒出依賴管理工具,個人覺得有幾方面考慮:
1.Go尚未確定動態(tài)庫的機制。編譯型語言依賴***也是二進制的,而不是源碼。一方面可以加快編譯速度,另外一方面也可以實現(xiàn)源碼保護,方便分發(fā)以及代理緩存,讓語言的適用范圍更廣。許多商業(yè)上的庫是不方便提供源碼的。所以依賴管理工具的實現(xiàn)需要動態(tài)庫的機制。而動態(tài)庫尚未確定的原因我覺得是Go 語言不想過早的引入二進制動態(tài)庫的格式兼容問題,初期全部用源碼是最省事的。
2.先讓社區(qū)試試水,看看效果和反饋。
任何一個語言,發(fā)展到一定階段都避不開依賴管理問題。前一段時間看到一篇寫Go語言的文章,嘲諷Java的Maven構(gòu)建個項目恨不能把半個互聯(lián)網(wǎng)下載下來,我當(dāng)時腦海中就浮現(xiàn)出長者的那句經(jīng)典語錄“圖樣圖森破”。Go當(dāng)前沒遇到這些問題的原因只是Go還比較年輕,庫還不夠豐富,以及Go的很多項目還不夠復(fù)雜。而像kubernetes這樣的項目,當(dāng)前依賴已經(jīng)有226個了,構(gòu)建一下,也快要下載半個Github了。所以個人覺得Go社區(qū)當(dāng)前還是非常需要一個類似于Gradle的工具,來解決依賴管理,構(gòu)建,多項目管理等問題。