漫談依賴管理工具:從Maven,Gradle到Go

來源:互聯網
上載者:User

為什麼要有依賴管理工具?

談依賴管理之前,我們先談談為什麼要有依賴管理工具這東西。

我們學了一種程式設計語言,然後寫了個“Hello World”,然後宣稱自己學了一門語言,這時候確實不需要關心依賴問題。

然而,當你要寫一個稍微複雜點的應用,那怕就是留言板這樣的,需要讀寫資料庫,就需要依賴資料庫驅動,就會遇到依賴管理的問題了。

再進一步,你寫了一個庫,想共用給別人使用,更需要瞭解依賴管理的問題。

當然,如果項目足夠簡單,你可以直接將依賴方的源碼放置在自己的項目中,或者將依賴庫的二進位檔案(比如jar,dll)放置在項目的lib裡。要提供給別人呢?把二進位包提供下載或者給別人傳過去。依賴管理工具出現之前大多數都是這樣搞的。

但如果再複雜些,依賴庫本身也有依賴怎麼弄呢?將依賴壓縮打包,然後放個readme協助檔案說明下,貌似也可以工作。

那如果你的項目依賴了好幾個,乃至幾十個庫,而各庫又有依賴,依賴也有自己的依賴,怎麼辦?怎麼檢測庫的依賴是否有版本衝突?以後升級的時候怎麼辦?怎麼判斷lib目錄下的某個檔案是否被依賴了?

到這一步必須要承認需要有個依賴管理工具了,無論你使用任何語言。我們大約也清楚了依賴管理要做些什麼。假設還沒有依賴管理工具,我們自己要設計一個,如何入手?

1.要有一種依賴庫的命名規則,或者叫座標(Coordinates)的定義規則,可以通過座標準確找到依賴的庫。

2.要有對應的設定檔規則,來描述和定義依賴。

3.要有中心倉庫儲存這些依賴庫,以及依賴庫的中繼資料(metadata),供使用方拉取。

4.還需要一個本地工具去解析這個設定檔,實現依賴的拉取。

以上其實就是各依賴管理工具的核心要素。

聊聊Maven

Maven誕生於2004年(來源維基),查詢了下,應該是各語言的依賴管理工具中早的。Ruby的gem也是2004年出現的,但gem離完備的依賴管理工具還差些,直到Ruby的bundler出現。Python的pip出現的更晚。

Maven的習慣是通過 groupID(一般是組織的網域名稱倒寫,遵循Java package的命名習慣)+ artifactId(庫本身的名稱) + version(版本)來定義座標,通過xml來做設定檔,提供了中心倉庫(repo.maven.org)以及本地工具(mvn)。

依賴定義: <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都類似,只是設定檔文法和座標命名規則有些差異。

至此,看起來Maven很簡單啊!為啥許多人會覺得Maven複雜呢?

主要在於以下兩點:

1.Java這樣需要編譯的語言,發布的庫是二進位版本的jar包,發布前需要有編譯的流程,而依賴和編譯是緊密相關的。不像Ruby,Node這樣的指令碼語言,將源碼和設定檔扔到倉庫就可以。

2.Maven並沒有將自己單純的定義為依賴管理工具,而是專案管理工具,它將項目的整個生命週期都囊括進去了。

第二點也是Ant+ivy和Maven思路上的區別,ivy認為已經有Ant這樣的編譯打包工具了,只需在上面做個外掛程式解決依賴問題即可,而Maven認為Ant本身也有改進的地方,所以一併改造了。

Maven的改進的核心思路是:Convention Over Configuration

即“約定大於配置”。既然大多數人習慣都把源碼目錄命名為src,那就約定好都用這個目錄,不用專門去配置。同樣,clean,compile,package等也約定好,不需要專門定義Ant task。這樣既簡化了設定檔,同時也降低了學習成本。一個Ant定義的項目,你需要閱讀協助檔案或者查看build.xml檔案才能瞭解如何編譯打包,而Maven定義的項目直接運行“mvn package”即可。

Java語言發明的比較早,初期這種思想還不普及,所以Java本身沒有對項目的規範,而新的語言基本都吸收了這個思想,對項目都做了約定和規範。比如Go語言,如果用C/C++可能需要定義複雜的Makefile來定義編譯的規則,以及如何運行測試案例,而在Go中,這些都是約定好的。

Maven定義為專案管理工具,包含了項目從源碼到發布的整個生命週期:

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本身的編輯打包等功能都是用外掛程式來實現的,也允許使用者自己定義外掛程式。

同時涉及構建生命週期的不同的階段,依賴也需要確定是編譯依賴?測試依賴?運行時依賴?於是依賴多了scope的定義。

如果僅僅是這樣把Maven理解成標準化的Ant+ivy+可擴充的外掛程式架構即可?但現實世界的項目往往更複雜。

