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

依赖的配置

依赖会有基本的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 只会分析编译主代码和测试代码需要用到的依赖,一些执行测试和运行时需要的依赖它就发现不了。

  • 12
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值