6、座標和依賴

1、  座標詳解

Maven座標爲各種構件引入秩序,任何一個構件都必須明確定義自己的座標,而一組Maven座標是通過一些元素定義的,它們是groupId、artifactId、version、packaging、classifier。

groupId:定義當前Maven項目隸屬的實際項目。Maven項目和實際項目不一定是一對一的關係。比如SpringFramework這一實際項目,其對應的Maven項目會有很多,如spring-core、spring-context等。這是由於Maven中模塊的概念,因此,一個實際項目往往會被劃分成很多模塊。其實groupId不應該對應項目隸屬的組織或公司。原因很簡單,一個組織下會有很多實際項目,如果groupId只定義到組織級別,而後我們會看到,artifactId只能定義Maven項目(模塊),那麼實際項目這個層將難以定義。最後,groupId的表示方式與Java包名的表示方式類似,通常與域名反向一一對應。

artifactId:該元素定義實際項目中的一個Maven項目(模塊),推薦的做法是使用實際項目名作爲前綴,這樣做的好處是方便尋找實際構件。

version:該元素定義Maven項目當前所處的版本

packaging: 該元素定義Maven項目的打包方式。首先,打包方式通常與所生成構件的文件擴展名對應,如packging爲Jar,則生成.jar文件,而使用war打包方式的Maven項目,最終生成的構件會有一個.war文件,不過這不是絕對的。其次,打包方式會影響到構建的生命週期,比如jar打包和war打包會使用不同的命令。最後,當不定義packing的時候,Maven會使用默認值jar

classifier:該元素用來幫助定義構建輸出的一些附屬構件。如:項目有可能會通過使用插件生成一些附屬構件,例如javadoc和sources,這時候javadoc和sources就是這2個構件就是這兩個附屬構件的classfier。這樣,附屬構件就有了自己唯一的座標。注意,不能直接定義項目的classifier,因爲附屬構件不是項目直接默認生成的,而是由附加的插件幫助生成。

上述5個元素中,groupId、artifactId、version是必須定義的,packaging是可選的(默認爲jar),而classifier是不能直接定義的。

同時,項目構件的文件名是與座標相對應的,一般的規則爲artifactId-version[-classifier].packaging,[-classifier]表示可選。這裏還要強調一點:packaging並非一定與構件擴展名對應,比如packaging爲maven-plugin的構件,擴展名爲jar

2、  依賴的配置

前面羅列了一些簡單的依賴配置,其實一個依賴聲明可以包含如下的一些元素:

<project>

<dependencies>

           <dependency>

                    <groupId>…</groupId>

                    <artifactId>…</artifactId>

                    <version>…</version>

                    <type>…</type>

                    <scope>…</scope>

                    <optional>…</optional>

                    <exclusions>

                             <exclusion>

                                       …

                                       </exclusion>

                                       …

                             </exclusions>

                    </dependency>

                    …

           </dependencies>

           …

</project>

根元素project下的dependencies可以包含一個或者多個dependency元素,以聲明一個或者多個項目依賴。每個依賴可以包含的元素有:

groupId、artifactId和version:依賴的基本座標,對於任何一個依賴來說,基本座標是最重要的,Maven根據座標才能找到需要的依賴。

type:依賴的類型,對於項目座標定義的packaging。大部分情況下,該元素不必聲明,其默認值爲jar。

scope:依賴範圍。稍後會介紹。

optional:標記依賴是否可選。稍後會介紹。

exclusions:用來排除傳遞性依賴。稍後會介紹

3、  依賴範圍

依賴範圍就是用來控制與這三種classpath(編譯classpath、測試classpath、運行classpath)的關係,Maven有以下幾種依賴範圍:

compile:編譯依賴範圍。如果沒有指定,就會默認使用該依賴範圍。使用此依賴範圍的Maven依賴,對於編譯、測試、運行三種classpath都有效。例如:spring-core

test:測試依賴範圍。使用此依賴範圍的Maven依賴,只對於測試classpath有效,在編譯主代碼或者運行項目的使用時將無法使用此類依賴。例如:junit

provided:已提供依賴範圍。使用此依賴範圍的Maven依賴,對於編譯和測試classpath有效,但在運行時無效。例如:servlet-api

runtime:運行時依賴。使用此依賴範圍的Maven依賴,對於測試和運行classpath有效,但在編譯主代碼時無效。例如:Jdbc,編譯時只需要JDK提供的jdbc接口。

system:系統依賴範圍。該依賴與三種classpath的關係,和provided依賴範圍完全一致。但是,使用system範圍的依賴時必須通過systemPath元素顯式地指定依賴文件的路徑。由於此類依賴不是通過Maven倉庫解析的,而且往往與本機系統綁定,可能造成構建的不可移植,因此應該謹慎使用。systemPath元素可以引用環境變量,如:

