maven3实战之坐标和依赖

何为maven坐标

maven的世界中拥有数量非常巨大的构件,也就是平时用的一些jar,war等文件。在maven为这些构件引入坐标概念之前,我们无法使用任何一种方式来唯一标识所有这些构件。

maven定义了这样一组规则:世界上任何一个构件都可以使用maven坐标唯一标识,maven坐标的元素包括groupIdartifactIdversionpackagingclassifier。现在,只要我们提供正确的坐标元素,maven就能找到对应的构件。比如说,当需要使用java5平台上TestNG的5.8版本时,就告诉maven:"groupId=org.testing;artifactId=testing;version=5.8;classifier=jdk15",maven就会从仓库中寻找相应的构件供我们使用。也许你会奇怪,maven是从哪里下载构件的呢?答案很简单,maven内置了一个中央仓库的地址(http://repo1.maven.org/maven2),该中央仓库包含了世界上大部分流行的开源项目构件,maven会在需要的时候去那里下载。

坐标详解

maven坐标为各种构件引入了秩序,任何一个构件都必须明确定义自己的坐标,而一组maven坐标是通过一些元素定义的,它们是groupId,artifactId,version,packaging,chassifier。先看一组坐标定义,如下:

 

Xml代码  

1. <groupId>org.sonatype.nexus</groupId>  

2. <artifactId>nexus-indexer</artifactId>  

3. <version>2.0.0</version>  

4. <packaging>jar</packaging>  

 

下面详细解释一下各个坐标元素:

1.groupId : 定义当前maven项目隶属的实际项目。首先,maven项目和实际项目不一定是一对一的关系。比如SpringFramework这一实际项目,其对应的maven项目会有很多,如: spring-core,spring-context等。这是由于maven中模块的概念,因此,一个实际项目往往会被划分成很多模块。其次,groupId不应该对应项目隶属的组织或公司。原因很简单,一个组织下会有很多个实际项目,如果groupId只定义到组织级别,而后面我们会看到,artifactId只能定义maven项目(模块),那么实际项目这个层将难以定义。最后,groupId的表示方式与java包名的表示方式类似,通常与域名反向一一对应。

 

2.artifactId : 该元素定义实际项目中的一个maven项目(模块),推荐的做法是使用实际项目名称作为artifactId前缀,这样做的好处是方便寻找实际构件。在默认情况下,maven生成的构件,其文件名会以artifactId作为开头,如:nexus-indexer-2.0.0.jar,使用实际项目名称作为前缀之后,就能方便从一个lib文件夹中找到某个项目的一组构件。

 

3.version : 该元素定义maven项目当前所处的版本,如:nexus-indexer-2.0.0.jar的版本是2.0.0。需要注意的是,maven定义了一套完整的版本规范,以及快照(SNAPSHOT)的概念。

 

4.packaging : 该元素定义maven项目的打包方式。首先,打包方式通常与所生成构件的文件扩展名对应,如:nexus-indexer.2.0.0.jar的packaging为jar,而使用war打包方式的maven项目,最终生成的构件会有一个.war文件,不过这不是绝对的。其次,打包方式会影响到构建的生命周期,比如jar打包和war打包会使用不同的命令。最后,当不定义packaging的时候,maven会使用默认值jar。

 

5.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,因为附属构件不是项目直接默认生成的,而是由附加的插件帮助生成。

 

上述5个元素中,groupId,artifactId,version是必须定义的,packaging是可选的(默认为jar),而classifier是不能直接定义的。

 

同时,项目构件的文件名是与坐标相对应的,一般的规则为: artifactId-version[-classifier].packaging,[-classifier]表示可选。

依赖的配置

依赖会有基本的groupId,artifactId和version等元素组成。其实一个依赖声明可以包含如下的一些元素:

 

Xml代码  

1. <project>  

2.     ...  

3.         <dependencies>  

4.             <dependency>  

5.                 <groupId>...</groupId>  

6.                 <artifactId>...</artifactId>  

7.                 <version>...</version>  

8.                 <type>...</type>  

9.                 <scope>...</scope>  

10.                <optional>...</optional>  

11.                <exclusions>  

12.                    <exclusion>  

13.                        ...  

14.                    </exclusion>  

15.                </exclusions>  

16.            </dependency>  

17.        </dependencies>  

18.    ...  

19.</project>  

 

根元素project下的dependencies可以包含一个或多个dependency元素,以声明一个或或多个项目依赖。每个依赖可以包含的元素有:

1.groupId,artifactId,version : 依赖的基本坐标,对于任何一个依赖来说,基本坐标是最重要的,maven根据坐标才能找到需要的依赖。

2.type : 依赖的类型,对应于项目坐标定义的packaging。大部分情况下,该元素不必声明,其默认值为jar。

3.scope : 依赖的范围,后续会讲到。

4.optional : 标记依赖是否可选,后续会讲到。

5.exclusions : 用来排除传递性依赖,后续会讲到。

 

大部分依赖声明只包含基本坐标,然而在一些特殊情况下,其他元素也是至关重要的。

依赖范围

maven在编译项目主代码的时候需要使用一套classpath。假如,在编译项目主代码的时候需要用到spring-core,该文件以依赖的方式被引入到classpath中。其次,maven在编译和执行测试代码的时候会使用另外一套classpath。如:JUnit就是一个很好的例子,该文件也以依赖的方式引入到测试使用的classpath中,不同的是这里的依赖范围是test。最后,实际运行maven项目的时候,又会使用一套classpath。

依赖范围就是用来控制依赖与这三种classpath(编译classpath,测试classpath,运行classpath)的关系,maven有以下几种依赖范围:

 

1.compile : 编译依赖范围。如果没有指定,就会默认使用该依赖范围。使用此依赖范围的maven依赖,对于编译测试运行三种classpath都有效。典型的例子,如:spring。

 

2.test : 测试依赖范围。使用此依赖范围的maven依赖,只对于测试classpath有效,在编译主代码或者运行项目时将无法使用此类依赖。典型的例子是:JUnit,它只有在编译测试代码及运行测试的时候才需要。

 

3.provided :  已提供依赖范围。使用此依赖范围的maven依赖,对于编译和测试classpath有效,但在运行时无效。典型的例子是:servlet-api,编译和测试项目的时候需要该依赖,但在运行项目的时候,由于容器已经提供,就不需要maven重复地引入一遍。

 

4.runtime : 运行时依赖范围。使用此依赖范围的maven依赖,对于测试和运行classpath有效,但在编译主代码时无效。典型的例子是JDBC驱动实现,项目主代码的编译只需要JDK提供的JDBC接口,只有在执行测试或者运行项目的时候才需要实现上述接口的具体JDBC驱动。

 

5.system : 系统依赖范围。该依赖与三种classpath的关系,和provided依赖范围完全一致。但是,使用system范围的依赖时必须通过systemPath元素显式地指定依赖文件的路径。由于此类依赖不是通过maven仓库解析的,而且往往与本机系统绑定,可能造成构建的不可移植,因此应该谨慎使用。systemPath元素可以引用环境变量,如:

 

Xml代码  

1. <dependency>  

2.     <groupId>javax.sql</groupId>  

3.     <artifactId>jdbc-stdext</artifactId>  

4.     <version>2.0</version>  

5.     <scope>system</scope>  

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

7. </dependency>  

 

6.import(maven2.0.9及以上) : 导入依赖范围。该依赖范围不会对三种classpath产生实际的影响,后续后讲到。

传递性依赖

1.何为传递性依赖

假如有一个account-email项目,该项目有一个org.springframework:spring-core:2.5.6的依赖,而实际上spring-core也有它自己的依赖,我们可以直接访问位于中央仓库的该构件的POM:http://repo1.maven.org/maven2/org/springframework/spring-core/2.5.6/spring-core-2.5.6.pom。该文件包含了一个commons-logging依赖。该依赖没有声明依赖范围,那么其依赖范围就是默认的compile。同时回顾一下account-email,spring-core的依赖范围也是compile。account-mail有一个compile范围的spring-core依赖,spring-core有一个compile范围的commons-logging依赖,那么commons-logging就会成为account-email的compile范围依赖,commons-loggins是account-email的一个传递性依赖。

有了传递性依赖机制,在使用spring的时候就不用去考虑它依赖了什么,也不用担心引入多余的依赖。maven会解析各个直接依赖的POM,将那些必要的间接依赖,以传递依赖的形式引入到当前的项目中。

 

2.传递性依赖和依赖范围 

依赖范围不仅可以控制依赖与三种classpath的关系,还对传递性依赖产生影响。上面的例子中,account-email对于spring-core的依赖范围是compile,spring-core对于commons-logging的依赖范围是compile,那么account-email对于commons-logging这一传递性依赖的范围也就是compile。假设A依赖B,B依赖C,我们说A对于B是第一直接依赖,B对于C是第二直接依赖,A对于C是传递性依赖第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围,如下图,最左边一列表示第一直接依赖范围,最上面一行表示第二直接依赖范围,中间交叉单元格表示传递性依赖范围。

 

compile

test

provided

runtime

compile

compile

--

--

runtime

test

test

--

--

test

rpovided

provided

--

provided

provided

runtime

runtime

--

--

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)会被解析使用。

 

