Maven的依赖管理、传递、冲突、父子工程的继承和聚合方式
作者:雷神乐乐
一、基于IDEA 进行Maven依赖管理
(一)依赖管理概念
我们在从网上下载需要的依赖的时候,不同的jar包与jar包之间有依赖关系,且不同的版本之间也需要注意是否有冲突,使用Maven就可以帮助我们去管理和自动解决软件包依赖问题,我们只需要关注我们需要的jar包和版本即可。
通过定义 POM 文件,Maven 能够自动解析项目的依赖关系,并通过 Maven 仓库自动下载和管理依赖,从而避免了手动下载和管理依赖的繁琐工作和可能引发的版本冲突问题。
总之,Maven 的依赖管理使得软件包依赖的管理,和使用更加智能和方便,简化了开发过程中的工作,并提高了软件质量和可维护性。
(二)Maven工程核心信息配置和解读(GAVP)
位置:pom.xml
<!-- 模型版本 -->
<modelVersion>4.0.0</modelVersion>
<!-- 公司或者组织的唯一标志,并且配置时生成的路径也是由此生成,
如com.companyname.project-group,maven会将该项目打成的jar包放本地路径:
/com/companyname/project-group -->
<groupId>com.companyname.project-group</groupId>
<!-- 项目的唯一ID,一个groupId下面可能多个项目,就是靠artifactId来区分的 -->
<artifactId>project</artifactId>
<!-- 版本号 -->
<version>1.0.0</version>
<!--打包方式
默认:jar
jar指的是普通的java项目打包方式, 项目打成jar包
war指的是web项目打包方式,项目打成war包
pom不会将项目打包,这个项目作为父工程,被其他工程聚合或者继承
-->
<packaging>jar/pom/war</packaging>(三)Maven工程依赖管理配置
位置:pom.xml
1.依赖管理和依赖添加
<!--
通过编写依赖jar包的gav必要属性,引入第三方依赖!
scope属性是可选的,可以指定依赖生效范围!
依赖信息查询方式:
1. maven仓库信息官网 https://mvnrepository.com/
2. mavensearch插件搜索
-->
<dependencies>
<!-- 引入具体的依赖包 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
<!-- 依赖范围 -->
<scope>runtime</scope>
</dependency>
</dependencies>2.依赖版本统一提取和维护
Maven中,有个<properties>标签,该标签用于定义项目构建过程中可以使用的属性。
通过在properties标签中自定义管理依赖的版本号,方便我们统一管理各种依赖的版本,当需要升级或更改依赖版本时,只需在<properties>标签中修改一次,而不需要逐个查找并替换POM文件中的每个依赖标签,这大大提高了维护的效率。
<properties>
<!-- 通过maven规定的固定的key,配置maven的参数:-->
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!--声明版本:命名随便,内部制定版本号即可-->
<junit.version>5.9.2</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<!--引用properties声明版本 -->
<version>${junit.version}</version>
<!--引用依赖的运行范围(尽量不要更改) -->
<scope>test</scope>
</dependency>
</dependencies>(四)依赖范围
通过设置坐标的依赖范围(scope),可以设置对应jar包的作用范围(三种classpath):编译环境(在main包下)、测试环境(在test包下)、运行环境(打包后的jar包或war包是否有对应的依赖包)。
scope标签中的依赖范围可以修改,但是不建议,在https://mvnrepository.com/中搜索出来的依赖范围是什么,就是什么,尽量不要修改!
| 依赖范围 | 描述 |
|---|---|
| compile | 编译依赖范围,scope 元素的缺省值,即如果下载的依赖没有scope标签,默认是compile范围。使用此依赖范围的 Maven 依赖,对于三种 classpath 均有效,即该 Maven 依赖在上述三种 classpath 均会被引入。例如,log4j 在编译、测试、运行过程都是必须的。 |
| test | 测试依赖范围。使用此依赖范围的 Maven 依赖,只对测试 classpath 有效。例如,Junit 依赖只有在测试阶段才需要。 |
| provided | 已提供依赖范围。使用此依赖范围的 Maven 依赖,只对编译 classpath 和测试 classpath 有效。例如,servlet-api 依赖对于编译、测试阶段而言是需要的,但是运行阶段,由于外部容器已经提供,故不需要 Maven 重复引入该依赖。 |
| runtime | 运行时依赖范围。使用此依赖范围的 Maven 依赖,只对测试 classpath、运行 classpath 有效。例如,JDBC 驱动实现依赖,其在编译时只需 JDK 提供的 JDBC 接口即可,只有测试、运行阶段才需要实现了 JDBC 接口的驱动。 |
| system | 系统依赖范围,其效果与 provided 的依赖范围一致。其用于添加非 Maven 仓库的本地依赖,通过依赖元素 dependency 中的 systemPath 元素指定本地依赖的路径。鉴于使用其会导致项目的可移植性降低,一般不推荐使用。 |
| import | 导入依赖范围,该依赖范围只能与 dependencyManagement 元素配合使用,其功能是将目标 pom.xml 文件中 dependencyManagement 的配置导入合并到当前 pom.xml 的 dependencyManagement 中。 |
(五)Maven工程依赖下载失败错误解决(重点)
1.依赖下载失败的主要原因
- 下载依赖时出现网络故障或仓库服务器宕机等原因,导致无法连接至 Maven 仓库,从而无法下载依赖。
- 依赖项的版本号或配置文件中的版本号错误,或者依赖项没有正确定义,导致 Maven 下载的依赖项与实际需要的不一致,从而引发错误。
- 本地 Maven 仓库或缓存被污染或损坏,导致 Maven 无法正确地使用现有的依赖项。
2.解决方案
- 检查网络连接和 Maven 仓库服务器状态。
- 确保依赖项的版本号与项目对应的版本号匹配,并检查 POM 文件中的依赖项是否正确。
- 清除本地 Maven 仓库缓存(lastUpdated 文件),因为只要存在lastupdated缓存文件,刷新也不会重新下载。本地仓库中,根据依赖的gav属性依次向下查找文件夹,最终删除内部的文件,刷新重新下载即可!

