正如前面文章所述,Maven 的一大功能是管理项目依赖。为了能自动化地解析任何一个 Java 构件, Maven 就必须将它们唯一标识,这就依赖管理的底层基础——坐标。本节将详细分析 Maven 坐标的作用,解释其每一个元素;在此基础上,再介绍如何配置Maven, 以及相关的经验和技巧,以帮助我们管理项目依赖。
关于坐标 (Coordinate
), 大家最熟悉的定义应该来自于平面几何。在一个平面坐标系中,坐标 (x,y) 表示该平面上与x 轴距离为y, 与 y 轴距离为x 的一点,任何一个坐标都能够唯一标识该平面中的一点。
在实际生活中,我们也可以将地址看成是一种坐标。省、市、区、街道等一系列信息 同样可以唯一标识城市中的任一居住地址和工作地址。邮局和快递公司正是基于这样一种 坐标进行日常工作的。
对应于平面中的点和城市中的地址, Maven 的世界中拥有数量非常巨大的构件,也就是平时用的一些jar、war等文件。在Maven 为这些构件引入坐标概念之前,我们无法使用任何一种方式 来唯一标识所有这些构件。因此,当需要用到 Spring Franework
依赖的时候,大家会去 Spring Framework
网站寻找,当需要用到 log4j
依赖的时候,大家又会去 Apache
网站寻找。又因为各个项目的网站风格迥异,大量的时间花费在了搜索、浏览网页等工作上面。
没有统一的规范、统一的法则,该工作就无法自动化。重复地搜索、浏览网页和下载类似的 jar文件,这本就应该交给机器来做。而机器工作必须基于预定义的规则, Maven 定义了这样一组规则:世界上任何一个构件都可以使用Maven 坐标唯一标识, Maven 坐标的元素包括 groupld
、artifactld
、version
、packaging
、clasifer
。现在,只要我们提供正确的坐标元素, Maven 就能找到对应的构件。比如说,当需要使用 Java5 平台上TestNG
的5.8版本时,就告诉 Maven: “groupld =org.testng; artifactld =testng; version=5.8; classifier =jdk15
”, Maven 就会从仓库中寻找相应的构件供我们使用。
也许你会奇怪, “Maven 是从哪里下载构件的呢?” 答案其实很简单, Maven 内置了一个中央仓库的地址 (http://repol.maven.ong/maven2
), 该中央仓库包含了世界上大部分流行的开源项目构件, Maven 会在需要的时候去那里下载。
在我们开发自己项目的时候,也需要为其定义适当的坐标,这是 Maven 强制要求的。 在这个基础上,其他Maven 项目才能引用该项目生成的构件,见下图。
Maven 坐标为各种构件引入了秩序,任何一个构件都必须明确定义自己的坐标,而 一 组 Maven 坐标是通过一些元素定义的,它们是 groupld
、artifactld
、version
、packaging
、classifier
。先看一组坐标定义,如下:
<groupId>org.sonatype.nexusgroupId>
<artifactId>nexus-indexerartifactId>
<version>2.0.0<Nersion>
<packaging>jar<packaging>
这是 nexus-indexer
的坐标定义, nexus-indexer
是一个对Maven 仓库编纂索引并提供搜索功能的类库,它是 Nexus
项目的一个子模块。后面会详细介绍 Nexus。上述代码片段中,其坐标分别为 groupld:org.sonatype.nexus、artifactld:nexus-indexer、version:2.0.0、packaging: jar
, 没有 classifier。下面详细解释一下各个坐标元素:
groupId
:定义当前 Maven 项目隶属的实际项目。首先, Maven 项目和实际项目不一定是一对一的关系。比如 Spring Framework
这一实际项目,其对应的 Maven 项目会有很多,如 spring-core
、spring-context
等。这是由于Maven 中模块的概念,因此,一个实际项目往往会被划分成很多模块。其次, groupId
不应该对应项目隶属的组织或公司。原因很简单,一个组织下会有很多实际项目,如果 groupId
只定义到组织级别, 而后面我们会看到,artifactId
只能定义Maven 项目(模块), 那么实际项目这个层将难以定义。最后, groupId
的表示方式与Java包名的表示方式类似,通常与域名反向一一对应。上例中, groupId
为 org.sonatype.nexus
, org.sonatype
表示 Sonatype
公司建立的一 个非盈利性组织,nexus
表示 Nexus 这一实际项目,该 groupId
与域名 nexus.sonatype.org
对应。artifactId
:该元素定义实际项目中的一个Maven项目(模块), 推荐的做法是使用实际项目名称作为 artifactId
的前缀。比如上例中的 artifactId
是 nexus-indexer
, 使用了实际项目名 nexus
作为前缀,这样做的好处是方便寻找实际构件。在默认情况下, Maven生成的构件,其文件名会以 artifactId
作为开头,如 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)的概念。后面文章会详细讨论版本管理内容。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 的主构件是基于Java 1.4平台的,而它又提供了一个classifier为 jdk5 的附属构件。注意,不能直接定义项目的 classifier, 因为附属构件不是项目直接默认生成的,而是由附加的插件帮助生成。上述5个元素中, groupId、artifactId、version
是必须定义的, packaging
是可选的(默认为jar), 而 classifier
是不能直接定义的。
同时,项目构件的文件名是与坐标相对应的, 一般的规则为 artifactId-version [-classifier].packaging
, [-classifier]
表示可选。比如上例 nexus-indexer
的主构件为 nexus-indexer-2.0.0.jar
, 附属构件有 nexus-indexer-2.0.0-javadoe.jar
。这里还要强调的一点是,packaging
并非一定与构件扩展名对应,比如 packaging 为 maven-plugin 的构件扩展名为 jar。
此外, Maven 仓库的布局也是基于Maven 坐标,这一点会在介绍 Maven 仓库的时候详细解释。理解清楚城市中地址的定义方式后,邮递员就能够开始工作了;同样地,理解清楚 Maven 坐标之后,我们就能开始讨论Maven 的依赖管理了。
上面介绍了maven配置文件中一些简单的依赖配置,可以看到依赖会有基本的 groupId
、artifactId
和 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 根据坐标才能找到需要的依赖。type
:依赖的类型,对应于项目坐标定义的 packaging
。大部分情况下,该元素不必声明,其默认值为jar。scope
:依赖的范围,后面会详细介绍。optional
:标记依赖是否可选,后面会详细介绍。exclusions
:用来排除传递性依赖,后面会详细介绍。大部分依赖声明只包含基本坐标,然而在一些特殊情况下,其他元素至关重要。后面会对它们的原理和使用方式详细介绍。
本节将详细解释什么是依赖范围,以及各种依赖范围的效果和用途。
首先需要知道, Maven 在编译项目主代码的时候需要使用一套 classpath
。 在上例中,编译项目主代码的时候需要用到 sping-core
, 该文件以依赖的方式被引入到 classpath中。其次 ,Maven 在编译和执行测试的时候会使用另外一套 classpath
。最后,实际运行Maven 项目的时候,又会使用一套 classpath
, 上例中的 spring-core
需要在该classpath
中,而JUnit 则不需要。
依赖范围就是用来控制依赖与这三种 classpath ( 编译 classpath、 测试 classpath、 运行 classpath) 的关系,Maven 有以下几种依赖范围:
compile
:编译依赖范围。如果没有指定,就会默认使用该依赖范围。使用此依赖范围 的 Maven 依赖,对于编译、测试、运行三种 classpath 都有效。典型的例子是 spring-core, 在编译、测试和运行的时候都需要使用该依赖。
test
:测试依赖范围。使用此依赖范围的Maven 依赖,只对于测试 classpath 有效,在 编译主代码或者运行项目的使用时将无法使用此类依赖。典型的例子是JUnit, 它只有在编译测试代码及运行测试的时候才需要。
provided
:已提供依赖范围。使用此依赖范围的Maven 依赖,对于编译和测试 classpath有效,但在运行时无效。典型的例子是 servlet-api, 编译和测试项目的时候需要 该依赖,但在运行项目的时候,由于容器已经提供,就不需要 Maven 重复地引入一遍。
runtime
:运行时依赖范围。使用此依赖范围的 Maven 依赖,对于测试和运行 classpath有效,但在编译主代码时无效。典型的例子是JDBC 驱动实现,项目主代码的编译只需要JDK 提供的JDBC 接口,只有在执行测试或者运行项目的时候才需要实现上述接口的具体JDBC 驱动。
system
:系统依赖范围。该依赖与三种 classpath 的关系,和 provided 依赖范围完全一致。但是,使用system 范围的依赖时必须通过systemPath 元素显式地指定依赖文件 的路径。由于此类依赖不是通过Maven 仓库解析的,而且往往与本机系统绑定,可能造成构建的不可移植,因此应该谨慎使用。 systemPath 元素可以引用环境变量,如:
<dependency>
<groupId>javax.sqlgroupId>
<artifactId>jdbc-stdextartifactId>
<version>2.0version>
<scope>systemscope>
<systemPath>${java.home}/lib/rt.jarsystemPath>
dependency>
import(Maven 2.0.9及以上)
:导入依赖范围。该依赖范围不会对三种 classpath 产生实际的影响,在后续介绍 Maven 依赖和 dependeneyManagement 的时候会详细介绍此依赖范围。
上述除 import
以外的各种依赖范围与三种 classpath的关系如下所示。
依赖范围(Scope) | 对于编译classpath有效 | 对于测试classpath有效 | 对于运行时classpath有效 | 例子 |
---|---|---|---|---|
compile | ✔️ | ✔️ | ✔️ | spring-core |
test | - | ✔️ | - | Junit |
provided | ✔️ | ✔️ | - | servlet-api |
runtime | - | ✔️ | ✔️ | JDBC驱动 |
system | ✔️ | ✔️ | - | 本地的 maven仓库之外的类库文件 |
考虑一个基于Spring Framework 的项目,如果不使用Maven, 那么在项目中就需要手动 下载相关依赖。由于Spring Framework 又会依赖于其他开源类库,因此实际中往往会下载一个很大的如 spring-framework-2.5.6-with-dependencies.zip
的包,这里包含了所有Spring Framework 的 jar包,以及所有它依赖的其他 jar包。这么做往往就引入了很多不必要的依赖。
另一种做法是只下载 spring-framework-2.5.6.zip
这样一个包,这里不包含其他相关依赖,到实际使用的时候,再根据出错信息,或者查询相关文档,加入需要的其他依赖。很显然,这也是一件非常麻烦的事情。
Maven 的传递性依赖机制可以很好地解决这一问题。例如现在有一个名为 account-email
的项目,该项目有一个 org.springframework:spring-core:2.5.6
的依赖,而实际上 spring-core 也有它自己的依赖,我们可以直接访问位于中央仓库的该构件的POM: http://repol.maven.org/maven2/org/springframework/spring-core/2.5.6/spring-core-2.5.6.pom
。该文件包含了一个commons-logging
依赖,如下。
<dependency>
<groupId>commons-logginggroupId>
<artifactId>commons-loggingartifactId>
<version>1.1.1version>
dependency>
该依赖没有声明依赖范围,那么其依赖范围就是默认的compile
。 同时回顾—下 account-email
, spring-core 的依赖范围也是 compile。
account-mail
有一个compile 范围的 spring-core 依赖, sping-core 有一个 compile 范围的 commons-logging 依赖,那么 commons-logging 就会成为 account-email
的 compile 范围依赖, commons-logging 是 account-email 的一个传递性依赖,如下图所示。
有了传递性依赖机制,在使用Spring Framework 的时候就不用去考虑它依赖了什么,也 不用担心引入多余的依赖。 Maven 会解析各个直接依赖的 POM, 将那些必要的间接依赖, 以传递性依赖的形式引入到当前的项目中。
依赖范围不仅可以控制依赖与三种 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 |
provided | provided | - | provided | provided |
runtime | runtime | - | - | runtime |
为了能帮助大家更好地理解上表,再举个例子。 account-email
项目有一个 com.icegreen:greenmail:1.3.1b
的直接依赖,我们说这是第一直接依赖,其依赖范围是test
; 而 greenmail又有一个 javax.mail:mail:1.4
的直接依赖,我们说这是第二直接依赖,其依赖范围是 compile
。 显然 javax.mail:mail:1.4
是 account-email 的传递性依赖,对照上表可以知道,当第一直接依赖范围为test
, 第二直接依赖范围是 compile
的时候,传递性依赖的范围是test, 因此 javax.mail:mail:1.4
是 account-email
的一个范围是 test
的传递性依赖。
仔细观察一下上表,可以发现这样的规律:当第二直接依赖的范围是 compile 的时候,传递性依赖的范围与第一直接依赖的范围一致;当第二直接依赖的范围是 test 的时候, 依赖不会得以传递;当第二直接依赖的范围是 provided 的时候,只传递第一直接依赖范围 也为provided 的依赖,且传递性依赖的范围同样为 provided; 当第二直接依赖的范围是runtime 的时候,传递性依赖的范围与第一直接依赖的范围一致,但 compile 例外,此时传递性依赖的范围为 runtime。
Maven 引入的传递性依赖机制, 一方面大大简化和方便了依赖声明,另一方面,大部 分情况下我们只需要关心项目的直接依赖是什么,而不用考虑这些直接依赖会引入什么传递性依赖。但有时候,当传递性依赖造成问题的时候,我们就需要清楚地知道该传递性依赖是从哪条依赖路径引入的。
例如,项目A 有这样的依赖关系: A->B->C->X(1.0)
、A->D->X(2.0)
, X 是 A 的传递性依赖,但是两条依赖路径上有两个版本的X, 那么哪个X 会被 Maven 解析使用呢? 两个版本都被解析显然是不对的,因为那会造成依赖重复,因此必须选择一个。Maven 依赖调解 (Dependency Mediation
) 的第一原则是:路径最近者优先。该例中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。那么到底谁会被解析
使用呢? 在Maven 2.0.8及之前的版本中,这是不确定的,但是从 Maven 2.0.9开始,为了尽可能避免构建的不确定性, Maven 定义了依赖调解的第二原则:第一声明者优先。
在依赖路径长度相等的前提下,在POM 中依赖声明的顺序决定了谁会被解析使用,顺序最靠前的那个依赖优胜。该例中,如果B 的依赖声明在C 之前,那么Y(1.0) 就会被解析使用。
假设有这样一个依赖关系,项目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 的依赖声明见下边代码清单。
<project>
<modelVersion>4.0.0modelVersion>
<groupId>com.xiaoshan.mvnbookgroupId>
<artifactId>project-bartifactId>
<version>1.0.0version>
<dependencies>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.10version>
<optional>trueoptional>
dependency>
<dependency>
<groupId>postgresqlgroupId>
<artifactId>postgresqlartifactId>
<version>8.4-701.jdbc3version>
<optional>trueoptional>
dependency>
dependencies>
project>
上述 XML代码片段中,使用
元素表示 mysql-connector-java
和 postgresql
这两个依赖为可选依赖,它们只会对当前项目B产生影响,当其他项目依赖于B的时候,这两个依赖不会被传递。因此,当项目A依赖于项目B的时候,如果其实际使用基于MySQL数据库,那么在项目A中就需要显式地声明 mysgl-connectorjava
这一依赖,见以下代码清单。
<project>
<modelVersion>4.0.0modelVersion>
<groupId>com.xiaoshan.mvnbookgroupId>
<artifactId>project-aartifactId>
<version>1.0.0version>
<dependencies>
<dependency>
<groupId>com.xiaoshan.mvnbookgroupId>
<artifactId>project-bartifactId>
<version>1.0.0version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.10version>
dependency>
dependencies>
project>
最后,关于可选依赖需要说明的一点是,在理想的情况下,是不应该使用可选依赖的。 前面我们可以看到,使用可选依赖的原因是某一个项目实现了多个特性,在面向对象设计中,有个单一职责性原则,意指一个类应该只有一项职责,而不是糅合太多的功能。
这个原则在规划 Maven 项目的时候也同样适用。在上面的例子中,更好的做法是为MySQL 和 PostgreSQL分别创建一个 Maven 项目 , 基于同样的 groupId 分配不同的artifactId, 如 com.xiaoshan. mvnbook:project-b-mysql
和 com.xiaoshan. mvnbook:project-b-postgresgl
, 在各自的 POM 中声明对应的JDBC 驱动依赖,而且不使用可选依赖,用户则根据需要选择使用 pro-ject-b-mysql
或者 project-b-postgresql
。 由于传递性依赖的作用,就不用再声明JDBC 驱动依赖。
Maven 依赖涉及的知识点比较多,在理解了主要的功能和原理之后,最需要的当然就是前人的经验总结了,我们称之为最佳实践。本小节归纳了一些使用Maven 依赖常见的技 巧,方便用来避免和处理很多常见的问题。
传递性依赖会给项目隐式地引入很多依赖,这极大地简化了项目依赖的管理,但是有些时候这种特性也会带来问题。例如,当前项目有一个第三方依赖,而这个第三方依赖由于某些原因依赖了另外一个类库的SNAPSHOT 版本,那么这个 SNAPSHOT 就会成为当前项目的传递性依赖,而 SNAPSHOT 的不稳定性会直接影响到当前的项目。这时就需要排除掉该SNAPSHOT, 并且在当前项目中声明该类库的某个正式发布的版本。还有 一些情况,你可能也想要替换某个传递性依赖,比如 Sun JTA API
, Hibernate
依赖于这个JAR, 但是由于版权的因素,该类库不在中央仓库中,而 Apache Geronimo项目有一个对应的实现。这时你就可以排除 Sun JAT API
, 再声明 Geronimo
的 JTA API
实现,见如下代码清单。
<project>
<modelVersion>4.0.0modelVersion>
<groupId>com.xiaoshan.mvnbookgroupId>
<artifactId>project-aartifactId>
<version>1.0.0version>
<dependencies>
<dependency>
<groupId>com.xiaoshan.mvnbookgroupId>
<artifactId>project-bartifactId>
<version>1.0.0version>
<exclusions>
<exclusion>
<groupId>com.xiaoshan.mvnbookgroupId>
<artifactId>project-cartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>com.xiaoshan.mvnbookgroupId>
<artifactId>project-cartifactId>
<version>1.1.0version>
dependency>
dependencies>
project>
上述代码中,项目A依赖于项目B, 但是由于一些原因,不想引入传递性依赖C, 而是自己显式地声明对于项目C 1.1.0版本的依赖。代码中使用 exclusions
元素声明排除依赖,exclusions
可以包含一个或者多个exclusion
子元素,因此可以排除一个或者多个传递性依赖。需要注意的是,声明 exclusion
的时候只需要 groupId
和 artifactId
,而不需要version
元素,这是因为只需要 groupId
和 artifactId
就能唯一定位依赖图中的某个依赖。换句话说,Maven解析后的依赖中,不可能出现 groupId
和artifactId
相同,但是version
不同的两个依赖。该例的依赖解析逻辑如下图所示。
在前面文章中介绍过,在一个项目中可能有很多关于 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
, 它们是来自同一项目的不同模块。
因此,所有这些依赖的版本都是相同的,而且可以预见,如果将来需要升级SpringFramework
, 这些依赖的版本会一起升级。这一情况在Java中似曾相识,考虑如下简单代码。
public double c(double r){
return 2 * 3.14 * r;
}
public double s(double r){
return 3. 14 * r * r;
}
这两个简单的方程式计算圆的周长和面积,稍微有经验的程序员一眼就会看出一个问题,使用字面量(3.14)显然不合适,应该使用定义一个常量并在方法中使用,见如下代码清单。
public final double PI = 3.14;
public double c(double r){
return 2 * PI * r;
}
public double s(double r){
return PI * r * r;
}
使用常量不仅让代码变得更加简洁,更重要的是可以避免重复,在需要更改 PI
的值的
时候,只需要修改一处,降低了错误发生的概率。
同理,对于account-email
中这些SpringFramework
来说,也应该在一个唯一的地方定义
版本,并且在dependency
声明中引用这一版本。这样,在升级SpringFramework
的时候就只需要修改一处,实现方式见如下代码清单。
<project>
<modelVersion>4.0.0modelVersion>
<groupId>com.xiaoshan.mvnbook.accountgroupId>
<artifactId>yaccount-emailartifactId>
<name>AccountEmailname>
<version>1.0.0-SNAPSHOTversion>
<properties>
<springframework.version>2.5.6springframework.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-coreartifactId>
<version>${springframework.version}version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-beansartifactId>
<version>${springframework.version}version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactIds>
<version>${springframework.version}version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-context-supportartifactId>
<version>${springframework.version}version>
dependency>
dependencies>
project>
这里简单用到了Maven 属性(后面文章会详细介绍 Maven 属性) , 首先使用 properties
元 素定义Maven 属性,该例中定义了一个 springframework. version
子元素,其值为 2.5.6
。有了这个属性定义之后, Maven 运行的时候会将 POM 中的所有的 ${springframework.version}
替换成实际值 2.5.6
。也就是说,可以使用美元符号$
和大括弧 {
和}
环绕的方式引用 Maven 属性。然后,将所有 Spring Framework
依赖的版本值用这一属性引用表示。这和在Java 中用常量 PI
替换 3.14
是同样的道理,不同的只是语法。
在软件开发过程中,程序员会通过重构等方式不断地优化自己的代码,使其变得更简 洁、更灵活。同理,程序员也应该能够对Maven 项目的依赖了然于胸,并对其进行优化, 如去除多余的依赖,显式地声明某些必要的依赖。
本文前面的内容已经介绍到: Maven 会自动解析所有项目的直接依赖和传递性依赖,并且根据规则正确判断每个依赖的范围,对于一些依赖冲突,也能进行调节,以确保任何一个构件只有唯一的版本在依赖中存在。在这些工作之后,最后得到的那些依赖被称为已解析依赖(Resolved Dependency
)。 可以运行如下的命令查看当前项目的已解析依赖:
mvn dependency:list
在项目中执行该命令,结果如图所示。
上图显示了所有 account-email
的已解析依赖,同时,每个依赖的范围也得以明确标示。
在此基础上,还能进一步了解已解析依赖的信息。将直接在当前项目POM 声明的依赖 定义为顶层依赖,而这些顶层依赖的依赖则定义为第二层依赖,以此类推,有第三、第四层依赖。当这些依赖经 Maven 解析后,就会构成一个依赖树,通过这棵依赖树就能很清楚地看到某个依赖是通过哪条传递路径引入的。可以运行如下命令查看当前项目的依赖树:
mvn dependency:tree
在项目中执行该命令,效果如下图所示。
从图中能够看到,虽然我们没有声明 org.sl4j:sl4j-api:1.3
这一依赖,但它还是经过 com.icegreen:greenmail:1.3
成为了当前项目的传递性依赖,而且其范围是 test。
使用 dependency:list
和 dependeney:tree
可以帮助我们详细了解项目中所有依赖的具体 信息,在此基础上,还有 dependency:analyze
工具可以帮助分析当前项目的依赖。
为了说明该工具的用途,先将 spring-context
这一依赖删除,然后构建项目,你会发现编译、测试和打包都不会有任何问题。通过分析依赖树,可以看到 spring-context
是 spring-context-support
的依赖,因此会得以传递到项目的 classpath
中。现在再运行如下命令:
mvn dependency:analyze
结果如下图所示。
该结果中重要的是两个部分。首先是Used undeclared dependencies
, 意指项目中使用到的,但是没有显式声明的依赖,这里是 spring-context
。 这种依赖意味着潜在的风险,当前项目直接在使用它们,例如有很多相关的Java import 声明,而这种依赖是通过直接依赖传递进来的,当升级直接依赖的时候,相关传递性依赖的版本也可能发生变化,这种变化不易察觉,但是有可能导致当前项目出错。例如由于接口的改变,当前项目中的相关代码无法编译。这种隐藏的、潜在的威胁一旦出现,就往往需要耗费大量的时间来查明真相。因此, 显式声明任何项目中直接用到的依赖。
结果中还有一个重要的部分是 Unused declared dependencies
, 意指项目中未使用的,但 显式声明的依赖,这里有 spring-core
和 spring-beans
、 需要注意的是, 对于这样一类依赖, 我们不应该简单地直接删除其声明,而是应该仔细分析。由于dependeney:analyze
只会分析编译主代码和测试代码需要用到的依赖, 一些执行测试和运行时需要的依赖它就发现不了。 很显然,该例中的 spring-core
和 spring-beans
是 运 行 Spring Framework
项目必要的类库,因此不应该删除依赖声明。当然,有时候确实能通过该信息找到一些没用的依赖,但一定要小心测试。
本文主要介绍了Maven 的两个核心概念:坐标和依赖。解释了坐标的由来,并详细阐述了各坐标元素的作用及定义方式。随后引入了一个项目实际的基于 Spring Framework 的模块,包括了POM 定义、业务代码和测试代码。在这一直观感受的基础上,再花篇幅介绍了 Maven 依赖,包括依赖范围、传递性依赖、可选依赖等概念。最后,当然少不了关于依赖的一些最佳实践。通过阅读本文,大家应该已经能够透彻地了解 Maven 的依赖管理机制。下一节将会介绍 Maven 的另一个核心概念:仓库。
⏪ 温习回顾上一篇(点击跳转):
《【Maven教程】(三)基础使用篇:入门使用指南——POM编写、业务代码、测试代码、打包与运行、使用Archetype生成项目骨架~》
⏩ 继续阅读下一篇(点击跳转):
《【Maven教程】(五)仓库:解析Maven仓库—布局、分类和配置,远程仓库的认证与部署,快照版本,依赖解析机制,镜像和搜索服务 ~》