依赖调解第一原则不能解决所有问题,比如这样的依赖关系:A-->B-->Y(1.0),A-->C-->Y(2.0),Y(1.0)和Y(2.0)的依赖路径长度是一样的,都为2。那么到底谁会被解析使用呢?在maven2.0.8及之前的版本中,这是不确定的,但是maven2.0.9开始,为了尽可能避免构建的不确定性,maven定义了依赖调解的第二原则:第一声明者优先。在依赖路径长度相等的前提下,在POM中依赖声明的顺序决定了谁会被解析使用。顺序最靠前的那个依赖优胜。

可选依赖

假设有这样一个依赖关系,项目A依赖于项目B,项目B依赖于项目X和Y,B对于X和Y的依赖都是可选依赖:A-->B,B-->X(可选),B-->Y(可选)。根据传递性依赖的定义,如果所有这三个依赖的范围都是compile,那么X,Y就是A的compile范围传递性依赖。然而,由于这里X,Y是可选依赖,依赖将不会得以传递。换句话说,X,Y将不会对A有任何影响。

为什么要使用可选依赖这一特性呢?可能项目B实现了两个特性,其中的特性一依赖于X,特性二依赖于Y,而且这两个特性是互斥的,用户不可能同时使用两个特性。比如B是一个持久层隔离工具包,它支持多种数据库,包括MySQL,PostgreSQL等,在构建这个工具包的时候,需要这两种数据库的驱动程序,但在使用这个工具包的时候,只会依赖一个数据库。

 