(六)Maven工程Build构建配置
项目构建是指将源代码、依赖库和资源文件等转换成可执行或可部署的应用程序的过程,在这个过程中包括编译源代码、链接依赖库、打包和部署等多个步骤。(将一系列的原材料生产为一个产品,即打成jar包或war包的过程)
默认情况下,构建不需要额外配置,都有对应的缺省配置。当然了,我们也可以在pom.xml定制一些配置,来修改默认构建的行为和产物。
例如:
- 指定构建打包文件的名称,非默认名称
- 制定构建打包时,指定包含文件格式和排除文件
- 打包插件版本过低,配置更高版本插件
构建配置是在pom.xml / build标签中指定!
1.指定打包命名
<!-- 默认的打包名称:artifactid+verson.打包方式 --> <build> <finalName>定义打包名称</finalName> </build>
使用案例:不指定打包名称默认是maven_web-1.0-SNAPSHOT.war

2.指定打包文件
如果在java文件夹中添加java类,resources文件夹中添加配置文件,即默认情况下,按照maven工程结构放置的文件,会默认被编译和打包到classes文件夹下:

但是如果我们不按照要求将文件放在对应的文件夹下,又想要将文件打包到classes中,例如mybatis中有时会将用于编写SQL语句的映射文件,和mapper接口都写在src/main/java下的某个包中,此时映射文件也不会被打包。
解决方案:使用resources标签,指定要打包资源的文件夹,要把哪些静态资源打包到 classes根目录下。
<build>
<!--设置要打包的资源位置-->
<resources>
<resource>
<!--设置资源所在目录-->
<directory>src/main/java</directory>
<includes>
<!--设置包含的资源类型-->
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</build>此时,不在resources文件夹下的文件也被成功打包!

3.配置依赖插件
dependencies标签下引入开发需要的jar包,我们可以在build/plugins/plugin标签引入插件。常用的插件:修改jdk版本、tomcat插件、mybatis分页插件、mybatis逆向工程插件等等。pom.xml文件中配置了tomcat插件,就可以省略在Edit Configurations中配置tomcat了。
具体步骤参考我的另一篇文章:《Maven3.8.1使用Tomcat8插件启动项目的方法(亲测有效)》
二、Maven依赖传递
(一)Maven依赖传递特性
概念
假如有Maven项目A,项目B依赖A,项目C依赖B。那么我们可以说 C依赖A。也就是说,依赖的关系为:C—>B—>A, 那么我们执行项目C时,会自动把B、A都下载导入到C项目的jar包文件夹中,这就是依赖的传递性。
作用
- 简化依赖导入过程
- 确保依赖版本正确
案例演示:
创建2个项目,maven_A和maven_B:

在maven_A的pom.xml文件中引入maven_B的坐标:

在maven_B的pom.xml文件中配置druid依赖,更新maven,发现maven_A项目中也能使用druid

使用mvn dependency:tree命令也可以查看依赖关系:

也可以使用下面的命令在文件中查看依赖关系:
mvn dependency:build-classpath > classpath.txt

(二)传递的原则
在 A 依赖 B,B 依赖 C 的前提下,C 是否能够传递到 A,取决于 B 依赖 C 时使用的依赖范围以及配置:只有scope标签是compile的才能传递,test和provided依赖范围都不能传递,需要手动配置!scope标签中的依赖范围可以修改,但是不建议,在https://mvnrepository.com/中搜索出来的依赖范围是什么,就是什么,尽量不要修改!

另外,还有依赖传递的终止标签:<optional>true</optional>
若配置了optional标签,则不能传递
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
<optional>true</optional>
</dependency>
(三)依赖传递终止的三个条件
- 非compile范围进行依赖传递
- 使用optional配置终止传递
- 依赖冲突(传递的依赖已经存在)
三、Maven依赖冲突
当直接引用或者间接引用出现了相同的jar包! 这时呢,一个项目就会出现相同的重复jar包,这就算作冲突!依赖冲突避免出现重复依赖,并且终止依赖传递!

maven自动解决依赖冲突问题能力,会按照自己的原则,进行重复依赖选择。同时也提供了手动解决的冲突的方式,不过不推荐!
解决依赖冲突(如何选择重复依赖)方式
1.自动选择原则
1.1依赖冲突之短路优先原则
- 短路优先原则(第一原则)
- A—>B—>C—>D—>E—>X(version 0.0.1)
- A—>F—>X(version 0.0.2)
- 则A依赖于X(version 0.0.2)。
示例:
maven_B的pom.xml:
<dependencies>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
</dependencies>maven_A的pom.xml:
<dependencies>
<dependency>
<groupId>com.alibaba.maven</groupId>
<artifactId>maven_B</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.7</version>
</dependency>
</dependencies>查看maven_A的依赖树:maven_B中的druid 1.2.8版本被排除,maven_A中使用的是druid 1.2.7


如果你的重复依赖没有置灰,把maven_A的pom.xml文件中,dependencies下的所有dependency清空,刷新后重新添加依赖,就能看到置灰的依赖了,重复依赖没有置灰是IDEA的显示问题。
1.2依赖冲突之声明优先原则
- 依赖路径长度相同情况下,则“先声明优先”(第二原则)
- A—>E—>X(version 0.0.1)
- A—>F—>X(version 0.0.2)
- 在<depencies></depencies>中,先声明的,路径相同,会优先选择!
- 在A的pom.xml文件中,如果先配置E,那么就使用的是0.0.1版本!
- 如果先配置F,那么就使用的是0.0.2版本!
示例:
创建项目maven_C,其pom.xml文件如下:
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.7</version>
</dependency>
</dependencies>maven_B的pom.xml:
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
</dependencies>maven_A的pom.xml:
<dependencies>
<dependency>
<groupId>com.alibaba.maven</groupId>
<artifactId>maven_B</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba.maven</groupId>
<artifactId>maven_C</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>查看maven_A的依赖树:maven_B先声明,就使用maven_B的druid 1.2.8版本


调换一下:
<dependencies>
<dependency>
<groupId>com.alibaba.maven</groupId>
<artifactId>maven_C</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba.maven</groupId>
<artifactId>maven_B</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>查看maven_A的依赖树:maven_C先声明,就使用maven_C的druid 1.2.7版本


2.手动排除
我们知道,先声明的就会被先使用,如果只想使用maven_C中的druid 1.2.7的依赖,可以手动排除依赖,使用exclusions标签,里面可以嵌套多个exclusion标签,exclusion标签中只需写明要排除的G和A坐标即可:
<dependencies>
<dependency>
<groupId>com.alibaba.maven</groupId>
<artifactId>maven_B</artifactId>
<version>1.0-SNAPSHOT</version>
<!--依赖排除-->
<exclusions>
<exclusion>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba.maven</groupId>
<artifactId>maven_C</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>查看依赖树:maven_B中的druid 1.2.8被成功排除!


