Maven实战.依赖(依赖范围、传递性依赖、依赖调解、可选依赖等)

news2025/1/13 2:36:26

文章目录

  • 依赖的配置
  • 依赖范围
  • 传递性依赖
  • 传递性依赖和依赖范围
  • 依赖调解
  • 可选依赖
  • 最佳实践
    • 排除依赖
    • 归类依赖
    • 优化依赖

依赖的配置

依赖会有基本的groupId、artifactld 和 version等元素组成。其实一个依赖声明可以包含如下的一些元素:

<project>
...
    <dependencies>
        <dependency>
            <groupId>...</groupId>
            <artifactId>...</artifactId>
            <version>...</version>
            <type>...</type>
            <scope>...</scope>
            <optional>...</optional>
            <exclusions>
                <exclusion>
                  	...
                </exclusion>
                ...
            </exclusions>
        </dependency>
        ...
    </dependencies>
...
</project>

其中每个标签的意思,直接参考Maven实战.pom.xml标签说明中的<dependencies>处。

依赖范围

参考Maven实战.pom.xml标签说明中的<scope>处。

传递性依赖

考虑一个基于 Spring Framework 的项目,如果不使用 Maven,那么在项目中就需要手动下载相关依赖。由于 Spring Framework 又会依赖于其他开源类库,因此实际中往往会下载一个很大的如 spring-framework-2.5.6-with-dependencies.zip 的包,这里包含了所有 SpringFramework 的 jar 包,以及所有它依赖的其他 jar 包。这么做往往就引人了很多不必要的依赖。另一种做法是只下载 spring-framework-2.5.6.zip 这样一个包,这里不包含其他相关依赖,到实际使用的时候,再根据出错信息,或者查询相关文档,加入需要的其他依赖。很显然,这也是一件非常麻烦的事情。

Maven 的传递性依赖机制可以很好地解决这一问题。假如有一个项目 account-email,该项目有一个 org.springframework : spring-core:2.5.6 的依赖,而实际上 spring-core 也有它自己的依赖,我们可以直接访问位于中央仓库的该构件的 POM,也就是 spring-core-2.5.6.pom。该文件包含了一个 commons-logging 依赖,如下。

<dependency>
  <groupId>commons-logging</groupId>
  <artifactId>commons-logging</artifactId>
  <version>1.1.1</version>
  <scope>compile</scope>
</dependency>

该依赖的 scope 是 compile(或者没有声明依赖范围则依赖范围就是默认的 compie)。同时项目 account-email 中,spring-core 的依赖范围我们也设置为 compile。那么,此时 commons-logging 就会成为 account-email 的 compile 范围依赖,commons-logging 是 account-email 的一个传递性依赖。如下图
在这里插入图片描述
有了传递性依赖机制,在使用 SpringFramework 的时候就不用去考虑它依赖了什么,也不用担心引人多余的依赖。Maven 会解析各个直接依赖的 POM,将那些必要的间接依赖以传递性依赖的形式引人到当前的项目中。

传递性依赖和依赖范围

依赖范围不仅可以控制依赖与三种 classpah 的关系,还对传递性依赖产生影响。上面的例子中,account-email 对于 spring-core 的依赖范围是 compile,spring-core 对于 commons-logging 的依赖范围是 compile,那么 account-email 对于 commons-logging 这一传递性依赖的范围也就是 compile。

假设A依赖于B,B依赖于C,我们说A对于B是第一直接依赖,B对于C是第二直接依赖,A对于C是传递性依赖第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围,如下图所示,最左边一列表示第一直接依赖范围,最上面一行表示第二直接依赖范围,中间的交叉单元格则表示传递性依赖范围。
在这里插入图片描述
为了能够更好地理解,这里再举个例子。account-email 项目有一个com.icegreen : greenmail : 1.3.1b 的直接依赖,我们说这是第一直接依赖,其依赖范围是 test,而 greenmail 又有一个 javax.mail : mail : 1.4 的直接依赖,我们说这是第二直接依赖,其依赖范围是 compile。显然javax.mail : mail : 1.4 是 account-email 的传递性依赖,对照上图可以知道,当第一直接依赖范围为 test,第二直接依赖范围是 compile 的时候,传递性依赖的范围是 test,因此 javax.mail : mail : 1.4 是 account-email 的一个范围是 test 的传递性依赖。

