【Maven教程】(四)坐标与依赖:坐标概念,依赖配置、范围、传递性和最佳实践 ~
Maven · 坐标与依赖
- 1️⃣ 什么是Maven 坐标
- 2️⃣ 坐标详解
- 3️⃣ 依赖的配置
- 4️⃣ 依赖范围
- 5️⃣ 传递性依赖
- 6️⃣ 依赖调解
- 7️⃣ 可选依赖
- 8️⃣ 最佳实践
- 8.1 排除依赖
- 8.2 归类依赖
- 8.3 优化依赖
- 🌾 总结

正如前面文章所述,Maven 的一大功能是管理项目依赖。为了能自动化地解析任何一个 Java 构件, Maven 就必须将它们唯一标识,这就依赖管理的底层基础——坐标。本节将详细分析 Maven 坐标的作用,解释其每一个元素;在此基础上,再介绍如何配置Maven, 以及相关的经验和技巧,以帮助我们管理项目依赖。
1️⃣ 什么是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 项目才能引用该项目生成的构件,见下图。

2️⃣ 坐标详解
Maven 坐标为各种构件引入了秩序,任何一个构件都必须明确定义自己的坐标,而 一 组 Maven 坐标是通过一些元素定义的,它们是 groupld、artifactld、version、packaging、classifier。先看一组坐标定义,如下:
<groupId>org.sonatype.nexus</groupId>
<artifactId>nexus-indexer</artifactId>
<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 的依赖管理了。
3️⃣ 依赖的配置
上面介绍了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:用来排除传递性依赖,后面会详细介绍。
大部分依赖声明只包含基本坐标,然而在一些特殊情况下,其他元素至关重要。后面会对它们的原理和使用方式详细介绍。
4️⃣ 依赖范围
本节将详细解释什么是依赖范围,以及各种依赖范围的效果和用途。
首先需要知道, 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.sql</groupId><artifactId>jdbc-stdext</artifactId><version>2.0</version><scope>system</scope><systemPath>${java.home}/lib/rt.jar</systemPath> </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仓库之外的类库文件 |
5️⃣ 传递性依赖
考虑一个基于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-logging</groupId><artifactId>commons-logging</artifactId><version>1.1.1</version>
</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。
6️⃣ 依赖调解
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) 就会被解析使用。
7️⃣ 可选依赖
假设有这样一个依赖关系,项目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.0</modelVersion><groupId>com.xiaoshan.mvnbook</groupId> <artifactId>project-b</artifactId><version>1.0.0</version><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>
</project>
上述 XML代码片段中,使用<optional>元素表示 mysql-connector-java 和 postgresql 这两个依赖为可选依赖,它们只会对当前项目B产生影响,当其他项目依赖于B的时候,这两个依赖不会被传递。因此,当项目A依赖于项目B的时候,如果其实际使用基于MySQL数据库,那么在项目A中就需要显式地声明 mysgl-connectorjava这一依赖,见以下代码清单。
<project><modelVersion>4.0.0</modelVersion><groupId>com.xiaoshan.mvnbook</groupId><artifactId>project-a</artifactId><version>1.0.0</version><dependencies><dependency><groupId>com.xiaoshan.mvnbook</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>
</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 驱动依赖。
8️⃣ 最佳实践
Maven 依赖涉及的知识点比较多,在理解了主要的功能和原理之后,最需要的当然就是前人的经验总结了,我们称之为最佳实践。本小节归纳了一些使用Maven 依赖常见的技 巧,方便用来避免和处理很多常见的问题。
8.1 排除依赖
传递性依赖会给项目隐式地引入很多依赖,这极大地简化了项目依赖的管理,但是有些时候这种特性也会带来问题。例如,当前项目有一个第三方依赖,而这个第三方依赖由于某些原因依赖了另外一个类库的SNAPSHOT 版本,那么这个 SNAPSHOT 就会成为当前项目的传递性依赖,而 SNAPSHOT 的不稳定性会直接影响到当前的项目。这时就需要排除掉该SNAPSHOT, 并且在当前项目中声明该类库的某个正式发布的版本。还有 一些情况,你可能也想要替换某个传递性依赖,比如 Sun JTA API, Hibernate 依赖于这个JAR, 但是由于版权的因素,该类库不在中央仓库中,而 Apache Geronimo项目有一个对应的实现。这时你就可以排除 Sun JAT API, 再声明 Geronimo 的 JTA API实现,见如下代码清单。
<project><modelVersion>4.0.0</modelVersion><groupId>com.xiaoshan.mvnbook</groupId><artifactId>project-a</artifactId><version>1.0.0</version><dependencies><dependency><groupId>com.xiaoshan.mvnbook</groupId><artifactId>project-b</artifactId><version>1.0.0</version><exclusions><exclusion><groupId>com.xiaoshan.mvnbook</groupId><artifactId>project-c</artifactId></exclusion></exclusions></dependency><dependency><groupId>com.xiaoshan.mvnbook</groupId><artifactId>project-c</artifactId><version>1.1.0</version></dependency></dependencies>
</project>
上述代码中,项目A依赖于项目B, 但是由于一些原因,不想引入传递性依赖C, 而是自己显式地声明对于项目C 1.1.0版本的依赖。代码中使用 exclusions 元素声明排除依赖,exclusions 可以包含一个或者多个exclusion子元素,因此可以排除一个或者多个传递性依赖。需要注意的是,声明 exclusion 的时候只需要 groupId 和 artifactId,而不需要version元素,这是因为只需要 groupId 和 artifactId就能唯一定位依赖图中的某个依赖。换句话说,Maven解析后的依赖中,不可能出现 groupId 和artifactId相同,但是version不同的两个依赖。该例的依赖解析逻辑如下图所示。

