【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 坐标与依赖 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,标识同一类型下的不同设备 设备类型:…...
VS + QT 封装带UI界面的DLL
一、创建编译DLL的项目 1.新建Qt Class Liabrary 2.新建项目,选择Qt Widgets Class 3.新建C类,可以在此类里面写算法函数用于调用。 4.下面是添加完Qt窗体类和C类之后的项目截图 5.修改头文件并编译 将uidemo_global.h中的ifdef内容复制到dialog.h上…...
逆向工程-架构真题(二十)
结构化程序设计采用自顶向下、逐步求精及模块化程序设计方法,通过()三种基本控制结构可以构造出任何单入口单出口程序。 顺序、选择和嵌套顺序、分支和循环分支、并发和循环跳转、选择和并发 答案:B 解析: 结构化设…...
Zookeeper 入门
第 1 章 Zookeeper 入门 1.1概述 Zookeeper从设计模式角度来理解:是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper就将…...
记录--前端使用a链接下载内容增加loading效果
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。 代码如下: // util…...
如何获取用户的ip地址
用户的 IP 地址可能会被隐藏或者修改,例如使用代理服务器、VPN 等工具,这样就无法准确获取用户的真实 IP 地址。 除了以上特殊情况,一般情况下 用户访问可能会经过一下链路 : 前端—>nginx—>网关—>服务 。 一般情况下后…...
单片机-控制按键点亮LED灯
1、按键电路图 定义四个按键引脚 1、按键按下 为 输入为低电平 2、按键不按下 IO有上拉电阻,为高电平 // 定义 按键的 管教 sbit KEY1 P3^1; sbit KEY2 P3^0; sbit KEY3 P3^2; sbit KEY4 P3^3; 2、LED灯电路图 LED 输出高电平为亮 // 定义LED灯 管教 sbit LED1…...
微信小程序新版隐私协议弹窗实现最新版
1. 微信小程序又双叒叕更新了 2023.08.22更新: 以下指南中涉及的 getPrivacySetting、onNeedPrivacyAuthorization、requirePrivacyAuthorize 等接口目前可以正常接入调试。调试说明: 在 2023年9月15号之前,在 app.json 中配置 __usePriva…...
GO语言圣经 第五章习题
练习5.1 修改findlinks代码中遍历n.FirstChild链表的部分,将循环调用visit,改成递归调用。 func visit(links []string, n *html.Node) []string {if n nil {return links}if n.Type html.ElementNode && n.Data "a" {for _, a : r…...
用kotlin 开发一个简单的多页面跳转
本文介绍一个简单的安卓应用的页面跳转例子,用的是kotlin。 运行时主页面是一个hello 和Jump 按钮,你按一下jump 按钮就转到 从页面,只是标识从页面。 开始建立一个简单工程,名为hello, 选择的是Empty views Activit…...
记录我的tensorrt 部署yolov8
系统 :ubuntu 18.04 代码 :GitHub - noahmr/yolov5-tensorrt: Real-time object detection with YOLOv5 and TensorRT conda 环境 : GitHub - noahmr/yolov5-tensorrt: Real-time object detection with YOLOv5 and TensorRT cuda : 11.8 …...
做网站什么类型好/百度下载安装
多线程的创建 继承Thread类实现Runnable接口实现Callable接口 三种方法都必须重写run()方法 在多线程中每一个线程都存在优先级,较高优先级会比较低优先级先执行。 下面主要介绍前两种方法。 通过继承Thread类 主要步骤: 重…...
阿里巴巴网站域名建设/深圳网络推广
1. 需要使用svnant,从SVN中获取源码 需要使用的扩展包:svnant-1.3.1.zip里所有的jar 下载地址:http://subclipse.tigris.org/files/documents/906/49042/svnant-1.3.1.zip build.xml中的写法 <!--定义SVN地址--><property name"…...
南昌地宝网免费发布/ios aso优化工具
我有一个文件格式如下:20150426010203 name120150426010303 name220150426010307 name320150426010409 name120150426010503 name420150426010510 name1我有兴趣找出列表中name1出现的时间差,然后计算这些出现的频率(例如,delta time1s出现20…...
水源logo设计制作网/广州百度快速排名优化
在整理《全唐诗》的文本之前,我们首先需要完成以下两个步骤: 确定需求 了解文本 在完成以上步骤后,我们开始实际着手整理文本,在整理的过程中大体上也包含两个流程: 文本解析结果输出 全唐诗文本语料在“全唐诗.tx…...
b2b电商网站建设/品牌广告语
引言 前一篇文章中讲解了 Web Deploy 技术的简单使用,以及避免已有的 ACL 设置被清除的办法。 而这一次我将会讲解在使用 Visual Studio (Express) 进行一键发布时自动完成 ACL 设置的办法。 原理 在解决上一篇文章的问题的过程中中提到了 MSBuild 指令的使用&#…...
网站3d展示怎么做的/网络推广方式有哪几种
蛋花花谈程序员年过四十该何去何从,据蛋花花了解程序员这个行业可以说是一碗青春饭。蛋花花认为除开年纪大了,技术跟不上,学习能力下降,等等的工作职能方面的问题。还有就是一个自身身体健康的问题。 蛋花花在业界流传着这么一句话…...