来自《Maven实战》第五章

Maven的一大功能是管理项目, 为了能自动化的解析任何一个java构件, Maven必须将它们唯一标识,这就是依赖管理的底层基础–坐标.

坐标为构件引入秩序

关于坐标最熟悉的应该是平面几何, 在平面几何坐标系中, 坐标(x,y)能够唯一标识平面中的一个点. 实际生活中, 地址也是一种坐标, 通过省市县区街道等一系列的信息,可以唯一标示城市中的任意地址, 邮局和快递正是基于这样一种坐标进行日常的工作.

对应于平面中的点和城市中的地址, Maven的世界中都有数量非常巨大的构件,也就是平时
用的一些warjar等文件。Maven通过定义了一组规则,使世界上任何一个构
件都可以使用Maven 坐标进行唯一标识,这大大改善了开发中查找构件和依赖的过程。

Maven 坐标的元素包括goupldartifactldversionpackagingclasifier。只要我们提供正确的坐标元素,Maven就能找到对应的构件。

需要使用某个构件时,只需要告诉Maven构件的坐标元素,Maven就会从仓库中寻找相应的构件供我们使用。Maven内置了一个中央仓库的地址
(htp://repol.maven.ang/mavern2),该中央仓库包含了世界上大部分流行的开源项目构件,Maven
会在需要的时候去那里下载。

在使用Maven开发自己项目的时候,Maven强制要求为其定义适当的坐标。在这个基础上,其他Maven项目才能引用该项目生成的构件。

坐标详解

Maven坐标为各种构件引入了秩序,任何一个构件都必须明确定义自己的坐标,它们是通过groupldartifaclldvesionpackagingclassifer定义的。如下:

<!-- 这是nexus-indexer的坐标定义,nexus-indexer是一个对Maven仓库编集索引并提供搜索
功能的类库,它是Nexus项目的一个子模块 -->
<groupId>org.sonatype.nexus <向roup1d>
<artifactId>nexus-indexer</artifactld>
<version>2.0.0<version>
<packaging>jar<packaging>

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

  1. groupId:必选,定义当前Maven项目隶属的实际项目。

首先,Maven项目和实际项目不一定是一对一的关系。比如SpringFramework这一实际项目,其对应的Maven项目会有很多,如spring-core、spring-context等。这是由于Maven中模块的概念,因此,一个实际项目往往会被划分成很多模块。

其次,groupId不应该对应项目隶属的组织或公司。一个组织下会有很多实际项目,如果groupId只定义到组织级别,那么实际项目这个层将难以定义。

最后,groupId的表示方式与Java包名的表示方式类似,通常与域名反向一对应。上例中,groupId为org.sonatype.nexus,org.sonatype表示Sonatype公司建立的一个非盈利性组织,nexus 表示Nexus这一实际项目,该groupId与域名nexus.sonatype.org对应。

  1. artifactId:必选,该元素定义实际项目中的一个Maven项目(模块),推荐的做法是使用实际项目名称作为前缀。

比如上例中的artifactldnexus-indexer,使用了实际项目名nexus作为前缀,这样做的好处是方便寻找实际构件。在默认情况下,Maven生成的构件,其文件名会以artifactId作为开头,如nesxus-indexer-2.0.0.jar,使用实际项目名称作为前缀之后,就能方便从一个lib文件夹中找到某个项目的一组构件。

  1. version: 必选,该元素定义Maven项目当前所处的版本,如上例中nexus-indexer的版本是2.0.0。
  2. pakaging: 非必选,该元素定义Mavcn项目的打包方式,默认值为jar,可选值[war、jar、pom]

打包方式通常与所生成构件的文件扩展名对应,如上例中packaging为jar,最终的文件名为nexus-indexer-2.0.0.jar,packaging并非一定与构件扩展名对应,比如packaging为maven-plugin的构件扩展名为jar。

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

依赖的配置

首先需要知道,Maven在编译项目主代码的时候需要使用一套classpath。其次,Maven在编译和执行测试的时候会使用另外一套classpath,不同的是这里的依赖范围是test。最后,实际运行Maven项目的时候,又会使用一套claspath依赖范围就是用来控制依赖与这三种classpath(编译classpath、测试classpath、运行classpath)的关系Maven有以下几种依赖范围:

  1. compile:编译依赖范围。如果没有指定,就会默认使用该依赖范围。使用此依赖范围的Maven依赖,对于编译、测试、运行三种classpath都有效。
  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仓库解析的。sysemPath元素可以引用环境变量。
<dependency>  
    <groupId>org.postgresql</groupId>  
    <artifactId>postgresql</artifactId>  
    <version>9.3</version>  
    <scope>system</scope>  
    <systemPath>${project.basedir}/lib/postgresql-9.3.jar</systemPath>  
</dependency>

使用这种方式引入的JAR包在打包时不会一起打包,所以打包后找不到该JAR包

解决办法是,将jar包放到resouces目录下,使用system依赖引入

  1. import:导入依赖范围,该依赖范围不会对三种classpath产生实际的影响,该依赖针对的是packagingpom类型的。

依赖范围scope

对于编译classpath有效

对于测试classpath有效

对于运行时classpath有效

例子

compile




spring-core

test




JUnit

provided




servlet-api

runtime




jdbc驱动实现

system




本地的,Maven仓库之外的类库文件

import

-

-

-

-

传递性依赖

概述

在没有Maven的情况下,在项目中引入spring,而spring又依赖其他的开源类库,一般就只能等运行报错,在去查文档,去网站下载其他的开源类库引入进来,非常麻烦。Maven的传递性依赖就是解决这个问题的。

在传递性依赖的机制下,Maven会自动解析各个直接依赖的POM,将必要的间接依赖,以传递性依赖的形式引入到当前项目中。

传递性依赖和依赖范围

依赖范围不仅可以控制依赖与三种classpath,还对传递性依赖产生影响。假设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

  1. 当第二直接依赖范围是compile时,依赖范围和第一直接依赖范围相同.
  2. 当第二直接依赖范围是test时,依赖范围不传递。
  3. 当第二直接依赖范围是provided时,只传递第一依赖范围是provided的依赖,而且传递依赖范围同样是provided
  4. 当第二直接依赖范围是runtime的,依赖范围和第一直接依赖范围相同,但compile除外,此时传递依赖范围是runtime

依赖调解原则

  1. 依赖调解第一原则:路径最近者优先。 当出现A->B->C->X(1.0), A->D->X(2.0), X是A的传递性依赖,但是两条依赖路径下,有两个版本的X,为避免造成依赖重复,必须选择一个,因为X1.0的依赖路径为3, 2.0的依赖路径为2,所以Maven会选择X2.0,
  2. 依赖调解第二原则:第一声明者优先。在依赖路径相等的情况下,在POM依赖声明中顺序靠前的依赖优胜。

可选依赖

可选依赖是通过dependencyoptional属性声明的,理想情况下是不需要可选依赖的。

最佳实践

排除依赖

项目A依赖项目B,项目B依赖于项目C,由于一些原因,不想通过传递性依赖来引入C,而是自己显式的声明对项目C的依赖,这时候可以通过exclusions元素声明排除依赖。

<dependency>
  <groupId>sample.b</groupId>
  <artifactId>project-b</artifactId>
  <version>1.0</version>
  <scope>compile</scope>
  <exclusions>
  <!-- 排除依赖 -->
    <exclusion>  <!-- declare the exclusion here -->
      <groupId>sample.c</groupId>
      <artifactId>Project-c</artifactId>
    </exclusion>
  </exclusions>
</dependency>
<!-- 自己依赖 -->
<dependency>
  <groupId>sample.c</groupId>
  <artifactId>project-c</artifactId>
  <version>1.0</version>
</dependency>
优化依赖
  1. 查看当前项目已解析的依赖:mvn dependency:list 可以看到下图中的已解析的依赖和每个依赖的范围
[INFO] 
[INFO] The following files have been resolved:
[INFO]    org.springframework:spring-beans:jar:2.5.6:compile
[INFO]    javax.activation:activation:jar:1.1:compile
[INFO]    javax.mail:mail:jar:1.4.7:compile
[INFO]    org.slf4j:slf4j-api:jar:1.7.21:test
[INFO]    commons-logging:commons-logging:jar:1.1.1:compile
[INFO]    org.springframework:spring-context-support:jar:2.5.6:compile
[INFO]    aopalliance:aopalliance:jar:1.0:compile
[INFO]    junit:junit:jar:4.7:test
[INFO]    com.icegreen:greenmail:jar:1.5.5:test
[INFO]    com.sun.mail:javax.mail:jar:1.5.6:test
[INFO]    org.springframework:spring-core:jar:2.5.6:compile
[INFO]    org.springframework:spring-context:jar:2.5.6:compile
  1. 将在pom中声明的依赖定义为顶层依赖,顶层依赖的依赖位第二层,以此类推,会形成一个依赖树,通过依赖树,就可以了很清楚的看到依赖路径。
mvn dependency:tree
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ account-email ---
[INFO] com.pudding.mvnstudy.account:account-email:jar:1.0-SNAPSHOT
[INFO] +- junit:junit:jar:4.7:test
[INFO] +- org.springframework:spring-core:jar:2.5.6:compile
[INFO] |  \- commons-logging:commons-logging:jar:1.1.1:compile
[INFO] +- org.springframework:spring-beans:jar:2.5.6:compile
[INFO] +- org.springframework:spring-context:jar:2.5.6:compile
[INFO] |  \- aopalliance:aopalliance:jar:1.0:compile
[INFO] +- org.springframework:spring-context-support:jar:2.5.6:compile
[INFO] +- javax.mail:mail:jar:1.4.7:compile
[INFO] |  \- javax.activation:activation:jar:1.1:compile
[INFO] \- com.icegreen:greenmail:jar:1.5.5:test
[INFO]    +- com.sun.mail:javax.mail:jar:1.5.6:test
[INFO]    \- org.slf4j:slf4j-api:jar:1.7.21:test
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
  1. 分析依赖:mvn dependency:analyze该结果包括两个部分:

Unused undeclared dependencies 指项目中使用到的,但没有显式声明的依赖,存在风险,应该显式声明任何项目中直接用到的依赖

Unused declared dependencies 指项目中显式声明了,而没有用到的依赖,对于这样的依赖应该仔细分析,而不是直接删除依赖