Maven 依赖

依赖范围

Maven 在编译项目主代码的时候需要使用一套 classpath。其次,在编译和执行测试的时候会使用另外一套 classpath。最后,实际运行 Maven 项目的时候,又会使用一套 classpath(这里的 classpath 指的是 Java 本身的 classpath,和 Maven、IDE 无关,平时新建 Maven 项目看到的 classpath 是 Maven 约定的,IDE 遵循 Maven 的约定生成的,也可以自己定义 classpath)

所谓的依赖范围就是用来控制依赖与这三种 classpath(编译、测试、运行)的关系,Maven 有以下几种依赖范围:

  • compile:编译依赖范围。如果没有指定,默认使用该依赖范围。使用此依赖范围时,对于编译、测试、运行都有效。例如:spring-core,编译、测试、运行时都需要使用该依赖

  • test:测试依赖范围。只对测试 classpath 有效,在编译主代码或者运行项目时无法使用此类依赖。例如:JUnit,它只在编译测试代码以及运行测试的时候才需要

  • provided:已提供依赖范围。对于编译和测试时有效,但在运行时无效。例如:servlet-api,编译和测试项目的时候需要该依赖,但运行时,由于容器已经提供,就不需要 Maven 重复引入了

  • runtime:运行时依赖范围。对于测试和运行 classpath 有效,但在编译主代码时无效。例如:JDBC 驱动实现,编译时只需要 JDK 提供的 JDBC 接口,只有在执行测试和运行时才需要实现上述接口的具体 JDBC 驱动(因为反射)

  • system:系统依赖范围。同 provided。但是,使用 system 范围的依赖时必须通过 systemPath 元素显式地指定依赖文件的路径。由于此类以来不是通过 Maven 仓库解析的,而且往往与本机系统绑定,可能造成构建的不可移植。主要用于依赖本地的、且 Maven 仓库之外的类库文件。systemPath 元素可以引用环境变量,如:


  javax.sql
  jdbc-stdext
  2.5
  system
  ${JAVA_HOME}/lib/test.jar

  • import:引入依赖范围,该依赖范围不会对三种 classpath 产生实际的影响,详见 dependencyManagement

上述除 import 以外的各种依赖范围与三种 classpath 的关系如下:

依赖范围 对于编译 classpath 有效 对于测试 classpath 有效 对于运行时 classpath 有效 例子
compile Y Y Y spring-core
test —— Y —— JUnit
provided Y Y —— servlet-api
runtime —— Y Y JDBC 驱动实现
system Y Y —— 本地的,Maven 仓库之外的类库文件

传递依赖和依赖范围

依赖一个 a.jar 时,如果 a.jar 依赖 b.jar,b.jar 会被 Maven 自动加载进来

假设 A 依赖于 B,B 依赖于 C,我们说 A 对于 B 是第一直接依赖,B 对于 C 是第二直接依赖,A 对于 C 是传递性依赖。第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围。如下表所示,最左边一行表示第一直接依赖范围,最上面一行表示第二直接依赖范围,中间的交叉单元格则表示传递性依赖范围

compile test provided runtime
compile compile —— —— runtime
test test —— —— test
provided provided —— provided provided
runtime runtime —— —— runtime
  • 当第二直接依赖的范围是 compile 的时候,传递性依赖的范围与第一直接依赖的范围一致

  • 当第二直接依赖的范围是 test 的时候,依赖不会得以传递

  • 当第二直接依赖的范围是 provided 的时候,只传递第一直接依赖范围也为 provided 的依赖,且传递性依赖的范围同样为 provided

  • 当第二直接依赖的范围是 runtime 的时候,传递性依赖的范围与第一直接依赖的范围一致,但 compile 例外,此时传递依赖范围为 runtime

依赖调解

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

  • 原则一:路径最近者优先。例如:A ->B ->C ->X(1.0),同时 A ->D ->X(2.0)。X 是 A 的传递性依赖,但是两条依赖路径上有两个版本的 X。因为 X(2.0) 路径更短,所以 2.0 版本的 X 会被解析使用

  • 原则二:第一声明者优先。在依赖长度相等情况下,解析在 pom 中依赖声明中顺序靠前的。例如:A ->B ->X(1.0) 同时 A ->D ->X(2.0)。如果 B 在 D 之前声明,那么 X(1.0) 会被解析

除以上两种原则外,还可以手动排除,例如:A ->B ->X(1.0) 同时 A ->X(2.0)。如果项目 A 希望加载 X(2.0) 可通过 元素来显式排除

可选依赖

假设:A ->B、B ->X(optional)、B ->Y(optional)。根据传递性依赖的定义,如果所有这三个依赖的范围都是 compile,那么 X、Y 就是 A 的 compile 范围传递性依赖。然而,由于这里 X、Y 是可选依赖,依赖讲不会传递。也就是说,X、Y 不会对 A 有任何影响

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

