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只会分析编译主代码和测试代码需要用到的依赖,一些执行测试和运行时需要的依赖它就发现不了,所以在删除这种声明的时候一定要谨慎。



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值