我們有了function用於組合代碼塊邏輯,有了object用於組合一組方法,有了package,namespace用於組合一組相關對象,但其實還需要有更高一個層次的組合定義 —– module,或者叫子項目。同一個項目下,不同的源碼目錄可能需要編譯打包成不同的二進位檔案,這些module共同構成了一個整體的項目。這個其實和源碼管理習慣有關係,是每個獨立的module作為單獨的源碼倉庫呢?還是將相關的module全部放在一起?從降低溝通成本的角度考慮,還是應該通過一個大的倉庫組織。

於是Maven引入了module的概念,同一個項目下可以有多個module,每個module有單獨的pom檔案來定義,但為了避免重複,Maven的pom檔案支援parent機制,子項目的pom檔案繼承parent pom的基本配置。可以說,module的機制將Maven的複雜度又提升了一個層次,很多人遇到Maven的坑多栽到這裡了。

這裡介紹一個Maven多項目版本管理的最佳實務:

1.父項目中配置版本號碼,子項目中不要顯示配置版本號碼,直接繼承父項目的版本號碼。

2.子項目之間的依賴通過${project.version}引用,不要明確配置版本號碼。

3.發布新版的時候,同時發布所有子項目,即便是該子項目未做變更。

4.最好通過Maven的release外掛程式發布,避免手動修改版本號碼導致的不一致問題。

即便是這樣,Maven的多項目版本管理經常也會遇到問題。主要是因為Maven的子項目之間的依賴也沿用的是第三方庫依賴的配置方式,需要指定子項目的版本號碼。另外子項目的parent需要顯式配置,也需要明確指定parent的版本號碼。一旦這些版本號碼出現錯誤,最後就會導致各種詭異的問題。

Maven的release外掛程式使用也比較複雜,該外掛程式其實做幾個事情:

1.先構建一遍項目,確認項目可以正常構建。

2.修改pom檔案的版本號碼到正式版,然後提交到源碼倉庫並打tag。

3.將該tag的源碼檢出,再構建一次,這次構建的jar包的版本是正式版的,將jar包上傳到Maven倉庫。

4.遞增版本號碼,修改pom檔案的版本號碼到SNAPSHOT,再次提交到源碼倉庫。

這個過程中,由於要構建兩次,提交兩次源碼倉庫,上傳一次jar包,任何一步出錯都會導致release失敗,所以使用比較複雜。

到此,Maven的核心概念都分析完了,其他的都是外掛程式機制上的一些擴充。大家也應該明白了Maven之所以最後變這麼複雜的原因。

但無論如何,Maven基本上是專案管理工具的標杆了,有的語言直接通過擴充外掛程式來用Maven管理,比如C++,C#(NMaven),或者做了移植Byldan(C#),不過貌似都是不太成功,估計主要原因應該是Maven是用Java寫的,有社區隔膜。

Gradle對Maven的改進

聊了Maven的思路和優勢,那Maven的缺點呢?這個我們和Gradle一起聊聊。Gradle就是在Maven的基礎上進行的改進。優勢主要體現在以下方面:

1.配置語言

Maven使用的是XML,受限於XML的表達能力以及XML本身的冗餘,會使pom.xml檔案顯得冗長而笨重。而Gradle是基於Groovy定義的一種DSL語言,簡潔並且表達能力強大。在Maven中,任何擴充都需要通過Maven外掛程式實現,但Gradle的設定檔本身就是一種語言,可以直接依賴任意Java庫,可以直接在build.gradle檔案中像Ant一樣定義task,比Ant的表達能力更強(Ant本身也是XML定義的)。

Gradle的設定檔中可以直接擷取到Project對象以及環境變數,可以通過程式對build過程進行更細緻的自訂控制,這個功能對於複雜的項目來說非常有用。

2.項目自包含(Self Provisioning Build Environment)

使用者下載了一個Maven定義的項目,如果沒用過Maven,還需要下載Maven工具包,瞭解Maven。但Gradle可以給項目產生一個 gradlew指令碼,使用者直接運行gradlew指令碼即可,該指令碼會自動檢測本地是否已經有Gradle,沒有則從網路下載,對使用者透明(當然,國內網路下最好還是自己先下載好)。

對倉庫的配置,Maven提供了一個本地的settings.xml設定檔,用於定義私人倉庫以及倉庫密碼這樣敏感的不應該放源碼倉庫裡的檔案。但這樣帶來的不便就是這些資訊項目中沒有自包含,所以Gradle幹掉了這種本地配置的機制,所有的定義都在項目裡。私人倉庫密碼這樣的可以放在項目下的 gradle.properties檔案裡不提交上去,通過其他方式分享給內部成員。這點可能各有優劣。

3.任務依賴以及執行機制

Maven的構建生命週期的每一步都是預定義好的(參看前文),外掛程式任務只能在預留的生命週期中的某個階段切入,雖然Maven的生命週期階段考慮很充分,但有時候也不能滿足需求。Maven會嚴格按照生命週期的階段從開始線性執行任務,而Gradle則使用了Directed Acyclic Graph來檢測任務的依賴關係,決定哪些任務可以並存執行,這樣使任務的定義以及執行都更靈活。