<dependency>

           <groupId>javax.sql</groupId>

           <artifactId>jdbc-stdext</artifactId>

           <version>2.0</version>

           <scope>sytem</scope>

           <systemPath>${java.home}/lib/rt.jar</systemPath>

</dependency>

import(Maven2.0.9及以上):導入依賴範圍。該依賴範圍不會對三種classpath產生實際的影響,在稍後的章節中介紹Maven依賴和dependencyManagement的時候詳細介紹此依賴範圍。

上述除import以外的各種依賴範圍與三種classpath的關係列成表格如下

依賴範圍

(scope)

對於編譯classpath有效

對於測試classpath有效

對於運行時classpath有效

例子

compile

Y

Y

Y

spring-core

test

Y

JUnit

provied

Y

Y

servlet-api 

runtime

Y

Y

JDBC驅動實現

system

Y

Y

本地除Maven倉庫以外的類庫文件

4、  傳遞性依賴和依賴範圍

假設A依賴於B,B依賴於C,我們說A對於B是第一直接依賴,B對於C是第二直接依賴,A對於C是傳遞性依賴。第一直接依賴和第二直接依賴的範圍決定了傳遞性依賴的範圍,如下表,最左邊一列表示第一直接依賴範圍,最上面一行表示第二直接依賴範圍,中間的交叉單元格則表示傳遞性依賴的範圍。

 

compile

test

provided

runtime

compile

compile

runtime

test

test

test

provided

provided

provided

provided

runtime

runtime

runtime

從表中可以發現這樣的規律:當第二直接依賴的範圍是compile時,傳遞性依賴的範圍與第一直接依賴的範圍一致;當第二直接依賴的範圍是test時,依賴不會得以傳遞;當第二直接依賴的範圍是provided時,只第一直接傳遞依賴範圍也爲provied的依賴,且傳遞性依賴的範圍同樣爲provied;當第二直接依賴範圍是runtime時,傳遞性依賴與第一直接依賴的範圍一致,但是compile例外,此時傳遞性依賴的範圍爲runtime。

5、  依賴調解

Maven引入的傳遞性依賴的機制,一方面大大簡化和方便了依賴聲明,另一方面,大部分情況下我們只需要關心項目的直接依賴是什麼,而不用考慮這些直接依賴會引入什麼傳遞性依賴。但有時候,當傳遞性依賴造成問題的時候,我們需要清楚地知道該傳遞性依賴是從哪條依賴路徑引入的。

例如,項目A有這樣的依賴關係:A->B->C->X(1.0)、A->D->X(2.0),X是A的傳遞性依賴,但是這兩條依賴路徑上有兩個版本的X,那麼哪個X會被Maven解析使用呢?兩個版本都被解析顯然是不對的,因爲那會造成依賴重複,因此必須選擇一個。Maven依賴調解(Dependenc Mediation)的第一原則是:路徑最近優先。但是路徑長度一樣怎麼辦?從Maven2.0.9開始,爲了儘可能避免構建的不確定性,Maven定義了依賴調解的第二原則:第一聲明者優先。在依賴路徑長度相等的前提下,在POM中依賴聲明的順序決定了誰會被解析使用。

6、  可選依賴

假設有這樣一個依賴關係,項目A依賴於項目B,項目B依賴於項目X和Y,B對於X和Y的依賴都是可選依賴:A->B、B->X(可選)、B->Y(可選)。根據傳遞性依賴的定義,如果所有這三個依賴的範圍都是compile,那麼X和Y就是A的compile範圍傳遞性依賴。然而,由於這裏X、Y是可選依賴,依賴將不會得以傳遞。

爲什麼要使用可選依賴這一特性呢?可能項目B實現了兩個特性,其中特性一依賴於X,特性二依賴於Y,而且這兩個特性是互斥的,用戶不可能同時使用兩個特性。比如B是一個持久層隔離工具包,它支持多種數據庫,包括MySQL、PostgreSQL等,在構建這個工具包的時候,需要這兩種數據庫的驅動程序,但是在使用這個工具包的時候,只會依賴一種數據庫。其中可選依賴的標識使用<option>元素表示。由於此時依賴不會傳遞,所以當項目A依賴於項目B的時候,如果實際使用基於MySQL數據庫,那麼在項目A中就需要顯示地聲明MySQL對應的依賴。

最後,關於可選依賴需要說明的一點是,在理想的情況下,是不應該使用可選依賴的。前面所講的情況是因爲某個項目實現了多個特性,在面向對象設計中,有個單一職責性原則,意指一個類應該只有一項職責,而不是糅合太多的功能。這個原則在規劃Maven項目的時候也同樣適用。上面的情況更好的做法是爲MySQL和PostgreSQL分別創建一個Maven項目,基於同樣的groupId分配不同的artifactId。

7、  最佳實踐

