【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 坐标的元素包括 groupldartifactldversionpackagingclasifer。现在,只要我们提供正确的坐标元素, 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 坐标是通过一些元素定义的,它们是 groupldartifactldversionpackagingclassifier。先看一组坐标定义,如下:

<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-corespring-context 等。这是由于Maven 中模块的概念,因此,一个实际项目往往会被划分成很多模块。其次, groupId不应该对应项目隶属的组织或公司。原因很简单,一个组织下会有很多实际项目,如果 groupId 只定义到组织级别, 而后面我们会看到,artifactId 只能定义Maven 项目(模块), 那么实际项目这个层将难以定义。最后, groupId 的表示方式与Java包名的表示方式类似,通常与域名反向一一对应。上例中, groupIdorg.sonatype.nexus, org.sonatype 表示 Sonatype 公司建立的一 个非盈利性组织,nexus 表示 Nexus 这一实际项目,该 groupId 与域名 nexus.sonatype.org 对应。
  • artifactId:该元素定义实际项目中的一个Maven项目(模块), 推荐的做法是使用实际项目名称作为 artifactId 的前缀。比如上例中的 artifactIdnexus-indexer, 使用了实际项目名 nexus 作为前缀,这样做的好处是方便寻找实际构件。在默认情况下, Maven生成的构件,其文件名会以 artifactId 作为开头,如 nexus-indexer-2.0.0.jar, 使用实际项目名称作为前缀之后,就能方便从一个 lib文件夹中找到某个项目的一组构件。考虑有5个项目,每个项目都有一个 core模块,如果没有前缀,我们会看到很多 core-1.2.jar这样的文件,加上实际项目名前缀之后,便能很容易区分 foo-core-1.2.jarbar-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.jarnexus-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配置文件中一些简单的依赖配置,可以看到依赖会有基本的 groupIdartifactIdversion 等元素组成。其实一个依赖声明还可以包含如下的一些元素:

<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 是传递性依赖。第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围,如下表所示,最左边一列表示第一直接依赖范围,最上面一行表示第二直接依赖范围,中间的交叉单元格则表示传递性依赖范围。

compiletestprovidedruntime
compilecompile--runtime
testtest--test
providedprovided-providedprovided
runtimeruntime--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.4account-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-javapostgresql 这两个依赖为可选依赖,它们只会对当前项目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-mysqlcom.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, 再声明 GeronimoJTA 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 的时候只需要 groupIdartifactId,而不需要version元素,这是因为只需要 groupIdartifactId就能唯一定位依赖图中的某个依赖。换句话说,Maven解析后的依赖中,不可能出现 groupIdartifactId相同,但是version不同的两个依赖。该例的依赖解析逻辑如下图所示。

在这里插入图片描述

8.2 归类依赖

在前面文章中介绍过,在一个项目中可能有很多关于 Spring Framework 的依赖,它们分别是 org.springframework:spring-core:2.5.6org.springframework:spring-beans:2.5.6org.springframework:spring-context:2.5.6org.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:listdependeney:tree 可以帮助我们详细了解项目中所有依赖的具体 信息,在此基础上,还有 dependency:analyze 工具可以帮助分析当前项目的依赖。

为了说明该工具的用途,先将 spring-context 这一依赖删除,然后构建项目,你会发现编译、测试和打包都不会有任何问题。通过分析依赖树,可以看到 spring-contextspring-context-support 的依赖,因此会得以传递到项目的 classpath 中。现在再运行如下命令:

mvn dependency:analyze

结果如下图所示。

在这里插入图片描述
该结果中重要的是两个部分。首先是Used undeclared dependencies, 意指项目中使用到的,但是没有显式声明的依赖,这里是 spring-context。 这种依赖意味着潜在的风险,当前项目直接在使用它们,例如有很多相关的Java import 声明,而这种依赖是通过直接依赖传递进来的,当升级直接依赖的时候,相关传递性依赖的版本也可能发生变化,这种变化不易察觉,但是有可能导致当前项目出错。例如由于接口的改变,当前项目中的相关代码无法编译。这种隐藏的、潜在的威胁一旦出现,就往往需要耗费大量的时间来查明真相。因此, 显式声明任何项目中直接用到的依赖。

结果中还有一个重要的部分是 Unused declared dependencies, 意指项目中未使用的,但 显式声明的依赖,这里有 spring-corespring-beans、 需要注意的是, 对于这样一类依赖, 我们不应该简单地直接删除其声明,而是应该仔细分析。由于dependeney:analyze 只会分析编译主代码和测试代码需要用到的依赖, 一些执行测试和运行时需要的依赖它就发现不了。 很显然,该例中的 spring-corespring-beans 是 运 行 Spring Framework 项目必要的类库,因此不应该删除依赖声明。当然,有时候确实能通过该信息找到一些没用的依赖,但一定要小心测试。