仔细观察一下上图,可以发现这样的规律:

  • 当第二直接依赖的范围是 compile 的时候,传递性依赖的范围与第一直接依赖的范围一致
  • 当第二直接依赖的范围是 test 的时候依赖不会得以传递
  • 当第二直接依赖的范围是 provided的时候,只传递第一直接依赖范围也为 provided 的依赖,且传递性依赖的范围同样为 provided;
  • 当第二直接依赖的范围是 runtime 的时候,传递性依赖的范围与第一直接依赖的范围一致,但 compile 例外,此时传递性依赖的范围为runtime。

依赖调解

Maven 引人的传递性依赖机制,一方面大大简化和方便了依赖声明,另一方面,大部分情况下我们只需要关心项目的直接依赖是什么,而不用考虑这些直接依赖会引入什么传递性依赖。但有时候,当传递性依赖造成问题的时候,我们就需要清楚地知道该传递性依赖是从哪条依赖路径引入的

例如,项目 A 有这样的依赖关系:A -> B -> C -> X(1.0)、A -> D -> X(2.0),X 是 A 的传递性依赖,但是两条依赖路径上有两个版本的 X,那么哪个 X 会被 Maven 解析使用呢?两个版本都被解析显然是不对的,因为那会造成依赖重复,因此必须选择一个。

Maven依赖调解(Dependency Mediation)的第一原则是:路径最近者优先。该例中 X(1.0) 的路径长度为3,而 X(2.0) 的路径长度为2,因此 X(2.0) 会被解析使用。

依赖调解第一原则不能解决所有问题,比如这样的依赖关系:A -> B -> Y(1.0)、A -> C -> Y(2.0),Y(1.0) 和 Y(2.0) 的依赖路径长度是一样的,都为2。那么到底谁会被解析使用呢?

在 Maven 2.0.8 及之前的版本中,这是不确定的,但是从 Maven2.0.9 开始,为了尽可能避免构建的不确定性,Maven 定义了依赖调解的第二原则:第一声明者优先。在依赖路径长度相等的前提下,在 POM 中依赖声明的顺序决定了谁会被解析使用,顺序最靠前的那个依赖优胜。该例中,如果 B 的依赖声明在 C 之前,那么 Y(1.0) 就会被解析使用。

可选依赖

假设有这样一个依赖关系,项目 A 依赖于项目 B,项目 B 依赖于项目 X 和 Y,B 对于 X 和 Y 的依赖都是可选依赖:A -> B、B -> X(可选)、B -> Y(可选)。根据传递性依赖的定义,如果所有这三个(A 对 B 的依赖、B 对 X 的依赖 以及 B 对 Y 的依赖)依赖的范围都是 compile,那么 X、Y 就是 A 的 compile 范围传递性依赖。然而,由于这里 X、Y 是可选依赖,依赖将不会得以传递。换句话说,X、Y 将不会对 A 有任何影响,如下图所示。
在这里插入图片描述
为什么要使用可选依赖这一特性呢?可能项目 B 实现了两个特性,其中的特性一依赖于 X,特性二依赖于 Y,而且这两个特性是互斥的,用户不可能同时使用两个特性。比如 B 是一个持久层隔离工具包,它支持多种数据库,包括 MSQL、PostgreSQL等。在构建这个工具包的时候,需要这两种数据库的驱动程序,但在使用这个工具包的时候,只会依赖一种数据库。

项目 B 的依赖声明如下:

<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</artifactId>
        <version>8.4-701.jdbc3</version>
        <optional>true</optional>
    </dependency>
</dependencies>

上述 XML 代码片段中,使用<optional>元素表示 mysql-connector-java和 postgresql 这两个依赖为可选依赖,它们只会对当前项目 B 产生影响,当其他项目依赖于B的时候,这两个依赖不会被传递。因此,当项目 A 依赖于项目 B 的时候,如果其实际使用基于MySOL数据库,那么在项目 A 中就需要自己显式地声明 mysql-connector-java 这一依赖。

最后,关于可选依赖需要说明的一点是,在理想的情况下,是不应该使用可选依赖的。前面我们可以看到,使用可选依赖的原因是某一个项目实现了多个特性,在面向对象设计中,有个单一职责性原则,意指一个类应该只有一项职责,而不是糅合太多的功能。这个原则在规划 Maven 项目的时候也同样适用。在上面的例子中,更好的做法是为MYSQL 和 PostgreSQL 分别创建一个 Maven 项目,基于同样的groupld 分配不同的 artifactld,在各自的 POM 中声明对应的JDBC驱动依赖,而且不使用可选依赖,用户则根据需要选择使用哪个驱动的 Maven 项目的构件。由于传递性依赖的作用,就不用再声明 JDBC 驱动依赖。

