共计 10030 个字符,预计需要花费 26 分钟才能阅读完成。
这又是一个系列,一个要把 Maven 讲透的系列,希望能够对大家有帮助!
前言
在前面的总结中,总是说到依赖这个东西,而且还有看到 dependencies
这个词在 pom.xml 文件中的使用,所以很多读者就很迫不及待的想知道这个依赖到底是什么东西?作为 Maven 中一个非常重要的概念,那到底该如何使用和配置,以及使用过程中有哪些注意事项,而这篇文章就是对 Maven 中的依赖进行详细的总结,一扫对依赖概念的不解。
依赖的配置
在 Maven 中,是在 pom.xml 文件中完成依赖的配置,我们先来看看依赖配置的语法。
<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
:用来排除传递性依赖,下面会专门进行总结。
很多时候,大部分依赖声明只包含 groupId
、artifactId
和version
这三个指定基本坐标的元素;而在一些特殊情况下,其它元素至关重要,也就是上面提到的 scope
、optional
和exclusions
。下面就对这三个要素进行详细的总结。
依赖范围
不知道大家还记不记得在《Maven 基础教程之使用入门》中的这段 junit 依赖代码:
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
这里就指定了 scope 这个要素的值,那这里指定这个要素有什么含义呢?
我们需要知道,Maven 在编译项目主代码的时候需要使用一套 classpath。举例来说:
- 当 Maven 编译项目主代码的时候如果需要用到 spring-core,该文件以依赖的方式被引入到 classpath 中;
- 当 Maven 编译和执行测试的时候会使用另外一套 classpath,则 junit 文件也会以依赖的方式引入到测试使用的 classpath 中;
- 当 Maven 项目运行时,又会使用一套 classpath。
所以依赖范围就是用来控制依赖与这三种 classpath(编译 classpath、测试 classpath、运行 classpath)的关系。在 Maven 中,我们可以针对 scope 要素设置以下依赖范围:
-
compile
:编译依赖范围。如果没有指定 scope 值,就会默认使用该依赖。使用该依赖范围的 Maven 依赖,对于编译、测试、运行三种 classpath 都有效; -
test
:测试依赖范围。使用此依赖范围的 Maven 依赖,只对于测试 classpath 有效,在编译主代码或者运行项目时都无法使用此依赖。对于上面的 junit 例子,它只有在编译测试代码及运行测试的用例的时候才需要; -
provided
:已提供依赖范围。使用此依赖范围的 Maven 依赖,对于编译和测试 classpath 有效,但在运行时无效。最典型的例子是 servlet-api,编译和测试项目的时候需要该依赖,但在运行项目的时候,由于容器已经提供,就不需要 Maven 重复地引入一遍; -
runtime
:运行时依赖范围。使用此依赖范围的 Maven 依赖,对于测试和运行 classpath 有效,但在编译主代码时无效。最典型的例子就是 JDBC 驱动实现,项目主代码的编译只需要 JDK 提供的 JDBC 接口,只有在执行测试或者运行项目的时候才需要实现上述接口的具体 JDBC 驱动。 -
system
:系统依赖范围。该依赖与三种 classpath 的关系和 provided 依赖范围完全一致。但是,使用 system 范围的依赖时必须通过 systemPath 元素显式地指定依赖文件的路径。由于此类依赖不是通过 Maven 仓库解析的,而且往往与本机系统绑定,可能造成构建的不可移植,因此谨慎使用。system
的使用举例:<dependency> <groupId>com.jellythink.BookStore</groupId> <artifactId>BookStore-SSO</artifactId> <version>1.0</version> <scope>system</scope> <systemPath>${basedir}/lib/BookStore-SSO-1.0.jar</systemPath> </dependency>
对于
system
系统依赖范围,在进行以上配置以后,编写代码时已经可以引入 Jar 包中的 class 了,但是在打包时,由于scope=system
,默认并不会将依赖包打进 WAR 包中,所有需要通过插件进行打包。例如:<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>2.10</version> <executions> <execution> <id>copy-dependencies</id> <phase>compile</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/lib</outputDirectory> <includeScope>system</includeScope> </configuration> </execution> </executions> </plugin>
会了更好的理解和记忆依赖范围与 classpath 的关系,将上述内容总结成一张表格。
依赖范围(scope) | 对于编译 classpath 有效 | 对于测试 classpath 有效 | 对于运行时 classpath 有效 | 例子 |
---|---|---|---|---|
compile | Y | Y | Y | spring-core |
test | – | Y | – | junit |
provided | Y | Y | – | servlet-api |
runtime | – | Y | Y | JDBC 驱动实现 |
system | Y | Y | – | 本地的,Maven 仓库之外的类库文件 |
依赖传递的几个注意事项
说到依赖传递,这里的关系就比较复杂,在没有使用 Maven 之前,大家是否有这样的开发体验;比如引入了包 A,由于我们不知道包 A 的依赖,只能在编译的时候,根据出错信息,再加入需要的其它依赖,很显然,这样的开发体验是及其糟糕的。而现在有了 Maven,Maven 中的传递性依赖机制可以很好的解决这一问题。这里就来详细的总结 Maven 中的依赖传递。
- 啥是依赖传递
在实际项目中,我们肯定会遇到如下图所示的这种依赖情况:A 依赖 B,B 又依赖 C。由于基于 Maven 创建的项目,有了传递性依赖机制,在使用 A 的时候就不用去考虑 A 依赖了什么,也不用担心引入多余的依赖。Maven 会解析各个直接依赖的 POM,将那些必要的间接依赖,以传递性依赖的形式引入到当前的项目中。
- 依赖传递的范围
在传递性依赖中,如上图所示的 A 依赖 B,B 依赖 C,我们就说 A 与 B 是第一直接依赖,B 与 C 是第二直接依赖,C 对于 A 是传递性依赖。第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围,下面通过一张表格来说明这种传递性依赖范围:对于上面的图,最左面的一列表示第一直接依赖范围,最上面一行表示第二直接依赖范围。比如 A 对 B 的依赖 scope 是 compile,B 对 C 的依赖 scope 是 runtime,那么 A 对 C 的依赖 scope 就是 runtime。如下图标注所示:
在实际使用过程中,对于这个依赖传递范围的关注还是比较少的,在以后的使用过程中,如果遇到问题,我们应该能想到这里总结的依赖传递范围相关的知识点。
-
依赖调解
先来说一个实际开发过程中经常会遇到的两个问题。问题一:比如项目 A 有这样的两个依赖关系:
从上图可以看到,项目 A 有两条依赖关系,X 是 A 的传递性依赖,但是你会发现两条依赖路径上有两个版本的 X,那么哪个 X 会被 Maven 解析使用呢?两个版本都被解析显然是不对的,因为那会造成依赖重复,因此必须选择一个。这种问题,该如何解决呢?
问题二:比如项目 A 有这样的两个依赖关系:
对于这种情况,和问题一的区别就是 X 作为 A 的传递性依赖,两个版本的依赖路径长度是一样的,都是 2,这情况下,那么到底谁又会被解析呢?
这里就涉及到 Maven 中的依赖调解了,当出现上述的两个问题时,Maven 就会运用内置的两个调解原则,确定到底哪个依赖会被最终解析使用。这两个内置的调解原则如下:
- 路径最短者优先;对于问题一,X(1.0)的路径长度为 2,X(2.0)的路径长度为 3,按照该路径最短者优先原则,则 X(1.0)会被解析使用;
- 第一声明者优先;对于问题二,在依赖路径长度相等的情况下,在 pom.xml 中依赖声明的顺序决定了谁会被解析使用,顺序最靠前的那个依赖优胜。
-
可选依赖
在 Maven 的世界里,存在着这样的一种依赖关系,如下图所示:从上图可以看到,M 和 N 对于 B 都是可选依赖,依赖将不会进行依赖范围传递,也就是说,如果 A 需要使用 M 或 N 时,还需要在 pom.xml 中显示的进行声明。
既然可选依赖没有依赖范围传递,导致我们需要人工不得不去进行一些显示的依赖声明,那为什么还要使用可选依赖这一特性呢?存在即合理!我们想象这样的一种场景,项目 B 实现了两个特性,其中的一个特性依赖于 M,另一个特性依赖于 N,而且这两个特性是互斥的,用户不可能同时使用这两个特性。比如 B 是一个持久层隔离工具包,它支持多种数据库,包括 MySQL、PostgreSQL 等,在构建这个工具包的时候,需要这两种数据库的驱动程序,但在使用这个工具包的时候,只会依赖一种数据库。这种情况下,项目 B 的依赖声明就会是这样的:
<project> <modelVersion>4.0.0</modelVersion> <groupId>com.jellythink.BookStore</groupId> <artifactId>project-B</artifactId> <version>1.0.0</version> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.15</version> <optional>true</optional> </dependency> <dependency> <groupId>postgresql</groupId> <artifactId>postgresql</artifactId> <version>9.1-901-1.jdbc4</version> <optional>true</optional> </dependency> </dependencies> </project>
上述 pom.xml 代码片段中,使用
<optional>
元素表示 mysql 和 postgresql 这两个依赖为可选依赖,这样一来,它们只会对当前的项目 B 产生影响,当其它项目依赖于 B 的时候,这两个依赖不会被传递。因此,当项目 A 依赖于项目 B 的时候,如果其实际使用基于 MySQL 数据库,那么在项目 A 中就需要显式地声明 mysql 依赖,比如这样:<project> <modelVersion>4.0.0</modelVersion> <groupId>com.jellythink.BookStore</groupId> <artifactId>project-A</artifactId> <version>1.0.0</version> <dependencies> <dependency> <groupId>com.jellythink.BookStore</groupId> <artifactId>project-B</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.15</version> </dependency> </dependencies> </project>
看完上面关于可选依赖的总结,大家都会觉的可选依赖确实非常麻烦,本来使用 Maven 是为了提升开发效率的,现在到好,搞的更复杂了;所以,在理想的情况下,是不应该使用可选依赖的。通过上面的例子可以看到,使用可选依赖的原因是某一个项目实现了多个特性,在面向对象设计中,有个单一职责性原则,意指一个类应该只有一项职责,而不是糅合太多的功能。这个原则在规划 Maven 项目的时候也同样适用。这样,对于上面的例子,更好的做法是为 MySQL 和 PostgreSQL 分别创建一个 Maven 项目,基于同样的 groupId 分配不同的 artifactId,在各自的 POM 中声明对应的 JDBC 驱动依赖,而且不使用可选依赖,这样用户则根据需要选择使用对应的依赖即可。
实战经验分享
Maven 的依赖涉及的知识点比较多,结合上面整理的内容,再通过结合前人的经验,这里分享一些实战经验,方便大家更好的理解和使用 Maven。
-
排除依赖
通过上面的总结,大家可以感受到传递性依赖带来的便利,但是有些时候这种特性也会带来问题。大家想象一下现在的这个应用场景。当前项目有一个第三方依赖,而这个第三方依赖由于某些原因依赖了另外一个类库的 SNAPSHOT 版本,那么这个 SNAPSHOT 就会成为当前项目的传递性依赖,而 SNAPSHOT 的不稳定性会直接影响到当前项目。这时就需要排除该 SNAPSHOT 版本,并且在当前项目中声明该类库的某个正式发布的版本。这种情况就需要使用到 Maven 中的排除依赖了,通过exclusion
关键字来排除对应的依赖。代码片段如下:<project> <modelVersion></modelVersion> <groupId>com.jellythink.BookStore</groupId> <artifactId>project-A</artifactId> <version>1.0.0</version> <dependencies> <dependency> <groupId>com.jellythink.BookStore</groupId> <artifactId>project-B</artifactId> <version>1.0.0</version> <exclusions> <exclusion> <groupId>com.jellythink.BookStore</groupId> <artifactId>project-C</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.jellythink.BookStore</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
不同的两个依赖。 -
归类依赖
现在很多应用都是基于 Spring 框架开发,但是使用 Spring 框架时,就需要引入多个 Spring 框架的依赖,比如这样子的:<project> <modelVersion>4.0.0</modelVersion> <groupId>com.jellythink.BookStore</groupId> <artifactId>project-A</artifactId> <version>1.0.0</version> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.1.6.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>5.1.6.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.1.6.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>5.1.6.RELEASE</version> </dependency> </dependencies> </project>
可以看到,这些依赖都是 Spring 框架的不同模块,在实际项目中,所有这些依赖的版本都是相同的,而且可以预见,如果将来需要升级 Spring 框架,这些依赖的版本会一起升级。按照上面的代码片段,我们需要所有 Spring 框架模块的
version
字段,这样一个一个的修改增加了错误发生的概率。此时,我们可以使用 Maven 中的归类依赖,通过引入 Maven 属性来简化这个问题。对于上面的代码片段,我们可以这样修改:<project> <modelVersion>4.0.0</modelVersion> <groupId>com.jellythink.BookStore</groupId> <artifactId>project-A</artifactId> <version>1.0.0</version> <properties> <springframework.version>5.1.6.RELEASE</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</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>${springframework.version}</version> </dependency> </dependencies> </project>
这里使用
properties
元素定义 Maven 属性,在 pom.xml 中可以使用美元符号和大括弧环绕的方式引用 Maven 属性。 -
优化依赖
在软件开发的过程中,我们通过工具来简化我们的工作,提升我们的工作效率,而 Maven 就是这样的一个工具。对于工具,有的时候,我们不能仅仅停留在使用的层面,而是能够掌控工具。通过前面的总结,我们知道 Maven 会自动解析所有项目的直接依赖和传递性依赖,并且根据规则正确判断每个依赖的范围,对于一些依赖冲突,也能进行调节,以确保任何一个构件只有唯一的版本在依赖中存在。在这些工作之后,最后得到的那些依赖被称为“已解析依赖”。我们可以通过以下命令查看当前项目的已解析依赖以及依赖范围:
mvn dependency:list
我们还可以通过以下命令查看项目的依赖树,通过这棵依赖树就能很清楚地看到某个依赖是通过哪条传递路径引入的:
mvn dependency:tree
使用
dependency:list
和dependency:tree
可以帮助我们详细了解项目中所有依赖的具体信息,在此基础上,还有dependency:analyze
工具可以帮助分析当前项目的依赖。mvn dependency:analyze
dependency:analyze
命令的输出结果中有两个重要的部分。首先是 Used undeclared dependencies,指的是项目中使用到的,但是没有显式声明的依赖。这种依赖是通过直接依赖传递进来的,当升级直接依赖的时候,相关传递性依赖的版本也可能发生变化,这种变化不易察觉,但是有可能导致当前项目出错;因此,显式声明任何项目中直接用到的依赖。另一个重要的部分是 Unused declared dependencies,指项目中未使用的,但是显式声明的依赖。需要注意的是,对于这样一类依赖,我们不能简单的直接删除其声明,而是应该仔细分析。由于
dependency:analyze
命令只会分析编译主代码和测试代码需要用到的依赖,一些执行测试盒运行时需要的依赖它就无法发现。当然了,有时候确实能通过该信息找到一些没用的依赖,但是一定要小心测试。
总结
现在回头一看,Maven 中依赖的知识点还真不少,虽然 Maven 中关于依赖的知识点很多,但是实际用到的,需要我们重点关注的内容是很少的,本文中有些概念我们只需要知道和理解就 OK 了。这篇文章非常的长,内容多,知识点也多,希望大家还是有点耐心,好好的阅读一下,争取一次就把 Maven 的依赖概念理解到位,一次学会,终生受益。希望你能喜欢这篇文章。
果冻想,玩代码,玩技术!
2019 年 4 月 7 日,于内蒙古呼和浩特。