🌾 总结

本文主要介绍了Maven 的两个核心概念:坐标和依赖。解释了坐标的由来,并详细阐述了各坐标元素的作用及定义方式。随后引入了一个项目实际的基于 Spring Framework 的模块,包括了POM 定义、业务代码和测试代码。在这一直观感受的基础上,再花篇幅介绍了 Maven 依赖,包括依赖范围、传递性依赖、可选依赖等概念。最后,当然少不了关于依赖的一些最佳实践。通过阅读本文,大家应该已经能够透彻地了解 Maven 的依赖管理机制。下一节将会介绍 Maven 的另一个核心概念:仓库。


温习回顾上一篇(点击跳转)
《【Maven教程】(三)基础使用篇:入门使用指南——POM编写、业务代码、测试代码、打包与运行、使用Archetype生成项目骨架~》

继续阅读下一篇(点击跳转)
《【Maven教程】(五)仓库:解析Maven仓库—布局、分类和配置,远程仓库的认证与部署,快照版本,依赖解析机制,镜像和搜索服务 ~》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/126773.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

sonarqube版本升级

官方文档&#xff1a;Upgrade guide 步骤1、停止原有sonarqube服务&#xff0c;如果是docker部署的直接停掉容器并删除 步骤2、部署最新版sonarqube&#xff0c;保留原有配置 步骤3、访问sonarqube web 显示维护中&#xff0c;根据官方给出的升级方法&#xff0c;在sonarqub…

学习笔记|小数点控制原理|数码管动态显示|段码跟位码|STC32G单片机视频开发教程(冲哥)|第十集:数码管动态显示

文章目录 1.数码管动态刷新的原理2.动态刷新原理3.8位数码管同时点亮新建一个数组选择每个位需要显示的内容实战小练&#xff1a;简易10秒免单计数器将刷新动作写成函数 课后练习: 1.数码管动态刷新的原理 上述图片引用自&#xff1a;51单片机初学2-数码管动态扫描 用一排端口来…

mysql 大表如何ddl

大家好&#xff0c;我是蓝胖子&#xff0c;mysql对大表(千万级数据)的ddl语句&#xff0c;在生产上执行时一定要千万小心&#xff0c;一不小心就有可能造成业务阻塞&#xff0c;数据库io和cpu飙高的情况。今天我们就来看看如何针对大表执行ddl语句。 通过这篇文章&#xff0c;…

推荐一款程序员截图神器!

快来看一下程序员必备的一款截图工具 今天就来和大家说一下作为程序员必备截图神器&#xff0c;几乎每一个程序员都会设置开机自启&#xff0c;因为这个截图功能太太太好用了&#xff01;&#xff01;&#xff01;只要你在键盘上按下F1就可以轻松截取整个屏幕&#xff0c;然后…

OpenHarmony:如何使用HDF驱动控制LED灯

一、程序简介 该程序是基于OpenHarmony标准系统编写的基础外设类&#xff1a;RGB LED。 目前已在凌蒙派-RK3568开发板跑通。详细资料请参考官网&#xff1a;https://gitee.com/Lockzhiner-Electronics/lockzhiner-rk3568-openharmony/tree/master/samples/b02_hdf_rgb_led。 …

【【STM32--28--IO引脚的复用功能】】

STM32–28–IO引脚的复用功能 STM32的IO复用功能 何为复用? 我们先了解一下何为通用 IO端口的输入或输出是由GPIO外设控制&#xff0c;我们称之为通用 复用&#xff1a; IO端口的输入或者是输出是由其他非GPIO外设控制就像经常说的USART 由 DR寄存器进行输出 STM32的IO复用功…

docker 笔记11: Docker容器监控之CAdvisor+InfluxDB+Granfana

1.原生命令 docker stats命令的结果 是什么 2.是什么 容器监控3剑客 CAdvisor监控收集InfluxDB存储数据Granfana展示图表 3.CAdvisor 4.InfluxDB 5.Granfana 6.总结 7.compose容器编排&#xff0c;一套带走 新建目录 7.1新建3件套组合的 docker-compose.yml version: 3.1vo…

通过 Jetbrains GateWay实现Remote Development

本次环境准备 环境准备&#xff1a;win10、一台安装有树莓派系统的树莓派&#xff08;也可以是其他的服务器&#xff09; 第一步&#xff1a;通过官网下载JetBrains Gateway 官网地址&#xff1a;https://www.jetbrains.com/remote-development/gateway/ 第二步&#xff1a;安装…

Shell 运算符及语法结构

目录 一、Shell运算符 1.1 表达式expr 1.2 运算操作 1.3 操作实例 二、Shell条件判断 2.1 基本语法 2.2 值、权限、类型、多条件判断 三、Shell流程控制 3.1 if 流程语法 3.2 case 流程语法 3.3 for 流程语法 3.4 内部运算符 3.5 while循环流程语法 四、Shell读…

