Maven使用之Maven的传递性依赖(三)

一、何为传递性依赖

当我们在项目中使用Spring时,如果不使用Maven,那么就需要在项目中手动下载Spring的相关依赖jar包,例如我们需要下载

commons-dbcp.jar、commons-beanutils.jar、aspectjweaver.jar、asm.jar等待许多的jar包,很显然这是一件非常麻烦的事。

Maven的传递型依赖机制可以很好的解决这一问题。以org.springframework:spring-2.5.6依赖为例,spring也有自己的依赖。其pom.xml的部分依赖信息如下:

	<dependencies>
<!-- External Dependencies -->
		<dependency>
			<groupId>aopalliance</groupId>
			<artifactId>aopalliance</artifactId>
			<version>1.0</version>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>asm</groupId>
			<artifactId>asm</artifactId>
			<version>2.2.3</version>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>asm</groupId>
			<artifactId>asm-commons</artifactId>
			<version>2.2.3</version>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>asm</groupId>
			<artifactId>asm-util</artifactId>
			<version>2.2.3</version>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjrt</artifactId>
			<version>1.6.1</version>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjweaver</artifactId>
			<version>1.6.1</version>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>backport-util-concurrent</groupId>
			<artifactId>backport-util-concurrent</artifactId>
			<version>3.0</version>
			<optional>true</optional>
		</dependency>
    <dependencies>

以上的依赖没有声明依赖范围,那么其依赖范围默认是compile。如不使用Maven,我们需要将上述依赖的jar包都需要导入我们的工程中。而有了Maven的传递性依赖机制,在我们导入spring依赖时就不需要考虑它依赖了什么,也不用担心过多或少引入依赖信息。Maven会解析每个直接依赖的pom.xml,将那些必要的间接依赖,以传递性依赖的形式引入到当前项目中。

二、传递性依赖和依赖范围

依赖范围不仅可以控制依赖与三种classpath的关系,还对传递性依赖产生影响。假如A依赖于B,B依赖于C,我们说A对于B是第一直接依赖,B对于C是第二直接依赖,A对于C是传递性依赖。第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围。其依赖范围如下图所示,其中,最左一列表示第一直接传递依赖范围,最上面一行表示第二直接传递依赖范围。

仔细观察上表,可以发现这样一个规律:当第二直接依赖的范围是compile的时候,传递性依赖的范围与第一直接依赖的范围一致;当第二直接依赖的范围是test的时候,依赖不会传递;当第二直接传递依赖的范围是provided的时候,只传递第一直接传递依赖范围也为provided的依赖,其传递性依赖的范围同样是provided;当第二直接依赖的范围是runtime时,传递性依赖的范围与第一直接依赖的范围一致,但除了compile例外,此时传递性依赖的范围为runtime。

三、依赖调解

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

例如:项目A有这样的依赖关系:A->B->C->X(1.0)、A->D->X(2.0),X是A的传递性依赖,但是这两条依赖路径上有1.0和2.0的两个版本的X,那么哪个X会被Maven解析使用呢?两个版本都引用显然是不对的,那样会造成依赖重复而出现引用冲突,因此必须选择一个。Maven依赖调解的第一原则是:路径最近者优先。该例中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。那么到底会是谁被解析使用呢?在Maven2.0.9之后,为了尽可能避免构建的不确定性,Maven定义了依赖调解的第二原则:第一声明者优先。在依赖长度相等的情况下,在POM中依赖声明的顺序决定了谁会被解析使用,顺序最靠前的那个依赖优先使用。在该例子中,如果B的依赖声明在C之前,那么Y(1.0)就会被解析使用。

四、可选依赖

假如有这样一个依赖关系,项目A依赖项目B,项目B依赖于项目X和Y,B对于X和Y的依赖都是可选依赖:A->B、B->X(可选)、B->Y(可选)。根据传递性依赖的定义,如果所有这三个依赖范围都是compile,那么X、Y就是A的compile范围的传递性依赖。然而,由于这里的X和Y是可选依赖,依赖将不会得以传递。换句话说,X、Y将不会对A有任何影响。

为什么要使用可选依赖这一特性呢?可能项目B实现了两个特性,其中的特性一依赖于X,特性二依赖于Y,而这两个特性是互斥的,用户不可能同时使用这两个特性。比如B是一个持久层隔离工具包,它支持多种数据库,包括MySQL、PostgreSQL等,在构建工具包的时候,需要这两种数据库的驱动程序,但是使用这个工具包的时候,只会依赖一种数据库。

例如项目B的依赖声明代码清单如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>maven-learn</artifactId>
        <groupId>com.jackson.mvnlearn</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>project-b</artifactId>

    <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.jdbc4</version>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