A、 排除依賴

傳遞性依賴會給項目隱式地引入很多依賴,這極大地簡化了項目依賴的管理,但是不是有些時候這種特性也會帶來問題。例如,當前項目有一個第三方依賴,而這個第三方依賴由於某些原因依賴了另一個類庫的SNAPSHOT版本,那個這個SNAPSHOT就會成爲當前項目的傳遞性依賴,而SNAPSHOT的不穩定性會直接影響到當前的項目。這時就需要排除掉該SNAPSHOT,並且當前項目中聲明該類庫的某個正式發佈的版本。還有一些情況,你可以也想要替換某個傳遞性依賴,如果Sun JTA API,Hibernate依賴於這個JAR,但是由於版權的因素,該類庫不在中央倉庫中,而Apache Geronimo項目有一個對應的實現。這時你就可以排除Sun JTA API,再聲明Geronimo的JTA API實現。

排除依賴使用元素,看如下結構,其中<exclusions> 位於<dependency>元素中

<exclusions>

<exclusion>

         <groupId>****</groupId>

         <artifactId>***</artifactId>

</exclusion>

</exclusions>

需要注意的是,聲明exclusion的時候只需要groupId和artifactId,而不需要version元素,這是因爲只需要groupId和artifactId就能唯一定位依賴圖中的某個依賴。換句話說,Maven解析後的依賴中,不可能出現groupId和artifactId相同,但是版本不同的兩個依賴。

B、 歸類依賴

有很多關於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-context-support:2.5.6,它們是來自同一個項目的不同模塊。因此,所有這些依賴的版本都是相同的,而且可以預見,如果將來需要升級Spring Framework,這些依賴的版本會一起升級。

<dependency>

         <groupId>org.springframework</groupId>

         <artifactId>spring-core</artifactId>

         <version>${springframework.version}</version>

 </dependency>

這裏用到了Maven屬性(在後面的章節中會詳細介紹),首先使用properties元素定義Maven屬性,有了這個屬性後,Maven運行時會將POM中所有${springframework.version}替換成實際的版本2.5.6

C、 優化依賴

Maven會自動解析所有項目的直接依賴和傳遞性依賴,並且根據規則正確判斷每個依賴的範圍,對於一些依賴衝突,也能進行調節,以確保任何一個構件只有唯一的版本在依賴中存在。在這些工作之後,最後得到的那些依賴被稱爲已解析依賴(Resolved Dependency)。可以運行如下的命令查看當前項目的已解析依賴(要在項目的根目錄中執行):

mvn dependency:list


圖片中對應的pom代碼爲:

<dependencies>

    <dependency>

      <groupId>junit</groupId>

      <artifactId>junit</artifactId>

      <version>3.8.1</version>

      <scope>test</scope>

    </dependency>

    <dependency>

      <groupId>org.springframework</groupId>

      <artifactId>spring-core</artifactId>

      <version>2.5.6</version>

    </dependency>

  </dependencies>

在此基礎上,還能進一步瞭解已解析依賴的信息。將直接在當前項目POM聲明的依賴定義爲頂層依賴,而這些頂層依賴的依賴則定義爲第二層依賴,以此類推,有第三、第四層依賴。當這些依賴經Maven解析後,就會構成一個依賴樹,通過這棵依賴樹就能很清楚地看到某個依賴是通過哪條路徑引入的。可以運行如下命令查看當前項目的依賴樹:mvn dependency:tree,效果如下:



從圖中能夠看到,雖然我們沒有聲明commons-logging: commons-logging.jar:1.1.1這一依賴,但它還是經過org.springframework:spring-core:2.5.6成爲了當前項目的傳遞性依賴。

另一個命令:mvn dependency:analyze

運行後的結果中重要的是兩個部分。

首先是Used undeclared dependencies,意指項目中使用到的,但是沒有顯式聲明的依賴,這裏是spring-context。這種依賴意味着潛在的風險,當前項目直接在使用它們,例如有很多相關的Java import聲明,而這種依賴是通過直接依賴傳遞進來的,當升級直接依賴時,相關傳遞性依賴的版本也會發生變化,這種變化不易察覺,但是有可能導致當前項目出錯。例如由於接口的改變,當前項目中的相關代碼無法編譯。這種隱藏的、潛在的威脅一旦出現,就往往需要耗費大量的時候來查明真相。因此,顯式聲明任何項目中直接用到的依賴。

結果中還有一個重要的部分是Unused declared dependencies,意指項目中未使用的,但顯式聲明的依賴。需要注意的是,對於這樣一類依賴,我們不應該簡單地直接刪除其聲明,而是應該分析。由於dependency:analyze只會分析編譯主代碼和測試代碼需要用到的依賴,一些執行測試和運行時需要的依賴它就發現不了,所以在刪除這種聲明的時候一定要謹慎。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章