四、Maven工程继承和聚合关系
(一)Maven工程继承关系
1.继承概念
Maven 继承是指在 Maven 的项目中,让一个项目从另一个项目中继承配置信息的机制。继承可以让我们在多个项目中共享同一配置信息,简化项目的管理和维护工作。
2.继承作用与背景
在父工程中统一管理项目中的依赖信息。
它的背景是:
- 对一个比较大型的项目进行了模块拆分。
- 一个 project 下面,创建了很多个 module。
- 每一个 module 都需要配置自己的依赖信息。
它背后的需求是:
- 对一个比较大型的项目进行了模块拆分。
- 一个 project 下面,创建了很多个 module。
- 每一个 module 都需要配置自己的依赖信息。
- 在每一个 module 中各自维护各自的依赖信息很容易发生出入,不易统一管理。
- 使用同一个框架内的不同 jar 包,它们应该是同一个版本,所以整个项目中使用的框架版本需要统一。
- 使用框架时所需要的 jar 包组合(或者说依赖信息组合)需要经过长期摸索和反复调试,最终确定一个可用组合。这个耗费很大精力总结出来的方案不应该在新的项目中重新摸索。 通过在父工程中为整个项目维护依赖信息的组合既保证了整个项目使用规范、准确的 jar 包;又能够将以往的经验沉淀下来,节约时间和精力。
创建一个父工程——maven_parent:
<groupId>com.alibaba.maven</groupId> <artifactId>maven_parent</artifactId> <version>1.0-SNAPSHOT</version> <!-- 当前工程作为父工程,它要去管理子工程,所以打包方式必须是 pom --> <packaging>pom</packaging>
创建一个子工程——maven_son:


<!-- 使用parent标签指定当前工程的父工程 -->
<parent>
<!--父工程的坐标-->
<artifactId>maven_parent</artifactId>
<groupId>com.alibaba.maven</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<!-- 子工程的坐标 -->
<!-- 如果子工程坐标中的groupId和version与父工程一致,那么可以省略 -->
<artifactId>maven_son</artifactId>创建好子工程maven_son后,父工程maven_parent的pom.xml文件也会发生改变:
<groupId>com.alibaba.maven</groupId>
<artifactId>maven_parent</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 创建子工程时,选择父工程后,会在父工程中自动生成: -->
<modules>
<module>maven_son</module>
</modules>
<!-- 当前工程作为父工程,它要去管理子工程,所以打包方式必须是 pom -->
<packaging>pom</packaging>在父工程maven_parent添加依赖:
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.7</version>
</dependency>
</dependencies>子工程会无条件继承父工程中的依赖:

甚至包括scope是test和provide的:

3.父工程依赖统一管理
在使用父工程的时候,一般很少直接使用dependencies,让所有依赖直接被子工程继承,而是使用dependencyManagement来统一管理依赖:
<!-- 使用dependencyManagement标签配置对依赖的管理 -->
<!-- 被管理的依赖并没有真正被引入到工程 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.7</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
</dependencyManagement>此时子工程中就需要手动选择需要继承父工程中的依赖:

<!-- 子工程引用父工程中的依赖信息时,可以把版本号去掉。 -->
<!-- 把版本号去掉就表示子工程中这个依赖的版本由父工程决定。 -->
<!-- 具体来说是由父工程的dependencyManagement来决定。 -->
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
</dependencies>

(二)Maven工程聚合关系
1.聚合概念
Maven 聚合是指将多个项目组织到一个父级项目中,以便一起构建和管理的机制。聚合可以帮助我们更好地管理一组相关的子项目,同时简化它们的构建和部署过程。
2.聚合作用
- 管理多个子项目:通过聚合,可以将多个子项目组织在一起,方便管理和维护。
- 构建和发布一组相关的项目:通过聚合,可以在一个命令中构建和发布多个相关的项目,简化了部署和维护工作。
- 优化构建顺序:通过聚合,可以对多个项目进行顺序控制,避免出现构建依赖混乱导致构建失败的情况。
- 统一管理依赖项:通过聚合,可以在父项目中管理公共依赖项和插件,避免重复定义。
3.聚合语法与使用方法
父项目中包含的子项目列表。
当我们对父工程进行了操作,比如package打包,里面的子工程也会进行相同的操作,提高开发效率。
<groupId>com.alibaba.maven</groupId>
<artifactId>maven_parent</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>maven_son</module>
<module>../maven_A</module>
<module>../maven_web</module>
</modules>
<!-- 当前工程作为父工程,它要去管理子工程,所以打包方式必须是 pom -->
<packaging>pom</packaging>父工程执行package命令:

对应的子工程会一键打包:

总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
