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包名的对应关系(顺序拼接):

1
2
3
4
5
6
7
8
9
<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包
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<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、类文件等
1
2
3
4
5
6
<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>的位置:

1
2
3
4
5
6
<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 仓库解析,所以可能会造成建构的不可移植。

如:

1
2
3
4
5
6
7
8
9
<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依赖冲突

直接依赖冲突

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<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 提供的三个目标来实现依赖分析:

1
2
3
$ mvn dependency:list
$ mvn dependency:tree
$ mvn dependency:analyze

如若需更精细的分析结果,可以在命令后使用诸如以下参数:

1
2
3
-Dverbose

-Dincludes=<groupId>:<artifactId>

Maven依赖冲突调解规则

四大原则:

  • 路径近者优先原则
1
2
3
A --> B --> X(1.1)         // dist(A->X) = 2

A --> C --> D --> X(1.0)   // dist(A->X) = 3

Maven可以按照第一原则自动调解依赖,结果是使用X(1.1)作为依赖。

  • 第一声明者优先原则
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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>显式排除)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<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>