上述XML中,使用<optional>true</optional>表示mysql-connector-java和postgresql这两个依赖是可选依赖,它们只会对当前项目B产生影响,当其他项目依赖于B项目时,这两个依赖不会被传递。因此,当项目A依赖项目B时,如果其实际使用基于MySQL数据库,那么在项目A中就需要显示声明mysql-connector-java这一依赖,项目A的POM文件代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>maven-learn</artifactId>
        <groupId>com.jackson.mvnlearn</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>project-a</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.jackson.mvnlearn</groupId>
            <artifactId>project-b</artifactId>
            <version>1.0-SNAPSHO</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.10</version>
        </dependency>
    </dependencies>
</project>

最后,关于可选依赖需要说明的是,在理想情况下,是不应该使用可选依赖的。之所以使用可选依赖是因为某一个项目实现了多个特性,在面向对象设计中,有一个单一职责原则,意指一个类应该只有一项职责,而不是揉合太多功能。这个原则在规划Maven项目时也同样适用。在上面的例子中,更好的做法是为MySQL和PostgreSQL分别创建一个Maven项目,基于同样的groupId分配不同的artifactId,在各自的POM中声明对应的JDBC驱动,而不是使用可选依赖。

五、依赖排除

传递性依赖会给项目隐式地引入很多依赖,这极大简化了项目依赖的管理,但是有些时候这种特性也会带来问题。例如,当前项目有一个第三方依赖,而这个第三方依赖由于某些原因依赖了另一个类库的SNAPSHOP版本,那么这个SNAPSHOP就会成为当前项目的传递性依赖,二SNAPSHOP的不稳定性会直接影响到当前项目。这时候就需要排除该SNAPSHOP,并且在当前项目中声明该类库的某个正式发布版本。可能还有一些情况需要排除并替换某个依赖。

例如,项目A依赖于项目B,但是由于一些原因,不想引入项目B中的某个依赖C,而是自己显示声明C-1.1.0的版本依赖。代码清单如下:

<dependencies>
        <dependency>
            <groupId>com.jackson.mvnlearn</groupId>
            <artifactId>project-b</artifactId>
            <version>1.0-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>com.jackson.mvnlearn</groupId>
                    <artifactId>project-c</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.jackson.mvnlearn</groupId>
            <artifactId>project-c</artifactId>
            <version>1.1.0</version>
        </dependency>
    </dependencies>

在上述代码中使用exclusions元素声明排除依赖,exclusions可以包含一个或多个exclusion子元素,因此可以排除一个或者多个传递性依赖。需要注意的是,声明exclusion的时候自需要groupId和artifactId,而不需要version元素,这是因为只需要groupId和artifactId就可以唯一定位依赖图中的某个依赖。也就是说,在Maven解析后的依赖中,不可能出现groupId和artifactId相同,但是version不同的两个依赖。

如果需要排除项目B的所有传递性依赖,可以使用占位符(*)号表示,示例代码如下:

<dependencies>
        <dependency>
            <groupId>com.jackson.mvnlearn</groupId>
            <artifactId>project-b</artifactId>
            <version>1.0-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>*</groupId>
                    <artifactId>*</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

六、归类依赖

我们在使用Spring时,会引入许多的Spring Framework的依赖,例如org.springframework:spring-core:2.5.6、org.springframework:spring-beans:2.5.6、org.springframework:spring-context:2.5.6和org.springframework-context-support:2.5.6等依赖信息,它们来自同一项目的不同模块。因此,所有这些依赖的版本都是相同的,而且可以预见,如果将来需要升级Spring Framework,这些依赖的版本也需要一起升级。如果一个一个修改是比较麻烦并且容易出错的,因此对于项目中引用的Spring Framework因该在一个唯一的地方定义版本,并且在dependency声明引用这一版本,这样,在升级Spring Framework的时候就只需要修改一处。实现方式见如下代码:

<properties>
        <spring.version>2.5.6</spring.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring</artifactId>
                <version>${spring.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-core</artifactId>
                <version>${spring.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-beans</artifactId>
                <version>${spring.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
                <version>${spring.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context-support</artifactId>
                <version>${spring.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

上述代码简单地使用了Maven的properties元素,首先使用properties元素定义一个Maven属性,该例中定义了一个spring.version子元素,其值为2.5.6。有了这个属性之后,Maven运行时就会将POM中的所有${spring.version}替换成实际值2.5.6。也就是说,可以使用${}的方式引用Maven属性。这样定义之后,在以后升级Spring Framework的版本时,只需要修改propertis中定义的spring.version元素的值即可。

七、优化依赖

在Maven项目中,我们如何去除多余的依赖、解决依赖冲突、如何明确地显示声明某些必要的依赖,对Maven项目进行优化?

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

mvn dependency:list
[INFO] 
[INFO] The following files have been resolved:
[INFO]    org.springframework:spring:jar:2.5.6:compile
[INFO]    org.springframework:spring-beans:jar:2.5.6:compile
[INFO]    commons-logging:commons-logging:jar:1.1.1:compile
[INFO]    org.hamcrest:hamcrest-core:jar:1.3:test
[INFO]    aopalliance:aopalliance:jar:1.0:compile
[INFO]    junit:junit:jar:4.12:test
[INFO]    javax.mail:mail:jar:1.4.1:compile
[INFO]    javax.activation:activation:jar:1.1:compile
[INFO]    org.slf4j:slf4j-api:jar:1.3.1:test
[INFO]    org.springframework:spring-context-support:jar:2.5.6:compile
[INFO]    com.icegreen:greenmail:jar:1.3.1b:test
[INFO]    org.springframework:spring-core:jar:2.5.6:compile
[INFO]    org.springframework:spring-context:jar:2.5.6:compile
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 25.918 s
[INFO] Finished at: 2019-11-13T23:18:04+08:00

在此基础上,我们能够进一步了解已解析依赖的信息。Maven将直接在当前项目POM中声明的依赖定义为顶层依赖,而这些顶层依赖中的依赖则定义为第二次依赖,以此类推,有第三层、第四层依赖等,当这些依赖经Maven解析之后,就会构成一个依赖树,通过这颗依赖树就可以很清晰地看到某个依赖是通过哪条路径引入的。可以运行如下命令查看当前项目的依赖树:

mvn denpendency:tree
 --- maven-dependency-plugin:2.8:tree (default-cli) @ account-email ---
[INFO] com.jackson.mvnlearn:account-email:jar:1.0-SNAPSHOT
[INFO] +- org.springframework:spring:jar:2.5.6:compile
[INFO] |  \- commons-logging:commons-logging:jar:1.1.1:compile
[INFO] +- org.springframework:spring-core:jar:2.5.6:compile
[INFO] +- org.springframework:spring-beans:jar:2.5.6:compile
[INFO] +- org.springframework:spring-context:jar:2.5.6:compile
[INFO] |  \- aopalliance:aopalliance:jar:1.0:compile
[INFO] +- org.springframework:spring-context-support:jar:2.5.6:compile
[INFO] +- javax.mail:mail:jar:1.4.1:compile
[INFO] |  \- javax.activation:activation:jar:1.1:compile
[INFO] +- junit:junit:jar:4.12:test
[INFO] |  \- org.hamcrest:hamcrest-core:jar:1.3:test
[INFO] \- com.icegreen:greenmail:jar:1.3.1b:test
[INFO]    \- org.slf4j:slf4j-api:jar:1.3.1:test
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.052 s
[INFO] Finished at: 2019-11-13T23:25:28+08:00
[INFO] ------------------------------------------------------------------------

也可以执行如下命令将当前项目的依赖树信息输入到某个文件中:

 mvn dependency:tree > tree.txt

从输出的依赖树可以看出,虽然我们没有声明org.slf4j:slf4j-api:jar:1.3.1这一依赖,但是它还是经过com.icegreen:greenmail:1.3.1b成为了当前项目的传递性依赖,而且其范围是test。

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

mvn dependency:analyze
$ mvn dependency:analyze
[INFO] Scanning for projects...
[INFO] 
[INFO] -----------------< com.jackson.mvnlearn:account-email >-----------------
[INFO] Building account-email 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] >>> maven-dependency-plugin:2.8:analyze (default-cli) > test-compile @ account-email >>>
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ account-email ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 2 resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ account-email ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ account-email ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 1 resource
[INFO] 
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ account-email ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] <<< maven-dependency-plugin:2.8:analyze (default-cli) < test-compile @ account-email <<<
[INFO] 
[INFO] 
[INFO] --- maven-dependency-plugin:2.8:analyze (default-cli) @ account-email ---
[WARNING] Used undeclared dependencies found:
[WARNING]    org.springframework:spring-context:jar:2.5.6:compile
[WARNING] Unused declared dependencies found:
[WARNING]    org.springframework:spring-core:jar:2.5.6:compile
[WARNING]    org.springframework:spring-beans:jar:2.5.6:compile
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.231 s
[INFO] Finished at: 2019-11-13T23:50:33+08:00
[INFO] ------------------------------------------------------------------------

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

结果在还有一个重要的部分是 Unused declared dependencies found,意指项目中未使用的,但是显示声明的依赖。对于这样的依赖,我们不应该简单地直接删除其声明,而是应该仔细分析。

补充说明:该文章是本人在阅读许晓斌著的《Maven实战》这本书时为了加深自己的理解和记忆进行编写的。这里也推荐各位读者去购买《Maven实战》这本书进行阅读学习

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值