主要讲解Maven的依赖管理相关知识。
前言
Maven确实比较简单,估计大家在工作期间,每天抽出一点时间,基础部分几天就可以学完,所以这块感觉真没有啥好讲的。
这篇文章先讲解依赖管理的基础知识,然后再结合一个简单的示例消耗一下。
基础知识
坐标
类比为数学中平面几何,坐标(x、y ),任何一个坐标都能唯一标识该平面中的一个点。该点对应到 maven 就是 .jar、.war 等文件的文件。Maven 使用 groupId、artifactId、version、packaging、classifier 等元素来组成自己的坐标,并定义一组这样的规则,只要能提供正确坐标元素 Maven 就能找到对应的构件。
坐标元素:
- groupId:定义当前 Maven 项目隶属的实际项目。
- artifactId:定义实际项目中的一个 Maven 项目(模块)。
- packaging:定义 Maven 项目打包方式。jar、war、pom。默认为 jar。
- version:定义 Maven 项目当前所处的版本。
- classifier:区分从同一 artifact 构建的具有不同内容的构件。
比如我经常用的示例:
<project> ... <groupId>org.example</groupId> <artifactId>demo5</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> ... </project>
构件名与坐标是对应的,一般规则是:artifactId-version[-classifier].packaging。
依赖声明
<project> ... <dependencies> <dependency> <groupId>实际项目</groupId> <artifactId>模块</artifactId> <version>版本</version> <type>依赖类型</type> <scope>依赖范围</scope> <optional>依赖是否可选</optional> <!—主要用于排除传递性依赖--> <exclusions> <exclusion> <groupId>…</groupId> <artifactId>…</artifactId> </exclusion> </exclusions> </dependency> <dependencies> ... </project>
- groupId、artifactId、version:依赖的基本坐标。
- type:依赖的类型,对应项目对应的 packaging,一般不必声明。
- scope:依赖的范围,后面详解。
- optional:标记依赖是否可选。
- exclusions:用来排除传递性依赖。
依赖范围
依赖范围就是用来控制依赖和三种classpath(编译classpath,测试classpath、运行classpath)的关系,Maven有如下几种依赖范围:
- **compile:**编译依赖范围。如果没有指定,就会默认使用该依赖范围。使用此依赖范围的Maven依赖,对于编译、测试、运行三种classpath都有效。典型的例子是spring-code,在编译、测试和运行的时候都需要使用该依赖。
- test: 测试依赖范围。使用次依赖范围的Maven依赖,只对于测试classpath有效,在编译主代码或者运行项目的使用时将无法使用此依赖。典型的例子是Jnuit,它只有在编译测试代码及运行测试的时候才需要。
- **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:**导入依赖范围。只在 dependencyManagement 标签中生效,导入已经定义好的 pom 文件中,该依赖范围不会对三种classpath产生实际的影响。
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-framework-bom</artifactId> <version>4.3.16.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
这个干啥用呢?what???
上述除import以外的各种依赖范围与三种classpath的关系如下:
开始还不太明白这个“依赖范围”是啥意思,其实就是在编译、测试、运行时的3个环节,虽然在pom.xml中配置了依赖项,但是有些依赖项只对某几个环节生效,对有些环节其实是不生效的。
依赖机制与特性
传递性依赖
比如一个account-email项目为例,account-email有一个compile范围的spring-code依赖,spring-code有一个compile范围的commons-logging依赖,那么commons-logging就会成为account-email的compile的范围依赖,commons-logging是account-email的一个传递性依赖。
假设A依赖于B,B依赖于C,我们说A对于B是第一直接依赖,B对于C是第二直接依赖,A对于C是传递性依赖。第一直接依赖和第二直接依赖的范围决定了传递性依赖的范围,如下图所示,最左边一行表示第一直接依赖范围,最上面一行表示第二直接依赖范围,中间的交叉单元格则表示传递依赖范围。
从上图中,我们可以发现这样的规律:
- 当第二直接依赖的范围是compile的时候,传递性依赖的范围与第一直接依赖的范围一致;
- 当第二直接依赖的范围是test的时候,依赖不会得以传递;
- 当第二直接依赖的范围是provided的时候,只传递第一直接依赖范围也为provided的依赖,切传递依赖的范围同样为provided;
- 当第二直接依赖的范围是runtime的时候,传递性依赖的范围与第一直接依赖的范围一致,但compile列外,此时传递性依赖范围为runtime。
依赖调解
当传递性依赖出现问题时,能够清楚地知道该传递性依赖是从哪条依赖路径中引入的。
- 路径最近者优先原则
- A->B->C->X(1.0)
- A->D->X(2.0)
- 第一声明者优先原则
- A->B->Y(1.0)
- A->C->Y(2.0)
此时由于依赖路径长度一致,按照第一声明者优先原则。在路径长度一致的前提下,如果 B 依赖在 POM 文件中声明顺序在 C 依赖之前,那么 Y(1.0) 则会被引入。
但是如果你在同一个文件中这么写:
<dependencies> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.11</version> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.10</version> </dependency> </dependencies>
如果有需要依赖commons-codec,得到的是1.11,不是1.10,可以这么理解:依赖调解只发生于构建来自不同 pom 时,而此时构建声明处于同一 pom,故不会触发依赖调解。
可选依赖
如图,项目中A依赖B,B依赖于X和Y,如果所有这三个的范围都是compile的话,那么X和Y就是A的compile范围的传递性依赖,但是如果我想X,Y不作为A的传递性依赖,不给他用的话,可以通过optional为true配置为可选依赖。
<project> <modelVersion>4.0.0</modelVersion> <groupId>com.juvenxu.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</groupId> <version>8.4-701.jdbc3</version> <optional>true</optional> </dependency> </dependencies> </project>
排除依赖
有时候你引入的依赖中包含你不想要的依赖包,你想引入自己想要的,这时候就要用到排除依赖了,比如下图中spring-boot-starter-web自带了logback这个日志包,我想引入log4j2的,所以我先排除掉logback的依赖包,再引入想要的包就行了。
这里注意:声明exclustion的时候只需要groupId和artifactId,而不需要version元素,这是因为只需要groupId和artifactId就能唯一定位依赖图中的某个依赖。
依赖示例
通常情况下,在一个共通的项目下,有一系列的项目。在这种情况下,我们可以创建一个公共依赖的 pom 文件,该 pom 包含所有的公共的依赖关系,我们称其为其他子项目 pom 的 pom 父。接下来的一个例子可以帮助你更好的理解这个概念。
接下来是上面依赖图的详情说明:
- App-UI-WAR 依赖于 App-Core-lib 和 App-Data-lib。
- Root 是 App-Core-lib 和 App-Data-lib 的父项目。
- Root 在它的依赖部分定义了 Lib1、lib2 和 Lib3 作为依赖。
App-UI-WAR 的 pom.xml 文件代码如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.companyname.groupname</groupId> <artifactId>App-UI-WAR</artifactId> <version>1.0</version> <packaging>war</packaging> <dependencies> <dependency> <groupId>com.companyname.groupname</groupId> <artifactId>App-Core-lib</artifactId> <version>1.0</version> </dependency> </dependencies> <dependencies> <dependency> <groupId>com.companyname.groupname</groupId> <artifactId>App-Data-lib</artifactId> <version>1.0</version> </dependency> </dependencies> </project>
App-Core-lib 的 pom.xml 文件代码如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>Root</artifactId> <groupId>com.companyname.groupname</groupId> <version>1.0</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>com.companyname.groupname</groupId> <artifactId>App-Core-lib</artifactId> <version>1.0</version> <packaging>jar</packaging> </project>
App-Data-lib 的 pom.xml 文件代码如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>Root</artifactId> <groupId>com.companyname.groupname</groupId> <version>1.0</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>com.companyname.groupname</groupId> <artifactId>App-Data-lib</artifactId> <version>1.0</version> <packaging>jar</packaging> </project>
Root 的 pom.xml 文件代码如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.companyname.groupname</groupId> <artifactId>Root</artifactId> <version>1.0</version> <packaging>pom</packaging> <dependencies> <dependency> <groupId>com.companyname.groupname1</groupId> <artifactId>Lib1</artifactId> <version>1.0</version> </dependency> </dependencies> <dependencies> <dependency> <groupId>com.companyname.groupname2</groupId> <artifactId>Lib2</artifactId> <version>2.1</version> </dependency> </dependencies> <dependencies> <dependency> <groupId>com.companyname.groupname3</groupId> <artifactId>Lib3</artifactId> <version>1.1</version> </dependency> </dependencies> </project>
现在当我们构建 App-UI-WAR 项目时, Maven 将通过遍历依赖关系图找到所有的依赖关系,并且构建该应用程序。
- 公共的依赖可以使用 pom 父的概念被统一放在一起。App-Data-lib 和 App-Core-lib 项目的依赖在 Root 项目里列举了出来(参考 Root 的包类型,它是一个 POM).
- 没有必要在 App-UI-W 里声明 Lib1, lib2, Lib3 是它的依赖。Maven 通过使用可传递的依赖机制来实现该细节。
后记
开始以为Mave东西还有点多,学了几天才发现,原来Mave也就这样。这是Mave系列的最后一篇,下个系列就开始看看微服务框架部分,希望能尽快把Java基础部分的知识学习完吧。