最佳实践

Maven 依赖涉及的知识点比较多,在理解了主要的功能和原理之后,最需要的当然就是经验总结了,这里归纳了一些使用 Maven 依赖常见的技巧,方便用来避免和处理很多常见的问题。

排除依赖

传递性依赖会给项目隐式地引入很多依赖,这极大地简化了项目依赖的管理,但是有些时候这种特性也会带来问题。例如,当前项目有一个第三方依赖,而这个第三方依赖由于某些原因依赖了另外一个类库的 SNAPSHOT 版本,那么这个 SNAPSHOT 就会成为当前项目的传递性依赖,而 SNAPSHOT 的不稳定性会直接影响到当前的项目。这时就需要排除掉该 SNAPSHOT,并且在当前项目中声明该类库的某个正式发布的版本。还有一些情况,你可能也想要替换某个传递性依赖,比如 Sun JTA API,Hibernate 依赖于这个 JAR,但是由于版权的因素,该类库不在中央仓库中,而 Apache Geronimo 项目有一个对应的实现。这时你就可以排除 Sun JAT API,再声明 Geronimo 的 JTA API 实现,示例代码如下:

<dependencies>
    <dependency>
        <groupId>com.juvenxu.mvnbook</groupId>
        <artifactId>project-b</artifactId>
        <version>1.0.0</version>
        <exclusions>
            <exclusion>
                <groupId>com.juvenxu.mvnbook</groupId>
                <artifactId>project-c</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>com.juvenxu.mvnbook</groupId>
        <artifactId>project-c</artifactId>
        <version>1.0.0</version>
    </dependency>
</dependencies>

上述代码中,当前项目 A 依赖于项目 B,但是由于一些原因,不想引入传递性依赖 C 而是自己显式地声明对于项目 C 1.1.0 版本的依赖。代码中使用 exclusions 元素声明排除依赖,exclusions 可以包含一个或者多个 exclusion 子元素,因此可以排除一个或者多个传递性依赖。需要注意的是,声明 exclusion 的时候只需要 groupId 和 artifactld,而不需要 version 元素,这是因为只需要 groupId 和 artifactld 就能唯一定位依赖图中的某个依赖。换句话说,Maven 解析后的依赖中,不可能出现 groupId 和 artifactld 相同,但是 version 不同的两个依赖
在这里插入图片描述

归类依赖

简单说就是一批依赖来自同一项目(groupId)的不同模块(artifactId),但是版本号相同,此时可以不用为每个依赖声明一个 version,而是统一定义版本号,然后每个依赖引用这个版本号即可。

<properties>
    <myproject.version>1.0.0</myproject.version>
</properties>

<dependencies>
    <dependency>
        <groupId>com.juvenxu.mvnbook</groupId>
        <artifactId>project-b</artifactId>
        <version>${myproject.version}</version>
    </dependency>
    <dependency>
        <groupId>com.juvenxu.mvnbook</groupId>
        <artifactId>project-c</artifactId>
        <version>${myproject.version}</version>
    </dependency>
</dependencies>

这里简单用到了Maven属性,首先使用 <properties > 元素定义 Maven 属性,该例中定义了一个 myproject.version 子元素,其值为1.0.0。有了这个属性定义之后,Mave n运行的时候会将POM 中的所有的 ${myproject.version} 替换成实际值1.0.0。也就是说,可以使用美元符号和大括弧环绕的方式引用 Maven 属性。然后,将所有统一依赖的版本值用这一属性引用表示。

优化依赖

在软件开发过程中,程序员会通过重构等方式不断地优化自己的代码,使其变得更简洁、更灵活。同理,程序员也应该能够对 Maven 项目的依赖了然于胸,并对其进行优化如去除多余的依赖,显式地声明某些必要的依赖。

Maven 会自动解析所有项目的直接依赖和传递性依赖,并且根据规则正确判断每个依赖的范围,对于一些依赖冲突,也能进行调节,以确保任何一个构件只有唯一的版本在依赖中存在。在这些工作之后,最后得到的那些依赖被称为已解析依赖(Resolved Dependency)。可以运行如下的命令查看当前项目的已解析依赖:

mvn dependency:list

在你的项目中执行该命令,结果会显示项目的所有的已解析依赖,同时,每个依赖的范围也得以明确标示。在此基础上,还能进一步了解已解析依赖的信息。

