Maven之三:坐标、依赖范围、依赖冲突
2022-09-18
Maven之三:坐标、依赖范围、依赖冲突
学习网站:http://mvnbook.com/index.html
Maven构件
构件:在Maven中,任何项目输出都可成为构件。
构件标识(唯一标识,也称为唯一坐标)
除了各种依赖jar包,我们自己开发的项目,也是要通过坐标进行唯一标识的,这样才能才其它项目中进行依赖引用。
坐标组成:
- groupId:当前Maven构件隶属的组织名。groupId一般分为多段,通常情况下,第一段为域,第二段为公司名称。域又分为 org、com、cn 等,其中 org 为非营利组织,com 为商业组织,cn 表示中国。以 apache 开源社区的 tomcat 项目为例,这个项目的 groupId 是 org.apache,它的域是org(因为tomcat是非营利项目),公司名称是apache,artifactId是tomcat。如果你的公司是mycom,有一个项目为myapp,那么groupId就应该是com.mycom.myapp。groupId的表示方式与Java包名的表示方式类似。(必须)
- artifactId:项目的唯一的标识符,实际对应项目的名称,就是项目根目录的名称。(必须)
- version:当前版本。(必须)
- packaging:打包方式,比如 jar,war…,默认是jar (可选)
- classifier:classifier通常用于区分从同一POM构建的具有不同内容的构件。它是可选的,它可以是任意的字符串,附加在版本号之后。classfier是不能直接定义的,需要结合插件使用。
标识和jar包名的对应关系(顺序拼接):
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.2.2</version>
<classifier>jdk15</classifier>
</dependency>
<!-- 对应的jar包名:
json-lib-2.2.2-jdk15.jar
-->
classifier标识使用场景
- 区分不同JDK版本所生成的jar包
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.2.2</version>
<classifier>jdk15</classifier>
</dependency>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.2.2</version>
<classifier>jdk13</classifier>
</dependency>
以上配置信息实际上对应的 jar 包是 json-lib-2.2.2-jdk15.jar 和 json-lib-2.2.2-jdk13.jar。
- 区分项目的不同组成部分,例如,源代码、javadoc、类文件等
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.2.2</version>
<classifier>jdk15-javadoc</classifier>
</dependency>
以上配置信息对应的是 json-lib-2.2.2-jdk15-javadoc.jar。
注意<classifier>
的位置:
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<classifier>jdk15-javadoc</classifier>
<version>2.2.2</version>
</dependency>
对应的是 json-lib-jdk15-javadoc-2.2.2.jar,可能会出现找不到jar包的情况。
构件特性
-
依赖传递,例如:项目依赖构件A,而构件A又依赖B,Maven会将A和B都视为项目的依赖。
-
“短路优先” 原则,构件之间存在版本冲突时,Maven会依据 “短路优先” 原则加载构件。此外,我们也可以在 pom.xml 中,使用
<exclusions></exclusions>
显式排除某个版本的依赖,以确保项目能够运行。- (a)项目依赖构件A和B,构件A → C → D(version:1.0.0),构件B → D(version:1.1.0),此时,Maven会优先解析加载D(version:1.1.0)。
- (b)项目依赖构件A和B,构件A → D(version:1.0.0), 构件B → D(version:1.1.0),此时,Maven会优先解析加载D(version:1.0.0)。
-
依赖范围,Maven在项目的构建过程中,会编译三套 ClassPath(ClassPath 是个逻辑概念,指定所依赖 Jar 的可见性),分别对应:编译期,运行期,测试期。而依赖范围就是为构件指定它可以作用于哪套 ClassPath
Maven依赖范围
为何需要依赖范围:在不同的执行阶段,需要的依赖可能不同。限制依赖的使用范围(包括编译、测试、运行等操作),使其在其他阶段无法使用。
- compile
编译依赖范围(默认),使用此依赖范围对于编译、测试、运行三种都有效,即在编译、测试和运行的时候都要使用该依赖 Jar 包。
- test
测试依赖范围,从字面意思就可以知道此依赖范围只能用于测试,而在编译和运行项目时无法使用此类依赖,典型的是JUnit,它只用于编译测试代码和运行测试代码的时候才需要。
- provided
此依赖范围,对于编译和测试有效,而对运行时无效。比如 servlet-api.jar 在 Tomcat 中已经提供了,我们只需要的是编译期提供而已。
- runtime
运行时依赖范围,对于测试和运行有效,但是在编译主代码时无效,典型的就是JDBC驱动实现。
- system
系统依赖范围,使用 system 范围的依赖时必须通过 systemPath 元素显示地指定依赖文件的路径,不依赖 Maven 仓库解析,所以可能会造成建构的不可移植。
如:
<dependencies>
<dependency>
<groupId>javax.sql</groupId>
<artifactId>jdbc-stdext</artifactId>
<version>2.0</version>
<scope>system</scope>
<systemPath>${java.home}/lib/rt.jar</systemPath>
</dependency>
<dependencies>
Maven依赖冲突
直接依赖冲突
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.0</version>
</dependency>
</dependencies>
根据上列的依赖顺序,项目将使用3.5.0版本的 MyBatis Jar。
传递依赖冲突
例如,项目 A 有这样的依赖关系:X->Y->Z(1.0)、X->G->Z(2.0),Z 是 X 的传递性依赖,但是两条依赖路径上有两个版本的 Z,那么哪个 Z 会被 Maven 解析使用呢?两个版本都被解析显然是不对的,因为那会造成依赖重复,因此必须选择一个。
Maven 依赖优化
实际上 Maven 是比较智能的,它能够自动解析直接依赖和传递性依赖,根据预定义规则判断依赖范围的合理性,也可以对部分依赖进行适当调整来保证构件版本唯一。
即使这样,还会有些情况使 Maven 发生误判,因此手工进行依赖优化还是相当有必要的。我们可以使用 maven-dependency-plugin 提供的三个目标来实现依赖分析:
$ mvn dependency:list
$ mvn dependency:tree
$ mvn dependency:analyze
如若需更精细的分析结果,可以在命令后使用诸如以下参数:
-Dverbose
-Dincludes=<groupId>:<artifactId>
Maven依赖冲突调解规则
四大原则:
- 路径近者优先原则
A --> B --> X(1.1) // dist(A->X) = 2
A --> C --> D --> X(1.0) // dist(A->X) = 3
Maven可以按照第一原则自动调解依赖,结果是使用X(1.1)作为依赖。
- 第一声明者优先原则
A --> B --> X(1.1) // dist(A->X) = 2
A --> C --> X(1.0) // dist(A->X) = 2
<!-- A pom.xml -->
<dependencies>
...
dependency B
...
dependency C
</dependencies>
若冲突依赖的路径长度相同,那么第一原则就无法起作用了。当路径长度相同,则需要根据A直接依赖包在pom.xml文件中的先后顺序来判定使用那条依赖路径,如果次级模块相同则向下级模块推,直至可以判断先后位置为止。假设依赖B位置在依赖C之前,则最终会选择X(1.1)依赖。
-
排除原则
-
版本锁定原则
-
覆盖原则:若相同类型但版本不同的依赖存在于同一个 pom 文件,依赖调解两大原则都不起作用,需要采用覆盖策略来调解依赖冲突,最终会引入最后一个声明的依赖。
解决依赖冲突
冲突解决方式简单粗暴,直接在 pom.xml 文件中排除冲突依赖即可(使用 <exclusions></exclusions>
显式排除)。
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-grizzly2-http</artifactId>
<!-- 剔除依赖 -->
<exclusions>
<exclusion>
<groupId>org.glassfish.hk2.external</groupId>
<artifactId>jakarta.inject</artifactId>
</exclusion>
...
</exclusions>
</dependency>