项目B的依赖声明如下:

Xml代码  

1. <project>  

2.     <modelVersion>4.0.0</modelVersion>  

3.     <groupId>com.juvenxu.mvnbook</groupId>  

4.     <artifactId>project-b</artifactId>  

5.     <version>1.0.0</version>  

6.     <dependencies>  

7.         <dependency>  

8.             <groupId>mysql</groupId>  

9.             <artifactId>mysql-connector-java</artifactId>  

10.            <version>5.1.10</version>  

11.            <optional>true</optional>  

12.        </dependency>  

13.        <dependency>  

14.            <groupId>postgresql</groupId>  

15.            <artifactId>postgresql</groupId>  

16.            <version>8.4-701.jdbc3</version>  

17.            <optional>true</optional>  

18.        </dependency>  

19.    </dependencies>  

20.</project>  

上述xml代码片段中,使用<optional>元素表示mysql-connector-java和postgresql这两个依赖为可选依赖,它们只会对当前项目B产生影响,当其他项目依赖于B的时候,这两个依赖不会被传递。因此,当项目A依赖于项目B的时候,如果其实际使用基于Mysql数据库,那么在项目A中就需要显式地声明mysql-connector-java这一依赖,代码:

Xml代码  

1. <project>  

2.     <modelVersion>4.0.0</modelVersion>  

3.     <groupId>com.juvenxu.mvnbook</groupId>  

4.     <artifactId>project-a</artifactId>  

5.     <version>1.0.0</version>  

6.     <dependencies>  

7.         <dependency>  

8.             <groupId>com.juvenxu.mvnbook</groupId>  

9.             <artifactId>project-b</artifactId>  

10.            <version>1.0.0</version>     

11.        </dependency>  

12.        <dependency>  

13.            <groupId>mysql</groupId>  

14.            <artifactId>mysql-connector-java</artifactId>  

15.            <version>5.1.10</version>  

16.        </dependency>  

17.    </dependencies>  

18.</project>  

最后,关于可选依赖需要说明的一点是,在理想的情况下,是不应该使用可选依赖的。前面我们可以看到,使用可选依赖的原因是某一个项目实现了多个特性,在面向对象设计中,有个单一职责性原则,意指一个类应该只有一项职责,而不是糅合太多的功能 。这个原则在规划maven项目的时候也同样适用。在上面的例子中,更好的做法是为mysql和postgresql分别创建一个maven项目,基于同样的groupId分配不同的artifactId。 

最佳实践之排除依赖

传递性依赖会给项目隐式地引入很多依赖,这极大地简化了项目依赖的管理。但是有些时候这种特性也会带来问题。例如,当前项目有一个第三方依赖,而这个第三方依赖由于某些原因依赖了另外一个类库的SNAPSHOT版本,那么这个SNAPSHOT就会成为当前项目的传递性依赖,而SNAPSHOT的不稳定性会直接影响到当前的项目。这时候需要排除掉该SNAPSHOT,并且在当前项目中声明该类库的某个正式发布版本。还有一些情况,你可能也想要替换某个传递性依赖,比如:sun JTA api,hibernate依赖于这个JAR,但是由于版权的因素。该类库不在中央仓库中,而apache Geronimo项目有一个对应的实现。这时你就可以排除Sun JTA API,再声明Geronimo和JTA API实现。

 

 

Xml代码  

1. <project>  

2.     <modelVersion>4.0.0</modelVersion>  

3.     <groupId>com.juvenxu.mvnbook</groupId>  

