【Maven教程】(四)坐标与依赖:坐标概念,依赖配置、范围、传递性和最佳实践 ~

这篇具有很好参考价值的文章主要介绍了【Maven教程】(四)坐标与依赖:坐标概念,依赖配置、范围、传递性和最佳实践 ~。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

【Maven教程】(四)坐标与依赖:坐标概念,依赖配置、范围、传递性和最佳实践 ~,Maven教程,maven,java,jvm,开发语言,学习,java-ee,经验分享

正如前面文章所述,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 项目才能引用该项目生成的构件,见下图。

【Maven教程】(四)坐标与依赖:坐标概念,依赖配置、范围、传递性和最佳实践 ~,Maven教程,maven,java,jvm,开发语言,学习,java-ee,经验分享

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 的一个传递性依赖,如下图所示。

【Maven教程】(四)坐标与依赖:坐标概念,依赖配置、范围、传递性和最佳实践 ~,Maven教程,maven,java,jvm,开发语言,学习,java-ee,经验分享

有了传递性依赖机制,在使用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.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有任何影响,如下图所示。

【Maven教程】(四)坐标与依赖:坐标概念,依赖配置、范围、传递性和最佳实践 ~,Maven教程,maven,java,jvm,开发语言,学习,java-ee,经验分享

为什么要使用可选依赖这一特性呢? 可能项目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不同的两个依赖。该例的依赖解析逻辑如下图所示。

【Maven教程】(四)坐标与依赖:坐标概念,依赖配置、范围、传递性和最佳实践 ~,Maven教程,maven,java,jvm,开发语言,学习,java-ee,经验分享

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

在项目中执行该命令,结果如图所示。

【Maven教程】(四)坐标与依赖:坐标概念,依赖配置、范围、传递性和最佳实践 ~,Maven教程,maven,java,jvm,开发语言,学习,java-ee,经验分享
上图显示了所有 account-email 的已解析依赖,同时,每个依赖的范围也得以明确标示。
在此基础上,还能进一步了解已解析依赖的信息。将直接在当前项目POM 声明的依赖 定义为顶层依赖,而这些顶层依赖的依赖则定义为第二层依赖,以此类推,有第三、第四层依赖。当这些依赖经 Maven 解析后,就会构成一个依赖树,通过这棵依赖树就能很清楚地看到某个依赖是通过哪条传递路径引入的。可以运行如下命令查看当前项目的依赖树:

mvn dependency:tree

在项目中执行该命令,效果如下图所示。

【Maven教程】(四)坐标与依赖:坐标概念,依赖配置、范围、传递性和最佳实践 ~,Maven教程,maven,java,jvm,开发语言,学习,java-ee,经验分享
从图中能够看到,虽然我们没有声明 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

结果如下图所示。

【Maven教程】(四)坐标与依赖:坐标概念,依赖配置、范围、传递性和最佳实践 ~,Maven教程,maven,java,jvm,开发语言,学习,java-ee,经验分享
该结果中重要的是两个部分。首先是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仓库—布局、分类和配置,远程仓库的认证与部署,快照版本,依赖解析机制,镜像和搜索服务 ~》
文章来源地址https://www.toymoban.com/news/detail-696705.html

【Maven教程】(四)坐标与依赖:坐标概念,依赖配置、范围、传递性和最佳实践 ~,Maven教程,maven,java,jvm,开发语言,学习,java-ee,经验分享

