Maven 坐标与依赖
Maven 的一大功能是管理项目依赖。为了能自动化地解析任何一个 Java 构件, Maven 就必须将它们唯一标识,这就依赖管理的底层基础 一一 坐标。本章将详细分析 Maven 坐标的作用,解释其每一个元素;在此基础上,再介绍如何配置 Maven,以及相关的经验和技巧,以帮助我们管理项目依赖。
1. 坐标详解
Maven 坐标为各种构件引人了秩序,任何一个构件都必须明确定义自己的坐标,而组 Maven 坐标是通过一些元素定义的,它们是 grouped、 artifact、 version、 packaging、classifier。先看一组坐标定义,如下:
<groupId>org.sonatype.neus</groupId>
<artifactId>nexus-indexer</artifactId>
<version>2.0.0</version>
<packaging>jar</packaging>
这是 nexus-indexer 的坐标定义, nexus-indexer 是一个对 Maven 仓库编纂索引并提供搜索功能的类库,它是 Nexus 项目的一个子模块。后面会详细介绍 Nexus。上述代码片段中,其坐标分别为 grouped: org.sonate.nexus、 artifact: nexus- indexer、 version: 2.0.0、 packaging: jar,没有 classifier。下面详细解释一下各个坐标元素:
-
grouped
定义当前 Maven 项目隶属的实际项目。首先, Maven 项目和实际项目不定是一对一的关系。比如 Springframework 这一实际项目,其对应的 Maven 项目会有很多,如 spring-core、 spring-context 等。这是由于 Maven 中模块的概念,因此,一个实际项目往往会被划分成很多模块。其次, grouped 不应该对应项目隶属的组织或公司。原因很简单,一个组织下会有很多实际项日,如果 grouped 只定义到组织级别,而后面我们会看到, artifact 只能定义 Maven 项目(模块),那么实际项目这个层将难以定义。最后, grouped 的表示方式与 Java 包名的表示方式类似,通常与域名反向对应。上例中, grouped 为 org.sonate.nexus,org-sonate表示 Sonatype 公司建立的一个非盈利性组织, nexus 表示 Nexus 这一实际项项目,该 groupeId 与域名 nexus.sonate.org 对应。 -
artifact
该元素定义实际项目中的一个 Maven 项目(模块),推荐的做法是使用实际项目名称作为 artifact 的前缀。比如上例中的 artifact 是 nexus-indexer,使用了实际项目名 nexus 作为前级,这样做的好处是方便寻找实际构件。在默认情况下,Maven 生成的构件,其文件名名会以 artifact 作为开头,如 nexus-indexer-2.0.0.jar,使用实际项目名称作为前缀之后,就能方便从一个 lib 文件夹中找到某个项目的一组构件。考虑有 5 个项目,每个项目都有一个 core 模块,如果没有前缀,我们会看到很多 core-1.2.jar 这样的文件,加上实际项目名前缀之后,便能很容易区分 foo-core-1. 2.jar, bar-core-1. 2.jar... -
version
该元素定义 Maven 项目当前所处的版本,如上例中 nexus-indexer 的版本是 2.0.0。需要注意的是, Maven 定套完成的版本规范,以及快照(SNAPSHOT)的概念。第13章会详细讨论版本管理内容。 -
packaging
该元素定义 Maven 项目的打包方式。首先,打包方式通常与所生成构件的文件扩展名对应,如上例中 packaging 为 jar,最终文件名为 nexus-indexer-2.0.0.jar,而使用war 打包方式的 Maven 项目,最终生成的构件会有一个 .war 文件,不过这不是绝对的。其次,打包方式会影响到构建的生命周期,比如 jar 打包包和 war 打包会使用不同的命令。最后,当不定义 packaging 的时候, Maven 会使用默认值 jar。 -
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 的典型例子是 TestNG, TestNG 的主构件是基于 Java1.4 平台的,而它又提供了一个 classifier 为 jdk5 的附属构件。注意,不能直接定义项目的 classifier,因为附属构件不是项目直接默认生成的,而是由附加的插件帮助生成。
上述5个元素中, groupId、 artifactId、 version 是必须定义的, packaging 是可选的(默认为 jar),而 classifier 是不能直接定义的。
同时,项目构件的文件名是与坐标相对应的,一般的规则为 artifactId-version [-classifer] .packaging,[-classifier] 表示可选。比如上例 nexus-indexer 的主构件为 nexus- indexer2.0.0.jar,附属构件有 nexus-indexer-2.0.0-javadoc.jar。这里还要强调的一点是, packaging 并非一定与构件扩展名对应,比如 packaging 为 maven-plugin 的构件扩展名为 jar。
此外, Maven 仓库的布局也是基于 Maven 坐标,这一点会在介绍 Maven 仓库的时侯详细解释。理解清楚 Maven 坐标之后,我们就能开始讨论 Maven 的依赖管理了。
坐标查找:
2. 依赖的配置
上文已经罗列了一些简单的依赖配置,可以看到依赖会有基本的 groupeId、artifactId 和 version 等元素组成。其实一个依赖声明可以包含如下的一些元素:
<dependency>
<groupId></groupId>
<artifactId></artifactId>
<version></version>
<scope></scope>
<type></type>
<optional></optional>
<exclusions>
<exclusion></exclusion>
</exclusions>
</dependency>
根元素 project 下的 dependencies 可以包含一个或者多个 dependency 元素,以声明一个或者多个项目依赖。每个依赖可以包含的元素有:
grouped、 artifact 和 version
依赖的基本坐标,对于任何一个依赖来说,基本坐标是最重要的, Maven 根据据坐标才能找到需要的依赖。type
依赖的类型,对应于项目坐标定义的 packaging。大部分情况下,该元素不必声明,其默认值为 jar。scope
依赖的范围optional
标记依赖是否可选,见 5.8 节。exclusions
用来排除传递依赖性,见 5.9 节。
大部分依赖声明只包含基本坐标,然而在特殊情况下,他元素至关重要。本章下面的小节会对它们的原理和使用方式详细介绍。
3. 依赖的范围
Junit 依赖的测试范围是 test,测试范围用元素 scope 表示。本节将详细解释什么是测试范围,以及各种测试范围的效果和用途。
- Maven 在编译项目主代码的时候需要使用一套 classpath。例如,编译项目主代码的时候需要用到 spring-core,该文件以依赖的方式被引入到 classpath 中。
- Maven 在编译和执行测试的时候会使用另外一套 classpath。例如, Junit 就是一个很好的例子,该文件也以依赖的方式引人到测试使用的 classpath 中,不同的是这里的依赖范围是 test。
- 实际运行 Maven 项目的时候,又会使用一套 classpath。例如, spring-core 需要在该 classpath 中,而 Junit 则不需要。
依赖范围就是用来控制依赖与这三种 classpath(编译 classpath、测试 classpath、运行 classpath)的关系, Maven 有以下几种依赖范围:
-
compile
编译依赖范围。如果没有指定,就会默认使用该依赖范围。使用此依赖范围的 Maven 依赖,对于 编译、测试、运行三种 classpath 都有效。典型的例子是 sping-core,在编译、测试和运行的时候都需要使用该依赖。 -
test
测试依赖范围。使用此依赖范围的 Maven 依赖,只对于 测试 classpath 有效,在编译主代码或者运行项目的使用时将无法使用此类依赖。典型的例子是 Junit,它只有在编译测试代码及运行测试的时侯才需要。 -
provided
已提供依赖范围。使用此依赖范围的 Maven 依赖,对于 编译和测试 classpath 有效,但在运行时无效。典型的例子是 servlet-api,编译和测试项目的时候需要该依赖,但在运行项目的时候,由于容器已经提供,就不需要 Maven 重复地引入一遍。 -
runtime
运行时依赖范围围。使用此依赖范围的 Maven 依赖,对于 测试和运行 classpath 有效,但在编译主代码时无效。典型的例子是 JDBC 驱动实现,项目主代码的编译只需要 JDK 提供的 JDBC 接口,只有在执行测试或者运行项目的时候才需要实现上述接口的具体JDBC驱动。 -
system
系统依赖范围。该依赖与三种 classpath 的关系, 和 provided 依赖范围完全一致。但是,使用 system 范围的依赖时必须通过 systemPath 元素显式地指定依赖文件的路径。由于此类依赖不是通过 Maven 仓库解析的,而且往往与本机系统绑定,可能造成构建的不可移植,因此应该谨慎使用。 system 元素可以引用环境变量,如:<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( Maven2.0.9及以上)
导入依赖范围。该依赖范围不会对三种 classpath 产生实际的影响,本书将在 8.3.3 节介绍 Maven 依赖和 dependencyManagement 的时候详细介绍此依赖范围。
上述除 import 以外的各种依赖范围与三种 classpath 的关系如下表所示:
依赖范围 | 编译有效 | 测试有效 | 运行有效 | 例子 |
---|---|---|---|---|
compile | Y | Y | Y | spring-core |
test | - | Y | - | JUnit |
provided | Y | Y | - | servlet-api |
runtime | - | Y | Y | JDBC驱动实现 |
system | Y | Y | - | 本地的,Maven仓库之外的类库 |
4. 传递性依赖
考虑项目 A 依赖 spring-core,而 spring-core 是依赖 commons- logging 的,那么导入 spring-core 后会自动导入 commons- logging,我们就说 commons- logging 是 A 的传递依赖。
4.1 传递性依赖和依赖范围
依赖范围不仅可以控制依赖与三种 classpath 的关系,还对传递性依赖产生影响。假设 A 依赖于 B,B 依赖于 C,我们说 A 对于 B 是第一直接依赖,B 对于 C 是第二直接依赖,A 对于 C 是传递性依赖。第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围,如表2所示,最左边一行表示第一直接依赖范围,最上面一行表示第二直接依赖范围,中间的交又单元格则表示传递性依赖范围。
表2 依赖范围影响传递性依赖
依赖范围 | compile | test | provided | runtime |
---|---|---|---|---|
compile | - | - | runtime | |
test | test | - | - | test |
provided | provided | - | provided | provided |
runtime | runtime | - | - | runtime |
仔细观察一下表2,可以发现这样的规律:
- 当第二直接依赖的范围是 compile 的时候,传递性依赖的范围与第一直接依赖的范围一致;
- 当第二直接依赖的范围是 test 的时候,依赖不会得以传递;
- 当第二直接依赖的范围是 provided 的时候,只传递第一直接依赖范围也为 provided 的依赖,且传递性依赖的范围同样为 provided;
- 当第二直接依赖的范围是 runtime 的时候,传递性依赖的范围与第一直接依赖的范围一致,但 compile 例外,此时传递性依赖的范围为 runtime
5. 依赖调解
5.1 调解原则
Maven 自动按照下边的原则调解:
-
第一声明者优先原则。在 pom.xml 文件定义依赖,先声明的依赖为准。
-
路径近者优先原则。例如:
# spring-beans-4.2.4优先被依赖在 A 中,因为其被 A 依赖的路径最近 A -> spirng-beans-4.2.4 A -> B -> spirng-beans-4.2.4
5.2 依赖冲突解决方案
5.2.1 排除依赖
在依赖 struts2-spring-plugin 的设置中添加排除依赖,排除 spring-beans,
下边的配置表示:依赖 struts2-spring-plugin,但排除 struts2-spring-plugin 所依赖的 spring-beans。
<!-- struts2-spring-plugin依赖spirng-beans-3.0.5 -->
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-spring-plugin</artifactId>
<version>2.3.24</version>
<!-- 排除 spring-beans-->
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</exclusion>
</exclusions>
</dependency>
5.2.2 锁定版本
面对众多的依赖,有一种方法不用考虑依赖路径、声明优化等因素可以采用直接锁定版本的方法确定依赖构件的版本,版本锁定后则不考虑依赖的声明顺序或依赖的路径,以锁定的版本的为准添加到工程中,此方法在企业开发中常用。
如下的配置是锁定了 spring-beans 和 spring-context 的版本:
<dependencyManagement>
<dependencies>
<!--这里锁定版本为4.2.4 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
</dependencies>
</dependencyManagement>
注意:在工程中锁定依赖的版本并不代表在工程中添加了依赖,如果工程需要添加锁定版本的依赖则需要单独添加
<dependencies>
<!--这里是添加依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
</dependencies>
上边添加的依赖并没有指定版本,原因是已在
6. 可选依赖
假设有这样一个依赖关系,项目 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 的依赖声明如下:
<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>
上述 XML 代码片段中,使用
<dependencies>
<dependency>
<groupId>com.github.binarylei</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>
最后,关于可选依赖需要说明的一点是,在理想的情况下,是不应该使用可选依赖的。前面我们可以看到,使用可选依赖的原因是某一个项目实现了多个特性,在面向对象设计中,有个单一职责性原则,意指一个类应该只有一项职责,而不是糅合太多的功能。这个原则在规划 Maven 项目的时候也同样适用。在上面的例子中,更好的做法是为 MYSQL 和 Postgresql 分别创建一个 Maven 项目,基于同样的 groupId 分配不同的的 artifactId,如 com.github.binarylei:project-b-mysql 和 com.github.binarylei:project-b-postgresql,在各自的 POM 中声明对应的 JDBC 驱动依赖,而且不使用可选依赖,用户则根据需要选择使用 projet-b-mysql 或者 project-b-postgresql。由于传递性依赖的作用,就不用再声明
JDBC 驱动依赖。
另外介绍几个 Maven 工具:
mvn dependency:list
mvn dependency:tree
mvn dependency:analyze