B 项目的依赖声明如下:


    1.0.0
    com.xxx.xx
    project-b
    2.5
    
        
            mysql
            mysql-connector-java
            5.1.10
            true
        
         
            postgresql
            postgresql
            8.4-701.jdbc3
            true
        
    

使用 元素表示这两个为可选依赖,这时依赖不会传递到 A 项目,当 A 项目需要使用基于 MySQL 数据库时,需要显式声明对 MySQL 的依赖:


    1.0.0
    com.xxx.xx
    project-a
    2.5
    
        
            com.xxx.xx
            project-b
            2.5
        
        
            mysql
            mysql-connector-java
            5.1.10
        
    

另外:在理想的情况下是不应该使用可选依赖的,因为在面向对象设计中,有一个单一职责原则,即一个 jar 的职责应该只有一个。所以在上面的例子中,最好是为 MySQL 和 PostgreSQL 分别创建一个 Maven 项目,基于同样的 groupId 分配不同的 artifactId,如 com.xxx.xx:project-b-mysql 和 com.xxx.xx:project-b-postgresql。然后再各自的 pom 中声明对应的 JDBC 驱动依赖,用户则根据需要选择使用 project-b-mysql 或 project-b-postgresql。由于传递性依赖的作用,就不用再声明 JDBC 驱动依赖了

排除依赖

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


    1.0.0
    com.xxx.xx
    project-a
    1.0.0
    
        
            com.xxx.xx
            project-b
            1.0.0
            
                com.xxx.xx
                project-c
            
        
        
            com.xxx.xx
            project-c
            1.1.1
        
    

上述代码中,项目 A 依赖于 B,但是由于某些原因,不想引入传递性依赖 C,而是自己显示地声明对于项目 C 1.1.1 版本的依赖。代码中使用 exclusions 元素声明排除依赖,exclusions 可以包含一个或多个 exclusion 子元素,因此可以排除一个或多个传递性依赖

归类依赖

有些依赖来自同一项目的不同模块,比如 org.springframework:spring-core:2.5.6、org.springframework:spring-beans:2.5.6、org.springframework:spring-context:2.5.6,所有这些依赖的版本都是相同的,如果将来需要升级 Spring Framework,这些依赖的版本会一起升级

就像用常量 PI 定义圆周率一样,Maven 也可以在一个唯一的地方定义版本,并且在 dependency 声明中引用这一版本。这样在升级 Spring Framework 时就只需修改一处:


    1.0.0
    com.xxx.xx
    project-a
    1.0.0

    
        2.5.6
    

    
        
            org.springframework
            spring-core
            ${springframework.version}
        
        
            org.springframework
            spring-beans
            ${springframework.version}
        
    

依赖优化

代码需要不断重构才能达到最优,依赖管理也是一样,需要不断的进行去除多余依赖,以及显式的声明某些必要的依赖

Maven 会自动解析所有项目的直接依赖和传递依赖,并根据规则判断每个依赖的范围,对于一些依赖冲突也能进行调节,以确保任何一个构件只有唯一的版本在依赖中存在。在这些工作之后得到这个项目的完整的已解析依赖

可通过运行以下命令查看当前项目的已解析依赖:

mvn dependency:list
Maven 依赖_第1张图片
已解析依赖查看

上图展示了当前项目中所有已解析的依赖,同时每个依赖的范围也得以明确标示

如果将直接在 pom 中声明的依赖定义为第一层依赖,这些顶层依赖的依赖定义为第二层依赖,则以此类推可以形成一个完整的依赖树

可运行以下明细查看当前项目的依赖树

mvn dependency:tree
Maven 依赖_第2张图片
依赖树

可运行以下命令对当前项目依赖进行简单分析

mvn dependency:analyze
Maven 依赖_第3张图片
依赖分析

上图中 Used undeclared dependencies,表示项目中使用到的,但是没有显式声明的依赖。这种依赖意味着潜在的风险,当前项目直接在使用它们,例如有很多相关的 Java import 声明,而这种依赖是通过直接依赖传递进来的,当升级直接依赖时,相关传递性依赖的版本也可能发生变化,这种变化不易察觉,但有可能导致当前项目出错。这种潜在的威胁一旦出现,就往往需要耗费大量时间来查明真相

还有一个 Unused declared dependencies,表示项目中未使用的,但是显式声明的依赖。需要注意的是,对于这类依赖,不应该简单地直接删除其声明,而是应该仔细分析。由于 dependency:analyze 只会分析编译主代码和测试代码需要用到的依赖,一些执行测试和运行时需要的依赖它就发现不了。所以在优化依赖时一定要小心测试

你可能感兴趣的