Maven學習筆記(四):坐標與依賴
來源:程序員人生 發布時間:2014-10-13 14:33:47 閱讀次數:2716次
Maven坐標詳解:
Maven定義了這樣一組規則:世界上任何一個構件都可以使用Maven坐標唯一標識,Maven坐標的元素包括groupId、artifactId、version、packaging、classifier。我們只需要提供正確的坐標元素,Maven就能找到對應的構件。比如當需要使用Java5平臺上的TestNG的5.8版本時,就告訴Maven:”groupId=org.testng;
artifactId=testng; version=5.8; classifer=jdk15,maven就會從中央倉庫(http://search.maven.org/#browse)中尋找相應的構件供我們使用。
先看一組坐標定義,如下:
<groupId>org.sonatype.nexus</groupId>
<artifactId>nexus-indexer</artifactId>
<version>2.0.0</version>
<packaging>jar</packaging>
nexus-indexer是一個對Maven倉庫編纂索引并提供搜索功能的類庫,它是Nexus項目中的一個子模塊。后面會詳細介紹Nexus。下面詳細解釋一下各個坐標元素:
groupId:定義當前Maven項目隸屬的實際項目。首先,Maven項目和實際項目不一定是一對一的關系,比如SpringFramework這一實際項目,其對應的Maven項目會有很多,如spring-core、spring-context等。一個實際項目通常會劃分成多個項目模塊。groupId不應該只對應于項目隸屬的組織或公司,原因是一個組織下會有很多實際項目,如果groupId只定義到組織級別,后面可以看到,artifactId只能對應Maven項目,那么實際項目這個層將難以定義。最后,groupId的表示方式與java包名的表示方式類似,通常與域名反向一一對應。
artifactId:該元素定義了實際項目中的一個Maven項目(模塊),推薦的做法是使用實際項目名稱作為artifactId的前綴。比如上例的artifactId是nexus-indexer,使用了實際項目名nexus作為前綴,這樣做的好處是方便尋找實際構件。
version:該元素定義了Maven項目當前所處的版本。實際上,Maven定義了一套完整的版本規范,以及快照(SNAPSHOT)的概念。在后面的章節將詳細討論。
packaging:該元素定義Maven項目的打包方式。首先,打包方式通常與所生成構件的文件擴展名對應,如上例中packaging為jar,最終的文件名為nexus-indexer-2.0.0.jar。而是用war打包方式的Maven項目,最終生成的構件會有一個.war文件,但這不是絕對的。當不定義packaging時,Maven會是用默認值jar。
classifier:該元素用來幫助定義構件輸出的一些附屬構件。附屬構件與主構件對應。如上例中的主構件是nexus-indexer-2.0.0.jar,該項目還會通過一些插件生成如nexus-indexer-2.0.0-javadoc.jar、nexus-indexer-2.0.0-sources.jar這樣一些附屬構件,其包含了Java文檔和源代碼。這時候,javadoc和sources就是這兩個附屬構件的classifier。這樣,附屬構件也就擁有了自己唯一的坐標。注意:不能直接定義項目的classifier,因為附屬構件不是項目直接默認生成的,而是由附加的插件幫助生成。
項目構件的文件名是與坐標相對應的,一般的規則是artifactId-version[-classifier].packaging,[-classifier]表示可選。這里還要強調一點,packaging并非一定與構件擴展名對應,比如packaging為maven-plugin的構件擴展名為jar。
依賴的配置:
一個依賴聲明可以包含如下的一些元素:
<project>
...
<dependencies>
<dependency>
<groupId>...</groupId>
<artifactId>...</artifactId>
<version>...</version>
<type>...</type>
<scope>...</scope>
<optional>...</optional>
<exclusions>
<exclusion>
...
</exclusion>
...
</exlusions>
</dependency>
...
</dependencies>
...
</project>
根元素project下的dependencies可以包含一個或者多個denpendency元素,以聲明一個或者多個項目依賴。每個依賴可以包含的元素有:
- groupId、artifactId和version:依賴的基本坐標,對于任何一個依賴來說,基本坐標是最重要的,Maven根據坐標才能找到需要的依賴。
- type:依賴的類型,對應于項目坐標定義的packaging。大部分情況下,該元素不必聲明,其默認值為jar。
- scope:依賴的范圍,請見后面小節
- optional:標記依賴是否可選,請見后面小節
- exclusions:用來排除傳遞性依賴,請見后面小節
Maven依賴范圍:
在Maven中,依賴范圍用元素scope表示。Maven在執行編譯、測試、運行時執行的是三套不同的classpath。
依賴范圍就是用來控制依賴與這三種classpath(編譯classpath、測試classpath、運行classpath)的關系,Maven有以下幾種依賴范圍:
compile:編譯依賴范圍。如果沒有指定,就會默認使用該依賴范圍。該此依賴范圍對于編譯、測試、運行三種classpath都有效。典型的例子是spring-core,在編譯、測試和運行的時候都需要使用該依賴。
test:測試依賴范圍。該依賴范圍只對于測試classpath有效,在編譯主代碼或者運行項目的時將無法使用此類依賴。典型的例子就是JUnit,它只有在編譯測試代碼及運行測試環境的時候才需要。
provided:已提供依賴范圍。該依賴范圍對于測試和運行class-path有效,但在運行時無效。典型的例子是servlet-api,編譯和測試項目的時候需要該依賴,但在運行項目的時候,由于容器已經提供,就不需要Maven重復引入一遍。
runtime:運行時依賴范圍。該范圍依賴,對于運行和測試class-path有效,但在編譯主代碼時無效。典型的例子是JDBC驅動實現,項目主代碼的編譯只需要JDK提供的JDBC接口,只有在執行測試或者運行項目的時候才需要實現上述接口的具體的JDBC驅動。
system:系統依賴范圍。該依賴與三種classpath的關系,和provided依賴范圍完全一致。但是,使用system范圍的依賴時必須通過systemPath元素顯式地指定依賴文件的路徑。由于此類依賴不是通過Maven倉庫解析的,而且往往與本機系統綁定,可能造成構建的不可移植,因此應該謹慎使用。systemPath元素可以引用環境變量,如:
<dependency>
<groupId>javax.sql</groupId>
<artifactId>jdbc-stdext</artifactId>
<version>2.0</version>
<scope>system</scope>
<systemPath>${java.home}/lib/rt.jar</systemPath>
</dependency>
import: 導入依賴范圍。該依賴范圍不會對三種classPath產生實際的影響,我們將在后面的章節詳細介紹該依賴。
傳遞性依賴:
何為傳遞性依賴:
傳遞性性依賴的意思是項目A依賴了B構件,而在B構件的pom.xml中又顯式的依賴了C構件,那么A項目也就會依賴到C構件。在不使用Maven的項目當中,我們通常需要手動的去尋找所有直接使用和間接使用的構件(傳遞性依賴),以及解決版本沖突的問題,這將耗費很大的精力且意義不大。
Maven的傳遞性依賴機制可以很好的解決這一問題。在A項目下有一個org.springframework:spring-core:2.5.6的依賴,而實際上spring-core也有它自己的依賴,例如spring-core-2.5.6.pom該文件包含了一個commos-logging依賴,見下面代碼:
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifact>
<version>1.1.1</version>
</denpendency>
commons-logging沒有聲明依賴范圍,那么其依賴范圍就是默認的compile,而spring-core一般的依賴范圍也是compile。
A項目有一個compile范圍的spring-core依賴,spring-core有一個compile范圍的commons-logging依賴,那么commons-logging就會成為A項目的compile范圍依賴,commons-logging是account-email的一個傳遞性依賴。
有了傳遞性依賴機制,在使用Spring Framework的時候就不用去考慮它依賴了什么,也不用擔心引入多于的依賴。Maven會解析各個直接依賴的POM,將那些必要的間接依賴,以傳遞性依賴的形式引入到當前的項目之中。
傳遞性依賴和依賴范圍:
假設A依賴與B,B依賴與C,我們說A對于B是第一直接依賴,B對于C是第二直接依賴,A對于C是傳遞性依賴。第一直接依賴的范圍和第二直接依賴的范圍決定了傳遞性依賴的范圍。如下表所示,最左邊一列表示第一直接依賴范圍,最上面一行表示第二直接依賴范圍,中間的交叉單元格則表示傳遞性依賴范圍。
|
compile |
test |
provided |
runtime |
compile |
compile |
- |
- |
runtime |
test |
test |
- |
- |
test |
provided |
provided |
- |
- |
provided |
runtime |
runtime |
- |
- |
runtime |
仔細觀察該表,可以發現如下的規律:當第二直接依賴的范圍是compile的時候,傳遞性依賴的范圍與第一直接依賴的范圍一致;當第二直接依賴的范圍是test的時候,依賴不會得以傳遞;當第二直接依賴的范圍是provided的時候,只傳遞第一直接依賴范圍也為provided的依賴,且傳遞性依賴的范圍同樣為provided;當第二直接依賴的范圍是runtime的時候,傳遞性依賴的范圍與第一直接依賴的范圍一致,但compile例外,此時傳遞性依賴的范圍為runtime。
依賴調解:
Maven的傳遞性依賴機制,一方面大大簡化和方便了依賴聲明。但有時候造成問題時,我們需要知道該傳遞性依賴是從哪條依賴路徑引入的。
例如,項目A有這樣的依賴關系:A->B->C->X(1.0)、A->D->X(2.0),X是A的傳遞性依賴,但是兩條依賴路徑上有兩個版本的X,那么哪個X會被Maven解析使用呢?兩個版本都被解析是不行的,因為會造成重復依賴。Maven依賴的第一原則是:路徑最近者優先。該例中X(1.0)的路徑長度為3,而X(2.0)的路徑長度為2,因此X(2.0)會被解析使用。
Maven定義了依賴調解的第二原則:第一聲明者優先。在依賴路徑長度相等的前提下,在POM中依賴聲明的順序決定了誰會被解析使用,順序最靠前的那個依賴優勝。
可選依賴:
假設有以下狀態的依賴:A->B、B->X(可選)、B->Y(可選)。根據傳遞性依賴的定義,如果所有這三個依賴的范圍都是compile,那么X、Y就是A的compile范圍傳遞性依賴。然而,由于這里X、Y是可選依賴,依賴將不會得以傳遞。
使用可選依賴的原因可能是B實現了兩個特性,其中的特性一依賴于X,特性二依賴于Y,而且這兩個特性是互斥的,用戶不可能同時使用這兩個特性。比如B是一個持久層隔離工具包,它支持多種數據庫,包括MySQL,PostgreSQL等,在構建工具包的時候,需要這兩種數據庫的驅動程序,但在使用這個工具包的時候,只會依賴一種數據庫。
項目B的依賴聲明見如下:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>project-b</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.10</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>8.4-701.jdbc3</version>
<optional>true</optional>
</dependency>
</dependencies>
</project>
在pom.xml中,使用<optional>元素表示mysql-connector-java和postgresql這兩個依賴為可選依賴,它們只對當前項目B產生影響,當其他項目依賴于B的時候,這兩個依賴不會被傳遞。因此,當項目A依賴于項目B的時候,如果其實際使用基于MySql數據庫,那么在A項目中就想要顯示地聲明mysql-connector-java這一依賴,見下面A項目pom.xml。
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>project-a</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>project-b</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>8.4-701.jdbc3</version>
</dependency>
</dependencies>
</project>
在理想情況中,是不應該使用可選依賴的。使用可選依賴的背景是一個項目實現了多個特性,在面向對象的設計中,有個單一職責性原則,意指一個類應該只有一項職責,而不是糅合太多的功能。在上面的例子中,更好的做法是為MySql和PostgreSQL分別創建一個Maven項目,基于同樣的groupId分配不同的artifactId,如com.juvenxu.mvnbook;project-b-mysql和com.juvenxu.mvnbook:project-b-postgresql,在各自的POM中聲明對應的JDBC驅動依賴,而且不適用可選依賴,用戶則根據需要選擇使用project-b-mysql或者project-b-postgresql。由于傳遞性依賴的作用,就不再聲明JDBC驅動依賴。
最佳實踐:
排除依賴:
傳遞性依賴雖然簡化了項目依賴的管理,但有時也會帶來一些問題,需要我們排除一些傳遞性依賴。例如:當前項目有一個第三方依賴,而這個第三方依賴依賴了另一個類庫的SNAPSHOT版本,那么這個SNAPSHOT就會成為當前項目的傳遞性依賴,而SNAPSHOT的不穩定性會直接影響到當前的項目。這時候就需要排除掉該SNAPSHOT,并且在當前的項目中聲明該類庫的某個正式發布的版本。
還有一些情況,你也可能需要排除依賴,比如SUN JTA API,Hibernate依賴于這個JAR,但是由于版本的因素,該類庫不在中央倉庫中,而Apache Geronimo項目中有一個相應的實現。這時你就可以排除Sun JAT API,再聲明Geronimo的JTA API實現。
排除依賴范例代碼如下:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>project-a</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>project-b</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>project-c</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>project-c</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies>
</project>
代碼中使用exclusions元素聲明排除依賴,exclusions可以包含一個或者多個exclusion子元素,因此可以排除一個或者多個傳遞性依賴。
歸類依賴:
很多時候,我們會使用到來自同一項目下的不同模塊,而且這些依賴的版本都是相同的。例如我們在使用spring framework時,分別引入的依賴為org.springframework:spring-core:2.5.6、org.springframework:spring-beans:2.5.6、org.springframework:spring-context:2.5.6、org.springframework:spring-support:2.5.6。如果將來需要升級Spring
Frame-work,這些依賴的版本會一起升級。
在Maven中可以使用歸類依賴,這樣可以避免重復,而且在修改值的時候,能夠降低錯誤發生的幾率。例子如下:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>account-email</artifactId>
<name>Account Email</name>
<version>1.0.0-SNAPSHOT</version>
<properties>
<springframework.version>2.5.6</springframework.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-support</artifactId>
<version>${springframework.version}</version>
</dependency>
</dependencies>
</project>
這里用到了Maven屬性(后面會詳細介紹Maven屬性),Maven運行的時候會將POM中的所有的${springframework.version}替換成實際值2.5.6。也就是說,可以使用美元符號和大括弧環繞的方式引用Maven屬性。然后,將所有Sping Framework依賴的版本值用這一屬性引用表示。
優化依賴:
Maven會自動解析所有項目的直接依賴和傳遞性依賴,并且根據規則正確判斷每個依賴的范圍,對于一些依賴沖突,也能進行調節,以確保任何一個構件只有唯一的版本在依賴中存在。在這些工作之后,最后得到的那些依賴被稱為已解析依賴。可以運行如下的命令查看當前項目的已解析依賴:
mvn dependency:list
在此基礎上,還能進一步了解已解析依賴的信息。將直接在當前項目POM聲明的依賴定義為頂層依賴,而這些頂層依賴的依賴則定義為第二層依賴,...當這些依賴經Maven解析后,就會構成一個依賴樹,通過這棵依賴樹就能很清楚地看到某個依賴是通過哪條路徑引入的。可以運行mvn dependency:tree來查看當前項目的依賴樹。
我們還可以使用mvn dependency:analyze來分析項目中的依賴。使用該工具可以得出兩類內容:
Used undeclared dependencies
意指項目中使用到的,但是沒有顯式聲明的依賴。這種依賴意為著潛在的風險,當前項目直接在使用它們,例如有很多相關Java import聲明,而這種依賴是通過直接依賴傳遞進來的,當升級直接依賴的時候,相關傳遞性依賴的版本可能發生改變,接口就可能發生改變,那么就會導致當前項目中的相關代碼無法編譯。因此,一般應該顯式聲明任何項目中直接用到的依賴。
Unused declared dependencies
意指項目中未使用的,但是顯式聲明的依賴。對于這一類依賴,我們應該認真的分析,由于mvn dependency:analyze只會分析編譯主代碼和測試代碼需要用到的依賴,一些執行測試盒運行時需要的依賴它發現不了。所以我們應該認真分析該依賴是否會被用到再決定對該依賴的取舍。
生活不易,碼農辛苦
如果您覺得本網站對您的學習有所幫助,可以手機掃描二維碼進行捐贈