8.2 归类依赖
在前面文章中介绍过,在一个项目中可能有很多关于 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.0</modelVersion><groupId>com.xiaoshan.mvnbook.account</groupId><artifactId>yaccount-email</artifactId><name>AccountEmail</name><version>1.0.0-SNAPSHOT</version><properties><springframework.version>2.5.6</springframework.version></properties><dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-core</artifactId><version>${springframework.version}</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-beans</artifactId><version>${springframework.version}</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactIds><version>${springframework.version}</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context-support</artifactId><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 是同样的道理,不同的只是语法。
8.3 优化依赖
在软件开发过程中,程序员会通过重构等方式不断地优化自己的代码,使其变得更简 洁、更灵活。同理,程序员也应该能够对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教程】(四)坐标与依赖:坐标概念,依赖配置、范围、传递性和最佳实践 ~
Maven 坐标与依赖 1️⃣ 什么是Maven 坐标2️⃣ 坐标详解3️⃣ 依赖的配置4️⃣ 依赖范围5️⃣ 传递性依赖6️⃣ 依赖调解7️⃣ 可选依赖8️⃣ 最佳实践8.1 排除依赖8.2 归类依赖8.3 优化依赖 🌾 总结 正如前面文章所述,Maven 的一大功能是管理项目依赖…...
Java“牵手”京东店铺所有商品API接口数据,通过店铺ID获取整店商品详情数据,京东店铺所有商品API申请指南
京东平台店铺所有商品数据接口是开放平台提供的一种API接口,通过调用API接口,开发者可以获取京东整店的商品的标题、价格、库存、月销量、总销量、库存、详情描述、图片、价格信息等详细信息 。 获取店铺所有商品接口API是一种用于获取电商平台上商品详…...
TuyaOS开发学习笔记(1)——NB-IoT开发搭建环境、编译烧写(MT2625)
一、搭建环境 1.1 官方资料 TuyaOS 1.2 安装VMware 官网下载:https://customerconnect.vmware.com/en/downloads/info/slug/desktop_end_user_computing/vmware_workstation_pro/16_0 百度网盘:https://pan.baidu.com/s/1oN7H81GV0g6cD9zsydg6vg 提取…...
Css 将div设置透明度,并向上移50px,盖住上面的元素一部分
可以使用CSS中的opacity和position属性来实现。 首先,将div的opacity属性设置为小于1的值,比如0.5,这样就可以设置透明度了。其次,将div的position设置为relative,然后再将它向上移动50px,即可盖住上面的元…...
HTTPS安全通信和SSL Pinning
随着互联网的迅速发展,网络通信安全问题日益凸显。在这一背景下,HTTPS作为一种加密通信协议得到了广泛应用,以保障用户的数据隐私和信息安全。本文将介绍HTTPS的基本原理、发展历程,以及与之相关的中间人攻击和防护方法。 1. HTT…...
PHP自己的框架PDO数据表前缀、alias、model、table、join方法实现(完善篇九--结束)
一、实现功能,数据表前缀、alias、model、table、join方法实现 二、表前缀实现 1、config.php增加表前缀 DB_PEX>fa_,//数据库前缀 2、增加表前缀方法function.php function model($table){$modelnew ModelBase($table,config("DB_PEX"));return $m…...
华为OD:敏感字段加密
题目描述: 给定一个由多个命令字组成的命令字符串: 1、字符串长度小于等于127字节,只包含大小写字母,数字,下划线和偶数个双引号; 2、命令字之间以一个或多个下划线_进行分割; 3、可以通过两个双引号”"来标识包含下划线…...
IDEA新建SpringBoot项目时启动编译报错:Error:java: 无效的源发行版: 17
文章目录 原因检查解决步骤修改jdk修改SpringBoot版本 原因 出现这种错误的原因可能是: 本机默认使用(编译)的jdk与该项目所使用的jdk版本不同。 jdk版本不适用于这个Idea,很典型的一个例子就是使用的Idea是2020的,而…...
【云原生进阶之PaaS中间件】第一章Redis-2.3.3集群模式
1 集群模式 Redis集群是一个提供在多个Redis节点之间共享数据的程序集。它并不像Redis主从复制模式那样只提供一个master节点提供写服务,而是会提供多个master节点提供写服务,每个master节点中存储的数据都不一样,这些数据通过数据分片的方式被自动分割到不同的master节点上…...
游戏发行商能够提供什么服务?
游戏发行商可以为游戏开发者提供广泛的服务,以帮助他们将游戏成功地引入市场并取得更好的业绩。以下是游戏发行商可能提供的一些服务: 市场营销和宣传:发行商通常具有丰富的市场营销经验,可以制定并执行有效的宣传和营销策略。他们…...
Linux 多进程解决客户端与服务器端通信
写一个服务器端用多进程处理并发,使两个以上的客户端可以同时连接服务器端得到响应。每当接受一个新的连接就fork产生一个子进程,让子进程去处理这个连接,父进程只用来接受连接。 与多线程相比的不同点:多线程如果其中一个线程操…...
Scala的模式匹配
Scala的模式匹配 Scala 中的模式匹配类似于Java 中的 switch 语法:下面是java中switch代码: int i 10 switch (i) {case 10 :System.out.println("10");break; case 20 :System.out.println("20");break; default :System.out.pr…...
HttPClient简介及示例:学习如何与Web服务器进行通信
文章目录 前言一、引入依赖二、使用步骤1.创建被调用者2.创建调用者三、结果被调用者服务:调用者服务: 总结 前言 欢迎来到本篇博客,这是一个关于HttPClient的入门案例的指南。🎉 在今天的网络世界中,与服务器进行数据…...
STS4 New 安装Spring Bean Configuration File
背景介绍 在创建spring项目后,如果想想创建spring bean Configuration的时候,发下菜单没有这个选项,需要通过下载Spring Roo插件可满足该操作。 参考案例 参考地址: STS4 New 菜单没有Spring Bean Configuration File选项_SQZHA…...
Java经典面试题(异或运算)
不爱生姜不吃醋⭐️⭐️⭐️ 🌻如果本文有什么错误的话欢迎在评论区中指正哦💗 🌻看完之后觉得不错的话麻烦动动小手点个赞赞吧👍 🌻与其明天开始,不如现在行动!💪 🌻大家…...
No primary or single unique constructor found for interface java.util.List
1.问题描述 前端 请求的参数 是 query形式, 参数在url中 报错信息: java.lang.IllegalStateException: No primary or single unique constructor found for interface java.util.List2.解决办法 controller中的 请求方法 参数 加上 RequestParam...
C#关于WebService中File.Exists()处理远程路径的异常记录
目录 前言方案一打开网站对应的程序池的高级设置按下图步骤设置凭据重启网站若方案一未能解决,请继续尝试方案二👇 方案二从控制面板进入到 凭据管理器为windows凭据添加凭据点击**Windows凭据**,并点击**添加Windows凭据**键入远程路径的地址…...
JavaWeb_LeadNews_Day10-Xxljob, Redis实现定时热文章
JavaWeb_LeadNews_Day10-Xxljob, Redis实现定时热文章 xxl-job概述windows部署调度中心docker部署调度中心 xxl-job入门案例xxl-job分片广播热点文章定时计算思路分析具体实现热文章计算定时计算 查询文章接口改造来源Gitee xxl-job概述 windows部署调度中心 运行 xxl-job\do…...
【WebRTC---源码篇】(二:二)视频源VideoSourceBase
作用 这个类继承自VideoSourceInterface<webrtc::VideoFrame>模板类,并且可以处理webrtc::VideoFrame class VideoSourceBase : public VideoSourceInterface<webrtc::VideoFrame> 重要成员变量 struct SinkPair {SinkPair(VideoSinkInterface<webrtc::Vid…...
Linux_8_磁盘存储和文件系统
1 磁盘结构 1.1 设备文件 一切皆文件: open(),read(),write(),close() 设备文件:关联至一个设备驱动程序,进而能够跟与之对应硬件设备进行通信 设备号码: 主设备号 major number,标识设备类型 次设备号 minor number,标识同一类型下的不同设备 设备类型:…...
【Axure高保真原型】引导弹窗
今天和大家中分享引导弹窗的原型模板,载入页面后,会显示引导弹窗,适用于引导用户使用页面,点击完成后,会显示下一个引导弹窗,直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...
深入浅出Asp.Net Core MVC应用开发系列-AspNetCore中的日志记录
ASP.NET Core 是一个跨平台的开源框架,用于在 Windows、macOS 或 Linux 上生成基于云的新式 Web 应用。 ASP.NET Core 中的日志记录 .NET 通过 ILogger API 支持高性能结构化日志记录,以帮助监视应用程序行为和诊断问题。 可以通过配置不同的记录提供程…...
CMake基础:构建流程详解
目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...
Cloudflare 从 Nginx 到 Pingora:性能、效率与安全的全面升级
在互联网的快速发展中,高性能、高效率和高安全性的网络服务成为了各大互联网基础设施提供商的核心追求。Cloudflare 作为全球领先的互联网安全和基础设施公司,近期做出了一个重大技术决策:弃用长期使用的 Nginx,转而采用其内部开发…...
相机Camera日志分析之三十一:高通Camx HAL十种流程基础分析关键字汇总(后续持续更新中)
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了:有对最普通的场景进行各个日志注释讲解,但相机场景太多,日志差异也巨大。后面将展示各种场景下的日志。 通过notepad++打开场景下的日志,通过下列分类关键字搜索,即可清晰的分析不同场景的相机运行流程差异…...
WEB3全栈开发——面试专业技能点P2智能合约开发(Solidity)
一、Solidity合约开发 下面是 Solidity 合约开发 的概念、代码示例及讲解,适合用作学习或写简历项目背景说明。 🧠 一、概念简介:Solidity 合约开发 Solidity 是一种专门为 以太坊(Ethereum)平台编写智能合约的高级编…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台
🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...
代理篇12|深入理解 Vite中的Proxy接口代理配置
在前端开发中,常常会遇到 跨域请求接口 的情况。为了解决这个问题,Vite 和 Webpack 都提供了 proxy 代理功能,用于将本地开发请求转发到后端服务器。 什么是代理(proxy)? 代理是在开发过程中,前端项目通过开发服务器,将指定的请求“转发”到真实的后端服务器,从而绕…...
以光量子为例,详解量子获取方式
光量子技术获取量子比特可在室温下进行。该方式有望通过与名为硅光子学(silicon photonics)的光波导(optical waveguide)芯片制造技术和光纤等光通信技术相结合来实现量子计算机。量子力学中,光既是波又是粒子。光子本…...