到了这里,关于【Maven教程】(四)坐标与依赖:坐标概念,依赖配置、范围、传递性和最佳实践 ~的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • maven的依赖范围scope使用

    标签的位置:dependencies/dependency/scope 标签的可选值:compile/test/provided/system/runtime/import #①compile 和 test 对比 main目录(空间) test目录(空间) 开发过程(时间) 部署到服务器(时间) compile 有效 有效 有效 有效 test 无效 有效 有效 无效 #②compile 和 provided 对比 main目录(空间

    2024年02月10日
    浏览(43)
  • 聊聊Maven的依赖传递、依赖管理、依赖作用域

    在Maven中,依赖是会传递的,假如在业务项目中引入了 spring-boot-starter-web 依赖: 那么业务项目不仅直接引入了 spring-boot-starter-web 依赖,还间接引入了 spring-boot-starter-web 的依赖项: spring-boot-starter 、 spring-boot-starter-json 、 spring-boot-starter-tomcat 、 spring-web 、 spring-webmvc 。 Maven依

    2024年02月08日
    浏览(40)
  • 【Maven】依赖管理—导入jar包的三种方式、依赖范围设置

    一、使用坐标导入 jar 包  二、使用坐标导入 jar 包 – 快捷方式  三、使用坐标导入 jar 包 – 自动导入  四、依赖范围 1、在 pom.xml 中编写 dependencies 标签 2、在 dependencies 标签中 使用 dependency 引入坐标 3、定义坐标的 groupId,artifactId,version 4、点击刷新按钮,使坐标生效 1、

    2024年02月16日
    浏览(55)
  • Maven 依赖传递和冲突、继承和聚合

    1.1.1 概念         假如有三个 Maven 项目 A 、 B 和 C ,其中项目 A  依赖 B ,项目 B  依赖 C 。那么我们可以说 A 依赖 C 。也就是说,依赖的关系为: A—B—C , 那么我们执行项目 A 时,会自动把 B 和 C 都下载导入到 A 项目的 jar 包文件夹中,这就是依赖的传递性。 1.1.2 作

    2024年01月21日
    浏览(47)
  • JAVA-MAVEN初学者教程(配置、pom.xml、依赖管理等)

    Java的包管理工具有Maven、Gradle等,其中Maven是一款服务于Java平台的自动化构建工具,把整个过程抽象成一个项目对象模型(Project Object Model,POM),它不仅可以用作包管理,还有许多的 插件 ,可以支持整个项目 的开发、打包、测试及部署 等一系列行为。Gradle是一个基于Apa

    2024年02月09日
    浏览(54)
  • 【Maven教程】(五)仓库:解析Maven仓库—布局、分类和配置,远程仓库的认证与部署,快照版本,依赖解析机制,镜像和搜索服务 ~

    上文详细介绍了Maven 坐标和依赖,坐标和依赖是任何一个构件在Maven 世界中的逻辑表示方式;而构件的物理表示方式是文件, Maven 通过仓库来统一管理这些文件。本文将详细介绍 Maven 仓库,在了解了Maven 如何使用仓库之后,将能够更高效地使用 Maven。 在Maven 世界中,任何一

    2024年02月09日
    浏览(41)
  • 解决maven 父工程依赖传递导致的 java.lang.NoClassDefFoundError: org/elasticsearch/xcontent/ToXContentObject

    因为项目需要,最近在学习elasticsearch,在使用elasticsearch Java 客户端时,出现了写问题,主要就是报各种的 NoClassDefFoundError 如: java.lang.NoClassDefFoundError: org/elasticsearch/xcontent/ToXContentObject ,出现这种 NoClassDefFoundError 的问题基本上就是maven 依赖错误或者版本不对,于是顺着这个

    2023年04月08日
    浏览(60)
  • IDEA 配置 Maven(解决依赖下载缓慢)

    第四步 主要讲了在IDEA中配置Maven,并且导入本地自己下载的Maven,速度直接起飞!!!听我一句劝, 不要用镜像,慢的要死。自己下一个 ,然后每次用的时候一导入,速度很快!!!! Maven 是专门用于管理和构建 Java 项目的工具,Apache Maven 是一个项目管理和构建工具,它基

    2024年02月03日
    浏览(46)
  • Maven本地配置获取nexus私服的依赖

    Nexus-在项目中使用Maven私服,Deploy到私服、上传第三方jar包、在项目中使用私服jar包: Nexus-在项目中使用Maven私服,Deploy到私服、上传第三方jar包、在项目中使用私服jar包_nexus maven-releases 允许deploy-CSDN博客 在上面讲的是在需要拉取私服依赖的项目中的pom中配置repository的方式去

    2024年02月05日
    浏览(46)
  • 新建SpringBoot Maven项目中pom常用依赖配置及常用的依赖的介绍

    完整的pom文件放在后面 1.springboot项目的总(父)依赖大全 当我们使用 spring 或 spring-boot 开发项目时,需要引入很多依赖,包括 spring 本身的组件、各种 spring-boot-starter、以及其它第三方依赖(如:slf4j、redis)。依赖多了,版本的选择是个问题,就怕哪个版本选择的不对导致出现

    2024年02月06日
    浏览(52)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包