将直接在当前项目 POM 声明的依赖定义为顶层依赖,而这些顶层依赖的依赖则定义为第二层依赖,以此类推,有第三、第四层依赖。当这些依赖经 Maven 解析后,就会构成一个依赖树,通过这棵依赖树就能很清楚地看到某个依赖是通过哪条传递路径引入的。可以运行如下命令查看当前项目的依赖树:

mvn dependency:tree

通过结果你会发现,有的依赖没有声明,但是通过别的依赖传递了进来,也能看见每个依赖的范围。

使用 dependency:list 和 dependency:tree 可以帮助我们详细了解项目中所有依赖的具体信息,在此基础上,还有dependency:analyze 工具可以帮助分析当前项目的依赖。

mvn dependency:analyze

该结果中重要的是两个部分。首先是 Used umdeclared dependencies,意指项目中使用到的,但是没有显式声明的依赖,这种依赖意味着潜在的风险,当前项目可能在使用它们,例如有很多相关的 Java import 声明,而这种依赖是通过直接依赖传递进来的,当升级直接依赖的时候,相关传递性依赖的版本也可能发生变化,这种变化不易察觉,但是有可能导致当前项目出错。例如由于接口的改变,当前项目中的相关代码无法编译。这种隐藏的、潜在的威胁一旦出现,就往往需要耗费大量的时间来查明真相。因此,最好显式声明任何项目中直接用到的依赖。

结果中还有一个重要的部分是 Unused declared dependeneies,意指项目中未使用的,但显式声明的依赖,需要注意的是,对于这样一类依赖,我们不应该简单地直接删除其声明,而是应该仔细分析。由于 dependency:analyze 只会分析编译主代码和测试代码需要用到的依赖,一些执行测试和运行时需要的依赖它就发现不了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1968638.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

单例模式及其思想

本文包括以下几点↓ 结论&#xff1a;设计模式不是简单地将一个固定的代码框架套用到项目中&#xff0c;而是一种严谨的编程思想&#xff0c;旨在提供解决特定问题的经验和指导。 单例模式&#xff08;Singleton Pattern&#xff09; 意图 旨在确保类只有一个实例&#xff…

Linux用户-用户组

作者介绍&#xff1a;简历上没有一个精通的运维工程师。希望大家多多关注我&#xff0c;我尽量把自己会的都分享给大家&#xff0c;下面的思维导图也是预计更新的内容和当前进度(不定时更新)。 Linux是一个多用户多任务操作系统,这意味着它可以同时支持多个用户登录并使用系统。…

每日OJ_牛客HJ74 参数解析

目录 牛客HJ74 参数解析 解析代码1 解析代码2 牛客HJ74 参数解析 参数解析_牛客题霸_牛客网 解析代码1 本题通过以空格和双引号为间隔&#xff0c;统计参数个数。对于双引号&#xff0c;通过添加flag&#xff0c;保证双引号中的空格被输出。 #include <iostream> #i…

解决文件夹打不开难题:数据恢复全攻略

在日常的电脑使用过程中&#xff0c;遇到文件夹无法打开的情况无疑是令人头疼的。这不仅可能影响到我们的工作效率&#xff0c;还可能导致重要数据的丢失。本文将深入探讨文件夹打不开的原因&#xff0c;并为您提供两种高效的数据恢复方案&#xff0c;助您轻松应对这一难题。 一…

p33 指针详解(1)(2)(3)

指针的进阶 1.字符指针 void test(int arr[]) { int szsizeof(arr)/sizeof(arr[0]); printf("%d\n", sz); } int main() { int arr[10] {0}; test(arr); return 0; } 这个代码在64位计算机中是8/42 在32位计算机中的是4/41 int main() {char c…

vue2 搭配 html2canvas 截图并设置截图时样式(不影响页面) 以及 base64转file文件上传 或者下载截图 小记

下载 npm install html2canvas --save引入 import html2canvas from "html2canvas"; //使用 html2canvasForChars() { // 使用that来存储当前Vue组件的上下文&#xff0c;以便在回调函数中使用 let that this; // 获取DOM中id为"charts"的元素&…

3.1 拓扑排序

有向图的存储 邻接矩阵 邻接表 拓扑排序 有向无环图&#xff1a;不存在环的有向图 环&#xff1a; 在有向图中&#xff0c;从一个节点出发&#xff0c;最终回到它自身的路径被称为环 入度&#xff1a; 以节点x为终点的有向边的条数被称为x的入度 出度&#xff1a; 以节…

汽车配件销售系统2024

下载在最后,编号ssm007 技术栈: ssmmysqljsp 展示: 下载地址: CSDN现在上传有问题,有兴趣的朋友先收藏.正常了贴上下载地址 备注: 运行有问题请私信我,私信按钮在文章左边) 另外接各种定制系统,java,spring,c,c,python