4.依賴管理更為靈活

Maven對依賴管理比較嚴格,依賴必須是源碼倉庫的座標。雖然也支援system scope的本地路徑配置,但還是有許多不方便之處(system scope的依賴,打包的時候不包含進來)。如果世界上所有的庫都通過Maven發布,當然沒有問題,但現實往往不是這樣的。這裡要吐槽一下國內的各大廠發布的sdk之類的庫,幾乎都不提供倉庫地址,就給個壓縮包放一堆jar包進來,讓使用者自己搞定依賴管理問題。而Gradle在這方面比較靈活,比如支援:

compile fileTree(dir: 'libs', include: '*.jar')
這樣的配置規則。

另外由於Gradle本身是一種語言,可以用編程的方式來管理依賴。比如大多數子項目都依賴某個庫,除了個別幾個,就可以這樣寫:

configure(subprojects.findAll {it.name != 'xxx1’ && it.name != ‘xxx2’}) { dependencies { compile("com.google.guava:guava:18.0”) } }
5.子項目以及動態依賴機制

動態依賴主要是用來解決幾個互相依賴的庫都在快速開發期間的依賴問題,不能每次地層庫修改發布新版本,上層庫都要修改依賴設定檔,所以需要動態設定依賴最新版本。

Maven的解決方案是SNAPSHOT機制,子項目之間也是通過這個機制來實現依賴的。遇到的問題我們前面也分析了。

Gradle的雖然也相容Maven倉庫的SNAPSHOT機制,但它自己的版本管理機制上,並沒有引入SNAPSHOT機制。它的依賴支援4.x,2.+這樣的配置規則,實現動態依賴(註:Maven也支援類似的規則,參看 Dependency Version Requirement Specification)。而子項目之間的依賴採用特殊的依賴配置,和第三方庫的配置規則有區別。它直接使用:

compile project(“:subpoject-name”);
這樣的配置,無需配置版本號碼,明確指定是子項目,避免Maven的子項目依賴帶來的版本號碼問題。子項目的配置中也不需要顯示配置父項目,只需要父項目單向中配置子項目列表即可。

同時Gradle的release機制也更為靈活,支援release到各種倉庫(包括Maven倉庫),但不控制release過程中的版本號碼產生,修改源碼倉庫等步驟,留給使用者自己通過手動或者CI工具,或者指令碼去解決。

關於Gradle相對Maven的改進這裡主要列舉這幾點,其他的可以參看Gradle官方的比較表格:maven_vs_gradle,這裡不再詳述。

Go語言的多項目以及依賴管理問題

最後再談談Go語言的多項目以及依賴管理問題。Go官方對這兩方面並未做約定或者提供工具,於是只能各自想辦法解決。多項目問題一般就是迴歸到了 Makefile+指令碼的解決方案,比如kubernetes。依賴管理,開源社區多用Godeps,kubernetes用的也是這個。Godeps通過源碼倉庫路徑以及源碼tag來確定庫的座標,只管理依賴,有點像ivy,不關心構建過程。Godepes會將依賴庫的依賴也添加到當前項目的依賴配置中,不是動態依賴傳遞機制。沒有scope,不區分是否是單元測試的依賴。一個倉庫只支援一個配置,沒有子項目概念,項目大了管理就比較複雜。另外它對傳遞依賴以及版本衝突的問題當前還是沒有解決太好(有一些相關Issue)。

一個語言的多項目以及依賴管理方案對這個語言的生態發展有很大的影響,Java發展到現在,Maven以及Gradle功不可沒,所以感覺Go官方應該對這兩方面有所作為。Go語言遲遲沒出依賴管理工具,個人覺得有幾方面考慮:

1.Go尚未確定動態庫的機制。編譯型語言依賴最好也是二進位的,而不是源碼。一方面可以加快編譯速度,另外一方面也可以實現源碼保護,方便分發以及代理緩衝,讓語言的適用範圍更廣。許多商業上的庫是不方便提供源碼的。所以依賴管理工具的實現需要動態庫的機制。而動態庫尚未確定的原因我覺得是Go 語言不想過早的引入二進位動態庫的格式相容問題,初期全部用源碼是最省事的。

2.先讓社區試試水,看看效果和反饋。

任何一個語言,發展到一定階段都避不開依賴管理問題。前一段時間看到一篇寫Go語言的文章,嘲諷Java的Maven構建個項目恨不能把半個互連網下載下來,我當時腦海中就浮現出長者的那句經典語錄“圖樣圖森破”。Go當前沒遇到這些問題的原因只是Go還比較年輕,庫還不夠豐富,以及Go的很多項目還不夠複雜。而像kubernetes這樣的項目,當前依賴已經有226個了,構建一下,也快要下載半個Github了。所以個人覺得Go社區當前還是非常需要一個類似於Gradle的工具,來解決依賴管理,構建,多專案管理等問題。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.