ElasticSearch的安装部署-----图文介绍

文章目录 背景什么是ElasticSearch使用场景 ElasticSearch的在linux环境下的安装部署前期准备分配权限(正式实操)启动ElasticSearch创建用户组创建用户&#xff0c;并设置密码用户添加到elasticsearch用户组指定用户操作目录的一个操作权限切换用户 解压elasticsearch修改es的配…

计算机脚本的概念,如何编写、使用脚本 (Script)?

一、脚本的概念和使用场景 在计算机领域的脚本&#xff0c;指的是使用一种特定的描述性语言&#xff0c;依据一定的格式编写的可执行文件脚本语言又被称为扩建的语言或者动态语言, 是一种编程语言, 用来控制软件应用程序, 脚本通常是以文本 (ASCⅡ) 保存, 只是在被调用时进行解…

【Docker】Docker的使用案例以及未来发展、Docker Hub 服务、环境安全的详细讲解

Docker的工具实践及root概念和Docker容器安全性设置 1. 使用案例2. Docker解决的问题3. Docker未来发展4. Docker Hub 服务5. 技术局限6. Docker环境安全7. 容器部署安全 1. 使用案例 Docker是一个命令行工具&#xff0c;它提供了中央“docker”执行过程中所需的所有工具。这使…

奇舞周刊第 505 期:实践指南-前端性能提升 270%!

记得点击文章末尾的“ 阅读原文 ”查看哟~ 下面先一起看下本期周刊 摘要 吧~ 奇舞推荐 ■ ■ ■ 实践指南-前端性能提升 270% 当我们疲于开发一个接一个的需求时&#xff0c;很容易忘记去关注网站的性能&#xff0c;到了某一个节点&#xff0c;猛地发现&#xff0c;随着越来越多…

存储空间压缩6倍 ,多点DMALL零售SaaS场景降本实践

&#x1f9d1;‍&#x1f4bc; 作者简介 冯光普&#xff1a;多点 DMALL 数据库团队负责人&#xff0c;负责数据库稳定性建设与 DB PaaS 平台建设&#xff0c;在多活数据库架构、数据同步方案等方面拥有丰富经验。 杨家鑫&#xff1a;多点高级 DBA&#xff0c;擅长故障分析与性能…

GPT引领前沿热点、AI绘图

GPT对于每个科研人员已经成为不可或缺的辅助工具&#xff0c;不同的研究领域和项目具有不同的需求。如在科研编程、绘图领域&#xff1a; 1、编程建议和示例代码: 无论你使用的编程语言是Python、R、MATLAB还是其他语言&#xff0c;都可以为你提供相关的代码示例。 2、数据可…

深度入门 Android 车机核心 CarService 的构成和链路

作者&#xff1a;TechMerger 本文将结合 Android 系统整体&#xff0c;对 CarService 的构成和链路对其做一个全面的分析和理解。 构成 1. CarServiceHelperService 系统服务 SystemServer 中专门为了 Automotive OS 设立的系统服务&#xff0c;用来管理车机的核心服务 CarS…

【Nginx24】Nginx学习:压缩模块Gzip

Nginx学习&#xff1a;压缩模块Gzip 又是一个非常常见的模块&#xff0c;Gzip 现在也是事实上的 Web 应用压缩标准了。随便打开一个网站&#xff0c;在请求的响应头中都会看到 Content-Encoding: gzip 这样的内容&#xff0c;这就表明当前这个请求的页面或资源使用了 Gzip 压缩…

Java基础——反射

1 概述 Java反射提供了一种动态获取类信息及动态修改运行规则的机制。 反射指运行时获取一个类中的信息&#xff0c;并控制其行为。运行时可以获取构造器对象Constructor&#xff0c;可以获取成员变量对象Field&#xff0c;可以获取方法对象Method 2 获取类对象的方式 获取C…

看完这篇 教你玩转渗透测试靶机Vulnhub——Momentum:2

Vulnhub靶机Momentum:2渗透测试详解 Vulnhub靶机介绍&#xff1a;Vulnhub靶机下载&#xff1a;Vulnhub靶机安装&#xff1a;Vulnhub靶机漏洞详解&#xff1a;①&#xff1a;信息收集&#xff1a;②&#xff1a;漏洞发现&#xff1a;③&#xff1a;文件上传漏洞利用&#xff1a;…

Mysql--事务

事务 开始之前&#xff0c;让我们先想一个场景&#xff0c;有的时候&#xff0c;为了完成某个工作&#xff0c;需要完成多种sql操作 比如转账 再比如下单 第一步 我的账户余额减少 第二步 商品的库存要减少 第三步 订单表中要新增一项 事务的本质&#xff0c;就是为了把多个操…