4.     <artifactId>project-a</artifactId>  

5.     <version>1.0.0</version>  

6.     <dependencies>  

7.         <dependency>  

8.             <groupId>com.juvenxu.mvnbook</groupId>  

9.             <artifactId>project-b</artifactId>  

10.            <version>1.0.0</version>  

11.            <exclusions>  

12.                <exclusion>  

13.                    <groupId>com.juvenxu.mvnbook</groupId>  

14.                    <artifactId>project-c</artifactId>  

15.                </exclusion>  

16.            </exclusions>  

17.        </dependency>  

18.        <dependency>  

19.            <groupId>com.juvenxu.mvnbook</groupId>  

20.            <artifactId>project-c</artifactId>  

21.            <version>2.2.0</version>  

22.        </dependency>  

23.    </dependencies>  

24.</project>  

 

上述代码中,项目A依赖于项目B,但是由于一些原因,不想引入传递性依赖C,而是自己显式地声明对于项目C1.1.0版本的依赖。代码中使用exclusions元素声明排除依赖,exclusions可以包含一个或多个exclusion子元素,因此可以排除一个或者多个传递性依赖。需要注意的是,声明exclusion的时候只需要groupIdartifactId,而不需要version元素,这是因为只需要groupId和artifactId就能唯一定位依赖图中的某个依赖。换句话说,maven解析后的依赖中,不可能出现groupId和artifactId相同,但是version不同的两个依赖。 

最佳实践之归类依赖

假如有个项目有很多关于SpringFramework的依赖,它们分别是org.springframework:spring-core:2.5.6、org.springframework:spring-bean:2.5.6、org.springframework:spring-context:2.5.6,它们是来自同一项目的不同模块。因此,所有这些依赖的版本会一起升级。因为它们版本是相同的,所以应该在一个唯一的地方定义版本并且在dependency声明中引用这一版本。这样,在升级时只需要修改一处即可,例:

 

Xml代码  

1. <project>  

2.     <modelVersion>4.0.0</modelVersion>  

3.     <groupId>com.juven.mvnbook.account</groupId>  

4.     <artifactId>accout-email</artifactId>  

5.     <version>1.0.0-SNAPSHOT</version>  

6.     <properties>  

7.         <springframework.version>1.5.6</springframework.version>  

8.     </properties>  

9.     <dependencies>  

10.        <dependency>  

11.            <groupId>org.springframework</groupId>  

12.            <artifactId>spring-core</artifactId>  

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

14.        </dependency>   

15.        <dependency>  

16.            <groupId>org.springframework</groupId>  

17.            <artifactId>spring-beans</artifactId>  

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

19.        </dependency>         

20.    </dependencies>  

21.</project>  

 

这里简单用到了maven属性首先使用properties元素定义maven属性,该例中定义了一个springframework.version子元素,其值为:2.5.6。有了这个属性定义之后,maven运行的时候会将POM中的所有的${springframework.version}替换成实际值:2.5.6。也就是说,可以使用美元符号和大括弧环绕的方式引用Maven属性。然后,将所有springframework依赖的版本值用这样一属性引用表示。

最佳实践之优化依赖

在软件开发过程中,程序员会通过重构等方式不断地优化自己的代码,使其变得更简洁、更灵活。同理,程序员也应该能够对maven项目的依赖了然于胸,并对其进行优化,如去除多余的依赖,显式地声明某些必要的依赖。

 

maven会自动解析所有项目的直接依赖和传递性依赖,并且根据规则判断每个依赖的范围,对于一些依赖冲突,也能进行调节,以确保任何一个构件只有唯一的版本在依赖中存在。在这些工作之后,最后得到的那些依赖被称为已解析依赖。可以运行如下命令查看当前项目的已解析依赖:

mvn dependency:list

 

在此基础上,还能进一步了解已解析依赖的信息。将直接在当前项目POM声明的依赖定义为顶层依赖,而这些顶层依赖的依赖定义为第二层依赖,以此类推,有第三,第四层依赖。当这些依赖经maven解析后,就会构成一个依赖树,通过这棵依赖树就能很清楚地看到某个依赖是通过哪条传递路径引入的。可以运行如下命令查看当前项目的依赖树:

mvn dependency:tree

 

使用dependency:list和dependency:tree可以帮助我们详细了解项目中所有依赖的具体信息,在此基础上,还有dependency:analyze工具可以帮助分析当前项目的依赖。我们运行如下命令:

mvn dependency:analyze

执行后的结果中重要的是两个部分。首先是Used undeclared dependencies,意指项目中使用到的,但是没有显式声明的依赖。结果中还有一个重要的部分是:Unused declared dependencies,意指项目中未使用的,但是显式声明的依赖

 

maven3实战之坐标和依赖

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值