upload-labs靶场(超详解)1-16关

pass1 从代码中可以看出&#xff0c;是通过js进行文件格式检查 <script type"text/javascript">function checkFile() {var file document.getElementsByName(upload_file)[0].value;if (file null || file "") {alert("请选择要上传的文件…

Nmap/DNS信息收集实验

​实验背景 在安全服务项目中&#xff0c;需要对网络结构进行分析评估&#xff0c;其中风险评估第一步就是信息收集&#xff0c;主要包括活跃主机发现、开放端口号、系统指纹信息等。 实验设备 一个网络 net:cloud0 一台模拟黑客 kali 主机 一台靶机 windows 主机 实验…

go 语言中 init() 函数是什么时候执行的?

文章目录 一、init() 函数什么时候执行&#xff1f;二、init() 函数特点三、代码执行顺序四、多个 init() 函数执行顺序1、一个源文件中多个 init() 函数2、一个包中多个 init() 函数3、多个包中多个 init() 函数&#xff08;不存在依赖&#xff09;4、多个包中多个 init() 函数…

MySQL--数据类型

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 在MySQL数据库管理系统中&#xff0c;可以通过存储引擎来决定表的类型。同时&#xff0c;MySQL数据库管理系统也提供了数据类型决定表存储数据的类型 …

记录导致计算轮廓面积出错的一个坑点

1.前言 计算轮廓面积是常见的几何算法话题&#xff0c;获取轮廓面积、计算轮廓法线等场景会涉及到。计算轮廓面积的方法有很多&#xff0c;一种常用的是微积分思路的分段求和办法&#xff0c;即组成轮廓的每条线段与X轴或Y轴进行有向投影&#xff0c;轮廓边线与X轴或Y轴的投影之…

【SQL Server】SQL Server基础知识概览

目录 第1章&#xff1a;SQL Server 概览 SQL Server 版本介绍 SQL Server 架构 SQL Server 组件 第1章&#xff1a;SQL Server 概览 SQL Server 版本介绍 SQL Server 是 Microsoft 开发的一款关系型数据库管理系统 (RDBMS)&#xff0c;广泛应用于企业级数据存储和处理场景…

Mysql学习-day15

Mysql学习-day15 1. 行列转换 在MySQL中&#xff0c;行列转换可以通过使用CASE语句结合聚合函数来实现。 表t_score数据如图所示 我们想要以学科为列名&#xff0c;展示每个学生的科目成绩&#xff0c;可以先用CASE语句来选出每科的成绩&#xff0c;再进行求和。 选择科目时…

【C++】模拟实现list

&#x1f984;个人主页:修修修也 &#x1f38f;所属专栏:数据结构 ⚙️操作环境:Visual Studio 2022 目录 一.了解项目及其功能 &#x1f4cc;了解list官方标准 了解模拟实现list &#x1f4cc;了解更底层的list实现 二.list迭代器和vector迭代器的异同 &#x1f4cc;迭代…

SSH实现电脑VScode免密登录到虚拟机其原理

在网上想看一下这个原理。发现写的还是比较乱&#xff0c;所以自己总结了一份方便回顾 SSH免密登录的原理主要基于非对称密钥加密技术&#xff0c;比较常用的是RSA算法。 以下是SSH免密登录的详细步骤和原理&#xff1a; 1. 生成密钥对 在客户端上生成一对密钥&#xff0c;…

系统复习Java日志体系

一&#xff0c;我们采用硬编码体验一下几个使用比较多的日志 分别导入几种日志的 jar 包 <?xml version"1.0" encoding"UTF-8"?><project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSch…

【已解决】YOLOv8加载模型报错:super().__init__(torch._C.PyTorchFileReader(name_or_buffer))

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

C#——Json数据存储

本文使用的软件为VS2022&#xff0c;不同的软件使用上有些许差异。 C#数据存储 关于数据存储&#xff0c;一般在退出控制台之后&#xff0c;暂存的数据都会释放掉&#xff0c;有没有什么方法能够在下一次进入的时候还能加载上一次的数据呢&#xff1f;答案是有的&#xff0c;关…