前面我们讲了maven项目中的最重要的文件:pom.xml 配置文件相关内容。介绍了pom 是如何定义项目,如何添加依赖的jar 包的等。
我们知道,在Maven的生命周期中,存在编译、测试、运行等过程,那么有些依赖只用于测试,比如junit;有些依赖编译用不到,只有运行的时候才能用到,比如mysql的驱动包在编译期就用不到(编译期用的是JDBC接口),而是在运行时用到的;还有些依赖,编译期要用到,而运行期不需要提供,因为有些容器已经提供了,比如servlet-api在tomcat中已经提供了,我们只需要的是编译期提供而已。
那么Maven是如何管理各个jar包的依赖关系,jar包之间的依赖传递和依赖冲突呢?所以接下来就讲一讲maven的依赖管理。
一、声明依赖
Maven 项目使用pom.xml 文件,可以方便的管理项目中所有依赖的jar包,之前也介绍过pom.xml 文件是如何定义依赖的。下面就以一段 junit依赖声明,演示pom.xml 是如何定义相关依赖的:
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <type>jar</type> <scope>test</scope> <optional>false</optional> <exclusions> <exclusion></exclusion> </exclusions> </dependency> </dependencies>
上面的示例我们定义了junit的依赖,而且junit只在测试阶段生效。具体配置参数如下:
- type:依赖类型,对应构件中定义的 packaging,可不声明,默认为 jar;
- scope:依赖范围,大致有compile、provided、runtime、test、system等几个;
- optional:依赖是否可选;
- exclusions:排除传递依赖。
总结说来,在pom.xml 配置文件中,配置节点引入了节点,它主要管理依赖的范围。大致有compile、provided、runtime、test、system等几个。引入 节点排除某些依赖。解决了控制各个jar包的依赖范围,jar包之间的依赖传递和依赖冲突的问题。
二、依赖范围
Maven在执行不同的命令时(mvn package,mvn test,mvn install ……),会使用不同的 classpath,Maven 对应的有三套 classpath,即:编译classpath、测试classpath,运行classpath。我们可在dependency中的scope元素中进行配置。从而决定了该依赖构件会被引入到哪一个 classpath 中。默认为:compile。Maven提供的依赖范围如下:
- compile:编译依赖范围,默认值。此选项对编译、测试、运行三种 classpath 都有效,如 hibernate-core-3.6.5.Final.jar,表明在编译、测试、运行的时候都需要该依赖;
- test:测试依赖范围。只对测试有效,表明只在测试的时候需要,在编译和运行时将无法使用该类依赖,如 junit;
- provided:已提供依赖范围。编译和测试有效,运行无效。如 servlet-api ,在项目运行时,tomcat 等容器已经提供,无需 Maven 重复引入;
- runtime:运行时依赖范围。测试和运行有效,编译无效。如 jdbc 驱动实现,编译时只需接口,测试或运行时才需要具体的 jdbc 驱动实现;
- system:系统依赖范围。和 provided 依赖范围一致,也是编译和测试有效,需要通过 显示指定,且可以引用环境变量;
- import:导入依赖范围。使用该选项,通常需要 pom,将目标 pom 的 dependencyManagement 配置导入合并到当前 pom 的 dependencyManagement 元素。
需要注意的是,在打包阶段,使用的是运行classpath。即引入到运行classpath中的Maven依赖会被一起打包。
三、依赖传递
所谓依赖传递,就是A依赖B,B依赖C,A就间接依赖C,那么A与C直接的依赖关系就叫传递性依赖。
直接依赖又叫第一直接依赖,传递性依赖又叫第二直接依赖,其中第一直接依赖和第二直接依赖的依赖范围,决定了传递性依赖的依赖范围。
下面使用hibernate-core的依赖关系详细介绍所谓的依赖传递:
如上图所示,hibernate-core 依赖 hibernate-commons-annotations ,而 hibernate-commons-annotations 又依赖 slf4j-api ,hibernate-core 对 slf4j-api 的依赖就是传递依赖。pom.xml 文件中我们只需要引入 hibernate-core 构件的依赖,Maven 会自动为我们引入依赖jar包及传递依赖的jar包,无需再手动引一遍相关依赖。所以不用担心依赖冲突的问题。
那么如何判定一个间接依赖是否有必要被引入呢?间接依赖被引入后其依赖范围又是什么呢?
其实很简单,就是通过第一直接依赖的依赖范围和第二直接依赖的依赖范围之间的关系,来判定是否有必要引入间接依赖以及确定引入间接依赖后其依赖范围,如下表所示:
所以,通过上表我们可以发现:
1)第二直接依赖的依赖范围为compile : 传递依赖的依赖范围同第一直接依赖的依赖范围一样。
2)第二直接依赖的依赖范围为test : 传递依赖不会被引入。
3)第二直接依赖的依赖范围为provided : 只有当第一直接依赖的依赖范围亦为provided时,传递依赖才会被引入,且依赖范围依然是provided。
4)第二直接依赖的依赖范围为runtime : 除了第一直接依赖的依赖范围为compile时传递依赖的依赖范围为runtime外,其余情况下,传递依赖的依赖范围同第一直接依赖的依赖范围一样。
四、依赖冲突
通常我们不需要关心传递性依赖,但是当多个传递性依赖中有对同一构件不同版本的依赖时,如何解决呢?通常我们有一下两个原则:
- 短路径优先:假如有以下依赖:A -> B -> C ->X(版本 1.0) 和 A -> D -> X(版本 2.0),则优先解析较短路径的 X(版本 2.0);
- 先声明优先:即谁先声明,谁被解析。
针对依赖冲突中的“短路径优先”,那如果我们就想使用长路径的依赖怎么办呢?这时可以使用依赖排除元素,显示排除短路径依赖。在非冲突的情况下,这种方法同样有效。
五、解决依赖冲突
一般情况下我们不需要关心依赖的问题。而当依赖出问题时,我们需要知道该如何解决依赖冲突。解决依赖冲突的方式如下:
(1)首先,使用:mvn dependency:tree
命令查看项目的依赖列表,
(2)然后,通过依赖列表,找出冲突的jar包。
(3)最后,可选依赖 option或是排除依赖 exclusions 处理相关冲突。
1.可选依赖 option
通过项目中的pom.xml中的dependency下的option元素中进行配置,只有显式地配置项目中某依赖的option元素为true时,该依赖才是可选依赖;不设置该元素或值为false时,作用是:当某个间接依赖是可选依赖时,无论依赖范围是什么,其都不会因为传递性依赖机制而被引入。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <!-- optional=true,依赖不会传递,该项目依赖devtools;之后依赖boot项目的项目如果想要使用devtools,需要重新引入 --> <optional>true</optional> </dependency>
上面的示例,当optional=true,devtools的依赖不会传递,该项目依赖devtools;之后依赖该项目的项目则不会依赖devtools组件,如果想要使用devtools,需要重新引入。
2.排除依赖 exclusions
我们知道,由于Maven的的传递性依赖机,有时候第三方组件B的C依赖由于版本(1.0)过低时。我们期望能够将该间接依赖直接剔除出去,这样不会影响到项目中其他依赖组件。这时可以通过exclusions元素实现。
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <exclusions> <exclusion> <artifactId>commons-logging</artifactId> <groupId>commons-logging</groupId> </exclusion> </exclusions> </dependency>
上面,我们排除了spring-core组件忠的commons-logging。
最后
以上,我们就把Maven的依赖关系,Jar包之间的依赖传递和依赖冲突介绍完了。