Maven坐標和依賴的實現(xiàn)示例
Maven的一大功能是管理項目依賴。為了能自動化地解析任何一個Java構(gòu)件,Maven就必須將它們唯一標識,這就依賴管理的底層基礎(chǔ)——坐標。將詳細分析Maven坐標的作用,解釋其每一個元素;在此基礎(chǔ)上,再介紹如何配置Maven,以及相關(guān)的經(jīng)驗和技巧,以幫助我們管理項目依賴。
1、何為Maven坐標
關(guān)于坐標(Coordinate),大家最熟悉的定義應(yīng)該來自于平面幾何。在一個平面坐標系中,坐標(x,y)表示該平面上與x軸距離為y,與y軸距離為x的一點,任何一個坐標都能夠唯一標識該平面中的一點。
在實際生活中,我們也可以將地址看成是一種坐標。省、市、區(qū)、街道等一系列信息同樣可以唯一標識城市中的任一居住地址和工作地址。郵局和快遞公司正是基于這樣一種坐標進行日常工作的。
對應(yīng)于平面中的點和城市中的地址,Maven的世界中擁有數(shù)量非常巨大的構(gòu)件,也就是平時用的一些jar、war等文件。在Maven為這些構(gòu)件引入坐標概念之前,我們無法使用任何一種方式來唯一標識所有這些構(gòu)件。因此,當需要用到Spring Framework依賴的時候,大家會去Spring Framework網(wǎng)站尋找,當需要用到log4j依賴的時候,大家又會去Apache網(wǎng)站尋找。又因為各個項目的網(wǎng)站風(fēng)格迥異,大量的時間花費在了搜索、瀏覽網(wǎng)頁等工作上面。沒有統(tǒng)一的規(guī)范、統(tǒng)一的法則,該工作就無法自動化。重復(fù)地搜索、瀏覽網(wǎng)頁和下載類似的jar文件,這本就應(yīng)該交給機器來做。而機器工作必須基于預(yù)定義的規(guī)則,Maven定義了這樣一組規(guī)則:世界上任何一個構(gòu)件都可以使用Maven坐標唯一標識,Maven坐標的元素包括groupId、artifactId、version、packaging、classifier?,F(xiàn)在,只要我們提供正確的坐標元素,Maven就能找到對應(yīng)的構(gòu)件。比如說,當需要使用Java5平臺上TestNG的5.8版本時,就告訴Maven:“groupId=org.testng;artifactId=testng;version=5.8;classifier=jdk15”,Maven就會從倉庫中尋找相應(yīng)的構(gòu)件供我們使用。也許你會奇怪,“Maven是從哪里下載構(gòu)件的呢?”答案其實很簡單,Maven內(nèi)置了一個中央倉庫的地址(http://repo1.maven.org/maven2),該中央倉庫包含了世界上大部分流行的開源項目構(gòu)件,Maven會在需要的時候去那里下載。
在我們開發(fā)自己項目的時候,也需要為其定義適當?shù)淖鴺?,這是Maven強制要求的。在這個基礎(chǔ)上,其他Maven項目才能引用該項目生成的構(gòu)件,見下圖:
2、坐標詳解
Maven坐標為各種構(gòu)件引入了秩序,任何一個構(gòu)件都必須明確定義自己的坐標,而一組Maven坐標是通過一些元素定義的,它們是groupId、artifactId、version、packaging、classifier。先看一組坐標定義,如下:
<groupId>org.sonatype.nexus</groupId> <artifactId>nexus-indexer</artifactId> <version>2.0.0</version> <packaging>jar</packaging>
這是nexus-indexer的坐標定義,nexus-indexer是一個對Maven倉庫編纂索引并提供搜索功能的類庫,它是Nexus項目的一個子模塊。后面會詳細介紹Nexus。上述代碼片段中,其坐標分別為groupId:org.sonatype.nexus、artifactId:nexus-indexer、version:2.0.0、packaging:jar,沒有classifier。下面詳細解釋一下各個坐標元素:
這是nexus-indexer的坐標定義,nexus-indexer是一個對Maven倉庫編纂索引并提供搜索功能的類庫,它是Nexus項目的一個子模塊。上述代碼片段中,其坐標分別為groupId:org.sonatype.nexus、artifactId:nexus-indexer、version:2.0.0、packaging:jar,沒有classifier。下面詳細解釋一下各個坐標元素:
- groupId:定義當前Maven項目隸屬的實際項目。首先,Maven項目和實際項目不一定是一對一的關(guān)系。比如SpringFramework這一實際項目,其對應(yīng)的Maven項目會有很多,如spring-core、spring-context等。這是由于Maven中模塊的概念,因此,一個實際項目往往會被劃分成很多模塊。其次,groupId不應(yīng)該對應(yīng)項目隸屬的組織或公司。原因很簡單,一個組織下會有很多實際項目,如果groupId只定義到組織級別,而后面我們會看到,artifactId只能定義Maven項目(模塊),那么實際項目這個層將難以定義。最后,groupId的表示方式與Java包名的表示方式類似,通常與域名反向一一對應(yīng)。上例中,groupId為org.sonatype.nexus,org.sonatype表示Sonatype公司建立的一個非盈利性組織,nexus表示Nexus這一實際項目,該groupId與域名nexus.sonatype.org對應(yīng)。
- artifactId:該元素定義實際項目中的一個Maven項目(模塊),推薦的做法是使用實際項目名稱作為artifactId的前綴。比如上例中的artifactId是nexus-indexer,使用了實際項目名nexus作為前綴,這樣做的好處是方便尋找實際構(gòu)件。在默認情況下,Maven生成的構(gòu)件,其文件名會以artifactId作為開頭,如nexus-indexer-2.0.0.jar,使用實際項目名稱作為前綴之后,就能方便從一個lib文件夾中找到某個項目的一組構(gòu)件??紤]有5個項目,每個項目都有一個core模塊,如果沒有前綴,我們會看到很多core-1.2.jar這樣的文件,加上實際項目名前綴之后,便能很容易區(qū)分foo-core-1.2.jar、bar-core-1.2.jar……
- version:該元素定義Maven項目當前所處的版本,如上例中nexus-indexer的版本是2.0.0。需要注意的是,Maven定義了一套完整的版本規(guī)范,以及快照(SNAPSHOT)的概念。
- packaging:該元素定義Maven項目的打包方式。首先,打包方式通常與所生成構(gòu)件的文件擴展名對應(yīng),如上例中packaging為jar,最終的文件名為nexus-indexer-2.0.0.jar,而使用war打包方式的Maven項目,最終生成的構(gòu)件會有一個.war文件,不過這不是絕對的。其次,打包方式會影響到構(gòu)建的生命周期,比如jar打包和war打包會使用不同的命令。最后,當不定義packaging的時候,Maven會使用默認值jar。
- classifier:該元素用來幫助定義構(gòu)建輸出的一些附屬構(gòu)件。附屬構(gòu)件與主構(gòu)件對應(yīng),如上例中的主構(gòu)件是nexus-indexer-2.0.0.jar,該項目可能還會通過使用一些插件生成如nexus-indexer-2.0.0-javadoc.jar、nexus-indexer-2.0.0-sources.jar這樣一些附屬構(gòu)件,其包含了Java文檔和源代碼。這時候,javadoc和sources就是這兩個附屬構(gòu)件的classifier。這樣,附屬構(gòu)件也就擁有了自己唯一的坐標。還有一個關(guān)于classifier的典型例子是TestNG,TestNG的主構(gòu)件是基于Java 1.4平臺的,而它又提供了一個classifier為jdk5的附屬構(gòu)件。注意,不能直接定義項目的classifier,因為附屬構(gòu)件不是項目直接默認生成的,而是由附加的插件幫助生成。
上述5個元素中,groupId、artifactId、version是必須定義的,packaging是可選的(默認為jar),而classifier是不能直接定義的。
同時,項目構(gòu)件的文件名是與坐標相對應(yīng)的,一般的規(guī)則為artifactId-version[-classifier].packaging,[-classifier]表示可選。比如上例nexus-indexer的主構(gòu)件為nexus-indexer-2.0.0.jar,附屬構(gòu)件有nexus-indexer-2.0.0-javadoc.jar。這里還要強調(diào)的一點是,packaging并非一定與構(gòu)件擴展名對應(yīng),比如packaging為maven-plugin的構(gòu)件擴展名為jar。此外,Maven倉庫的布局也是基于Maven坐標。
理解清楚城市中地址的定義方式后,郵遞員就能夠開始工作了;同樣地,理解清楚Maven坐標之后,我們就能開始討論Maven的依賴管理了。
3、解析一個Maven項目的POM
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.juvenxu.mvnbook.account</groupId> <artifactId>account-email</artifactId> <name>Account Email</name> <version>1.0.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>2.5.6</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>2.5.6</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>2.5.6</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>2.5.6</version> </dependency> <dependency> <groupId>javax.mail</groupId> <artifactId>mail</artifactId> <version>1.4.1</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.7</version> <scope>test</scope> </dependency> <dependency> <groupId>com.icegreen</groupId> <artifactId>greenmail</artifactId> <version>1.3.1b</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.5</source> <target>1.5</target> </configuration> </plugin> </plugins> </build> </project>
先觀察該項目模塊的坐標,groupId:com.juvenxu.mvnbook.account;artifactId:account-email;version:1.0.0-SNAPSHOT。由于該模塊屬于賬戶注冊服務(wù)項目的一部分,因此,其groupId對應(yīng)了account項目。緊接著,該模塊的artifactId仍然以account作為前綴,以方便區(qū)分其他項目的構(gòu)建。最后,1.0.0-SNAPSHOT表示該版本處于開發(fā)中,還不穩(wěn)定。
再看dependencies元素,其包含了多個dependency子元素,這是POM中定義項目依賴的位置。以第一個依賴為例,其groupId:artifactId:version為org.springframework:spring-core:2.5.6,這便是依賴的坐標,任何一個Maven項目都需要定義自己的坐標,當這個Maven項目成為其他Maven項目的依賴的時候,這組坐標就體現(xiàn)了其價值。本例中的spring-core,以及后面的spring-beans、spring-context、spring-context-support是Spring Framework實現(xiàn)依賴注入等功能必要的構(gòu)件,由于我們的關(guān)注點在于Maven,只會涉及簡單的Spring Framework的使用,不會詳細解釋Spring Framework的用法,如果大家有不清楚的地方,請參閱Spring Framework相關(guān)的文檔。
在spring-context-support之后,有一個依賴為javax.mail:mail:1.4.1,這是實現(xiàn)發(fā)送必須的類庫。
緊接著的依賴為junit:junit:4.7,JUnit是Java社區(qū)事實上的單元測試標準,詳細信息請參閱http://www.junit.org/,這個依賴特殊的地方在于一個值為test的scope子元素,scope用來定義依賴范圍。這里我們暫時只需要了解當依賴范圍是test的時候,該依賴只會被加入到測試代碼的classpath中。也就是說,對于項目主代碼,該依賴是沒有任何作用的。JUnit是單元測試框架,只有在測試的時候才需要,因此使用該依賴范圍。
隨后的依賴是com.icegreen:greenmail:1.3.1b,其依賴范圍同樣為test。這時也許你已經(jīng)猜到,該依賴同樣只服務(wù)于測試目的,GreenMail是開源的郵件服務(wù)測試套件,account-email模塊使用該套件來測試郵件的發(fā)送。關(guān)于GreenMail的詳細信息可訪問http://www.icegreen.com/greenmail/。
最后,POM中有一段關(guān)于maven-compiler-plugin的配置,其目的是開啟Java 5的支持。
3.1、構(gòu)建項目
使用mvn clean install構(gòu)建account-email,Maven會根據(jù)POM配置自動下載所需要的依賴構(gòu)件,執(zhí)行編譯、測試、打包等工作,最后將項目生成的構(gòu)件account-email-1.0.0-SNAP-SHOT.jar安裝到本地倉庫中。這時,該模塊就能供其他Maven項目使用了。
4、依賴的配置
依賴會有基本的groupId、arti-factId和version等元素組成。其實一個依賴聲明可以包含如下的一些元素:
<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根據(jù)坐標才能找到需要的依賴。
- type:依賴的類型,對應(yīng)于項目坐標定義的packaging。大部分情況下,該元素不必聲明,其默認值為jar。
- scope:依賴的范圍。
- optional:標記依賴是否可選。
- exclusions:用來排除傳遞性依賴。
大部分依賴聲明只包含基本坐標,然而在一些特殊情況下,其他元素至關(guān)重要。
5、依賴范圍
上一節(jié)提到,JUnit依賴的測試范圍是test,測試范圍用元素scope表示。本節(jié)將詳細解釋什么是測試范圍,以及各種測試范圍的效果和用途。
首先需要知道,Maven在編譯項目主代碼的時候需要使用一套classpath。在上例中,編譯項目主代碼的時候需要用到spring-core,該文件以依賴的方式被引入到classpath中。其次,Maven在編譯和執(zhí)行測試的時候會使用另外一套classpath。上例中的JUnit就是一個很好的例子,該文件也以依賴的方式引入到測試使用的classpath中,不同的是這里的依賴范圍是test。最后,實際運行Maven項目的時候,又會使用一套classpath,上例中的spring-core需要在該classpath中,而JUnit則不需要。
依賴范圍就是用來控制依賴與這三種classpath(編譯classpath、測試classpath、運行classpath)的關(guān)系,Maven有以下幾種依賴范圍:
- compile:編譯依賴范圍。如果沒有指定,就會默認使用該依賴范圍。使用此依賴范圍的Maven依賴,對于編譯、測試、運行三種classpath都有效。典型的例子是spring-core,在編譯、測試和運行的時候都需要使用該依賴。
- test:測試依賴范圍。使用此依賴范圍的Maven依賴,只對于測試classpath有效,在編譯主代碼或者運行項目的使用時將無法使用此類依賴。典型的例子是JUnit,它只有在編譯測試代碼及運行測試的時候才需要。
- test:測試依賴范圍。使用此依賴范圍的Maven依賴,只對于測試classpath有效,在編譯主代碼或者運行項目的使用時將無法使用此類依賴。典型的例子是JUnit,它只有在編譯測試代碼及運行測試的時候才需要。
- provided:已提供依賴范圍。使用此依賴范圍的Maven依賴,對于編譯和測試class-path有效,但在運行時無效。典型的例子是servlet-api,編譯和測試項目的時候需要該依賴,但在運行項目的時候,由于容器已經(jīng)提供,就不需要Maven重復(fù)地引入一遍。
- runtime:運行時依賴范圍。使用此依賴范圍的Maven依賴,對于測試和運行class-path有效,但在編譯主代碼時無效。典型的例子是JDBC驅(qū)動實現(xiàn),項目主代碼的編譯只需要JDK提供的JDBC接口,只有在執(zhí)行測試或者運行項目的時候才需要實現(xiàn)上述接口的具體JDBC驅(qū)動。
- system:系統(tǒng)依賴范圍。該依賴與三種classpath的關(guān)系,和provided依賴范圍完全一致。但是,使用system范圍的依賴時必須通過systemPath元素顯式地指定依賴文件的路徑。由于此類依賴不是通過Maven倉庫解析的,而且往往與本機系統(tǒng)綁定,可能造成構(gòu)建的不可移植,因此應(yīng)該謹慎使用。systemPath元素可以引用環(huán)境變量,如:
<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(Maven 2.0.9及以上):導(dǎo)入依賴范圍。該依賴范圍不會對三種classpath產(chǎn)生實際的影響,后面將介紹Maven依賴和dependencyManagement的時候詳細介紹此依賴范圍。
上述除import以外的各種依賴范圍與三種classpath的關(guān)系如下表所示:
6、傳遞性依賴
6.1、何為傳遞性依賴
考慮一個基于Spring Framework的項目,如果不使用Maven,那么在項目中就需要手動下載相關(guān)依賴。由于Spring Framework又會依賴于其他開源類庫,因此實際中往往會下載一個很大的如spring-framework-2.5.6-with-dependencies.zip的包,這里包含了所有Spring Framework的jar包,以及所有它依賴的其他jar包。這么做往往就引入了很多不必要的依賴。另一種做法是只下載spring-framework-2.5.6.zip這樣一個包,這里不包含其他相關(guān)依賴,到實際使用的時候,再根據(jù)出錯信息,或者查詢相關(guān)文檔,加入需要的其他依賴。很顯然,這也是一件非常麻煩的事情。
Maven的傳遞性依賴機制可以很好地解決這一問題。以account-email項目為例,該項目有一個org.springframework:spring-core:2.5.6的依賴,而實際上spring-core也有它自己的依賴,我們可以直接訪問位于中央倉庫的該構(gòu)件的POM:http://repo1.maven.org/maven2/org/springframework/spring-core/2.5.6/spring-core-2.5.6.pom。該文件包含了一個commons-logging依賴,代碼清單如下所示:
<dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.1</version> </dependency>
該依賴沒有聲明依賴范圍,那么其依賴范圍就是默認的compile。同時回顧一下account-email,spring-core的依賴范圍也是compile。
account-mail有一個compile范圍的spring-core依賴,spring-core有一個compile范圍的commons-logging依賴,那么commons-logging就會成為account-email的compile范圍依賴,commons-logging是account-email的一個傳遞性依賴,如下圖所示:
有了傳遞性依賴機制,在使用Spring Framework的時候就不用去考慮它依賴了什么,也不用擔(dān)心引入多余的依賴。Maven會解析各個直接依賴的POM,將那些必要的間接依賴,以傳遞性依賴的形式引入到當前的項目中。
6.2、傳遞性依賴和依賴范圍
依賴范圍不僅可以控制依賴與三種classpath的關(guān)系,還對傳遞性依賴產(chǎn)生影響。上面的例子中,account-email對于spring-core的依賴范圍是compile,spring-core對于commons-logging的依賴范圍是compile,那么account-email對于commons-logging這一傳遞性依賴的范圍也就是compile。假設(shè)A依賴于B,B依賴于C,我們說A對于B是第一直接依賴,B對于C是第二直接依賴,A對于C是傳遞性依賴。第一直接依賴的范圍和第二直接依賴的范圍決定了傳遞性依賴的范圍,如下表所示,最左邊一列表示第一直接依賴范圍,最上面一行表示第二直接依賴范圍,中間的交叉單元格則表示傳遞性依賴范圍:
為了能夠幫助讀者更好地理解上表,這里再舉個例子。account-email項目有一個com.icegreen:greenmail:1.3.1b的直接依賴,我們說這是第一直接依賴,其依賴范圍是test;而greenmail又有一個javax.mail:mail:1.4的直接依賴,我們說這是第二直接依賴,其依賴范圍是compile。顯然javax.mail:mail:1.4是account-email的傳遞性依賴,對照上表可以知道,當?shù)谝恢苯右蕾嚪秶鸀閠est,第二直接依賴范圍是compile的時候,傳遞性依賴的范圍是test,因此javax.mail:mail:1.4是account-email的一個范圍是test的傳遞性依賴。
仔細觀察一下上表,可以發(fā)現(xiàn)這樣的規(guī)律:當?shù)诙苯右蕾嚨姆秶莄ompile的時候,傳遞性依賴的范圍與第一直接依賴的范圍一致;當?shù)诙苯右蕾嚨姆秶莟est的時候,依賴不會得以傳遞;當?shù)诙苯右蕾嚨姆秶莗rovided的時候,只傳遞第一直接依賴范圍也為provided的依賴,且傳遞性依賴的范圍同樣為provided;當?shù)诙苯右蕾嚨姆秶莚untime的時候,傳遞性依賴的范圍與第一直接依賴的范圍一致,但compile例外,此時傳遞性依賴的范圍為runtime。
7、依賴調(diào)解
Maven引入的傳遞性依賴機制,一方面大大簡化和方便了依賴聲明,另一方面,大部分情況下我們只需要關(guān)心項目的直接依賴是什么,而不用考慮這些直接依賴會引入什么傳遞性依賴。但有時候,當傳遞性依賴造成問題的時候,我們就需要清楚地知道該傳遞性依賴是從哪條依賴路徑引入的。
Maven引入的傳遞性依賴機制,一方面大大簡化和方便了依賴聲明,另一方面,大部分情況下我們只需要關(guān)心項目的直接依賴是什么,而不用考慮這些直接依賴會引入什么傳遞性依賴。但有時候,當傳遞性依賴造成問題的時候,我們就需要清楚地知道該傳遞性依賴是從哪條依賴路徑引入的。
依賴調(diào)解第一原則不能解決所有問題,比如這樣的依賴關(guān)系:A->B->Y(1.0)、A->C->Y(2.0),Y(1.0)和Y(2.0)的依賴路徑長度是一樣的,都為2。那么到底誰會被解析使用呢?在Maven 2.0.8及之前的版本中,這是不確定的,但是從Maven 2.0.9開始,為了盡可能避免構(gòu)建的不確定性,Maven定義了依賴調(diào)解的第二原則:第一聲明者優(yōu)先。在依賴路徑長度相等的前提下,在POM中依賴聲明的順序決定了誰會被解析使用,順序最靠前的那個依賴優(yōu)勝。該例中,如果B的依賴聲明在C之前,那么Y(1.0)就會被解析使用。
8、可選依賴
假設(shè)有這樣一個依賴關(guān)系,項目A依賴于項目B,項目B依賴于項目X和Y,B對于X和Y的依賴都是可選依賴:A->B、B->X(可選)、B->Y(可選)。根據(jù)傳遞性依賴的定義,如果所有這三個依賴的范圍都是compile,那么X、Y就是A的compile范圍傳遞性依賴。然而,由于這里X、Y是可選依賴,依賴將不會得以傳遞。換句話說,X、Y將不會對A有任何影響,如下圖所示:
為什么要使用可選依賴這一特性呢?可能項目B實現(xiàn)了兩個特性,其中的特性一依賴于X,特性二依賴于Y,而且這兩個特性是互斥的,用戶不可能同時使用兩個特性。比如B是一個持久層隔離工具包,它支持多種數(shù)據(jù)庫,包括MySQL、PostgreSQL等,在構(gòu)建這個工具包的時候,需要這兩種數(shù)據(jù)庫的驅(qū)動程序,但在使用這個工具包的時候,只會依賴一種數(shù)據(jù)庫。
項目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>
上述XML代碼片段中,使用<optional>元素表示mysql-connector-java和postgresql這兩個依賴為可選依賴,它們只會對當前項目B產(chǎn)生影響,當其他項目依賴于B的時候,這兩個依賴不會被傳遞。因此,當項目A依賴于項目B的時候,如果其實際使用基于MySQL數(shù)據(jù)庫,那么在項目A中就需要顯式地聲明mysql-connector-java這一依賴,見代碼如下所示:
<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>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.10</version> </dependency> </dependencies> </project>
最后,關(guān)于可選依賴需要說明的一點是,在理想的情況下,是不應(yīng)該使用可選依賴的。前面我們可以看到,使用可選依賴的原因是某一個項目實現(xiàn)了多個特性,在面向?qū)ο笤O(shè)計中,有個單一職責(zé)性原則,意指一個類應(yīng)該只有一項職責(zé),而不是糅合太多的功能。這個原則在規(guī)劃Maven項目的時候也同樣適用。在上面的例子中,更好的做法是為MySQL和PostgreSQL分別創(chuàng)建一個Maven項目,基于同樣的groupId分配不同的artifactId,如com.juvenxu.mvnbook:project-b-mysql和com.juvenxu.mvnbook:project-b-postgresql,在各自的POM中聲明對應(yīng)的JDBC驅(qū)動依賴,而且不使用可選依賴,用戶則根據(jù)需要選擇使用project-b-mysql或者project-b-postgresql。由于傳遞性依賴的作用,就不用再聲明JDBC驅(qū)動依賴。
9、最佳實踐
Maven依賴涉及的知識點比較多,在理解了主要的功能和原理之后,最需要的當然就是前人的經(jīng)驗總結(jié)了,我們稱之為最佳實踐。本小節(jié)歸納了一些使用Maven依賴常見的技巧,方便用來避免和處理很多常見的問題。
9.1、排除依賴
傳遞性依賴會給項目隱式地引入很多依賴,這極大地簡化了項目依賴的管理,但是有些時候這種特性也會帶來問題。例如,當前項目有一個第三方依賴,而這個第三方依賴由于某些原因依賴了另外一個類庫的SNAPSHOT版本,那么這個SNAPSHOT就會成為當前項目的傳遞性依賴,而SNAPSHOT的不穩(wěn)定性會直接影響到當前的項目。這時就需要排除掉該SNAPSHOT,并且在當前項目中聲明該類庫的某個正式發(fā)布的版本。還有一些情況,你可能也想要替換某個傳遞性依賴,比如Sun JTA API,Hibernate依賴于這個JAR,但是由于版權(quán)的因素,該類庫不在中央倉庫中,而Apache Geronimo項目有一個對應(yīng)的實現(xiàn)。這時你就可以排除Sun JAT API,再聲明Geronimo的JTA API實現(xiàn),見代碼如下所示:
<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>
上述代碼中,項目A依賴于項目B,但是由于一些原因,不想引入傳遞性依賴C,而是自己顯式地聲明對于項目C 1.1.0版本的依賴。代碼中使用exclusions元素聲明排除依賴,exclusions可以包含一個或者多個exclusion子元素,因此可以排除一個或者多個傳遞性依賴。需要注意的是,聲明exclusion的時候只需要groupId和artifactId,而不需要version元素,這是因為只需要groupId和artifactId就能唯一定位依賴圖中的某個依賴。換句話說,Maven解析后的依賴中,不可能出現(xiàn)groupId和artifactId相同,但是version不同的兩個依賴。該例的依賴解析邏輯如下圖所示:
9.2、歸類依賴
,有很多關(guān)于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,它們是來自同一項目的不同模塊。因此,所有這些依賴的版本都是相同的,而且可以預(yù)見,如果將來需要升級Spring Frame-work,這些依賴的版本會一起升級。
對于account-email中這些Spring Framework來說,也應(yīng)該在一個唯一的地方定義版本,并且在dependency聲明中引用這一版本。這樣,在升級Spring Framework的時候就只需要修改一處,實現(xiàn)方式見代碼清單如下:
<project> <modelVersion>4.0.0</modelVersion> <groupId>com.juven.mvnbook.account</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-context-support</artifactId> <version>${springframework.version}</version> </dependency> </dependencies> </project>
這里簡單用到了Maven屬性,首先使用properties元素定義Maven屬性,該例中定義了一個springframework.version子元素,其值為2.5.6。有了這個屬性定義之后,Maven運行的時候會將POM中的所有的${springframework.version}替換成實際值2.5.6。也就是說,可以使用美元符號和大括弧環(huán)繞的方式引用Maven屬性。然后,將所有Spring Framework依賴的版本值用這一屬性引用表示。
9.3、優(yōu)化依賴
在軟件開發(fā)過程中,程序員會通過重構(gòu)等方式不斷地優(yōu)化自己的代碼,使其變得更簡潔、更靈活。同理,程序員也應(yīng)該能夠?qū)aven項目的依賴了然于胸,并對其進行優(yōu)化,如去除多余的依賴,顯式地聲明某些必要的依賴。
通過閱讀前面的內(nèi)容,我們應(yīng)該能夠了解到:Maven會自動解析所有項目的直接依賴和傳遞性依賴,并且根據(jù)規(guī)則正確判斷每個依賴的范圍,對于一些依賴沖突,也能進行調(diào)節(jié),以確保任何一個構(gòu)件只有唯一的版本在依賴中存在。在這些工作之后,最后得到的那些依賴被稱為已解析依賴(Resolved Dependency)??梢赃\行如下的命令查看當前項目的已解析依賴:
mvn dependency:list
上圖顯示了所有shardingsphere-5.3.0的已解析依賴,同時,每個依賴的范圍也得以明確標示。
在此基礎(chǔ)上,還能進一步了解已解析依賴的信息。將直接在當前項目POM聲明的依賴定義為頂層依賴,而這些頂層依賴的依賴則定義為第二層依賴,以此類推,有第三、第四層依賴。當這些依賴經(jīng)Maven解析后,就會構(gòu)成一個依賴樹,通過這棵依賴樹就能很清楚地看到某個依賴是通過哪條傳遞路徑引入的。可以運行如下命令查看當前項目的依賴樹:
mvn dependency:tree
從上圖中能夠看到,雖然我們沒有聲明org.slf4j:slf4japi:1.3這一依賴,但它還是經(jīng)過com.icegreen:greenmail:1.3成為了當前項目的傳遞性依賴,而且其范圍是test。
使用dependency:list和dependency:tree可以幫助我們詳細了解項目中所有依賴的具體信息,在此基礎(chǔ)上,還有dependency:analyze工具可以幫助分析當前項目的依賴。
為了說明該工具的用途,先將spring-context這一依賴刪除,然后構(gòu)建項目,你會發(fā)現(xiàn)編譯、測試和打包都不會有任何問題。通過分析依賴樹,可以看到spring-context是spring-context-support的依賴,因此會得以傳遞到項目的classspath中?,F(xiàn)在再運行如下命令:
mvn dependency:analyze
該結(jié)果中重要的是兩個部分。首先是Used undeclared dependencies,意指項目中使用到的,但是沒有顯式聲明的依賴,這里是spring-context。這種依賴意味著潛在的風(fēng)險,當前項目直接在使用它們,例如有很多相關(guān)的Java import聲明,而這種依賴是通過直接依賴傳遞進來的,當升級直接依賴的時候,相關(guān)傳遞性依賴的版本也可能發(fā)生變化,這種變化不易察覺,但是有可能導(dǎo)致當前項目出錯。例如由于接口的改變,當前項目中的相關(guān)代碼無法編譯。這種隱藏的、潛在的威脅一旦出現(xiàn),就往往需要耗費大量的時間來查明真相。因此,顯式聲明任何項目中直接用到的依賴。
結(jié)果中還有一個重要的部分是Unused declared dependencies,意指項目中未使用的,但顯式聲明的依賴,這里有spring-core和spring-beans。需要注意的是,對于這樣一類依賴,我們不應(yīng)該簡單地直接刪除其聲明,而是應(yīng)該仔細分析。由于dependency:analyze只會分析編譯主代碼和測試代碼需要用到的依賴,一些執(zhí)行測試和運行時需要的依賴它就發(fā)現(xiàn)不了。很顯然,該例中的spring-core和spring-beans是運行Spring Framework項目必要的類庫,因此不應(yīng)該刪除依賴聲明。當然,有時候確實能通過該信息找到一些沒用的依賴,但一定要小心測試。
到此這篇關(guān)于Maven坐標和依賴的實現(xiàn)示例的文章就介紹到這了,更多相關(guān)Maven坐標和依賴內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中Scanner類與BufferReader類的不同點(非常詳細)
這篇文章主要介紹了Java中Scanner類與BufferReader類的不同點(非常詳細)的相關(guān)資料,需要的朋友可以參考下2016-08-08spring-boot整合Micrometer+Prometheus的詳細過程
這篇文章主要介紹了springboot整合Micrometer+Prometheus的詳細過程,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2024-05-05Java生成指定范圍內(nèi)的一個隨機整數(shù)2種方式
本文主要介紹了Java生成指定范圍內(nèi)的一個隨機整數(shù)2種方式,主要使用Math.random()和Random.nextInt()這兩種,具有一定的參考價值,感興趣的可以了解一下2023-04-04創(chuàng)建網(wǎng)關(guān)項目(Spring Cloud Gateway)過程詳解
這篇文章主要介紹了創(chuàng)建網(wǎng)關(guān)項目(Spring Cloud Gateway)過程詳解,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-09-09Java任意長度byte數(shù)組轉(zhuǎn)換為int數(shù)組的方法
這篇文章主要給大家介紹了關(guān)于Java任意長度byte數(shù)組轉(zhuǎn)換為int數(shù)組的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者使用Java具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07