Maven 基础知识(二)依赖机制


传递依赖

maven通过读取分析工程依赖的其他工程的pom文件,自动的把依赖工程对应的依赖(包括这些工程自身的依赖以及从父工程继承到的依赖)加入到当前工程的依赖里面。

传递依赖机制虽然可以让我们方便的引入项目需要的全部依赖,但很容易就会使我们工程的依赖变的庞大复杂,并且引入的依赖很可能会同时依赖一个jar包的不同版本。因此maven在传递依赖机制中加入了一些机制来管理最终加入到工程中的依赖项

  • 依赖仲裁(Dependency mediation)
  • 依赖范围(Dependency scope)
  • 依赖管理(Dependency management)
  • 排除依赖(Excluded dependencies)
  • 选择性依赖(Optional dependencies)

依赖仲裁

当在依赖树中出现同一个依赖的多个版本时,依赖仲裁 用来决定最终采用哪个版本。

maven采用选择 最近 的机制来决定最终的版本号, 最近 指的是在工程的依赖树中距离当前的工程路径最短,这就是为什么我们可以通过在当前工程中声明一个特定依赖,从而复盖传递过来的依赖的原因。如果两个依赖在依赖树中的距离一样,则选择 最先 声明的。

  1. 场景1 工程A有如下依赖树

    A
    ├── B
    │   └── C
    │       └── D 2.0
    └── E
      └── D 1.0
    
    

    此时对于D的依赖,有两条路径 A -> B -> C -> D 2.0A -> E -> D 1.0,因为第二条的路径短,所以最终选择 D 1.0

  2. 场景2 工程A有如下依赖树

    A
    ├── B
    │   └── C
    │       └── D 2.0
    └── E
        └—— F
            └—— D 3.0
    
    

    此时对于D的依赖有两条 A -> B -> C -> D 2.0A -> E -> F -> D 3.0,此时两条路径一样长,选择先声明的,所以最终选择D 2.0

  • 根据 依赖仲裁 的机制,当我们在自己的工程中明确写明一个依赖的版本时,就可以确保这就是最终采用的版本。但是有一个例外就是 硬性需求(Hard requirements)优先级总是高于 软需求(Soft requirement) 。

依赖范围

  1. classpath

    在说明 依赖范围 的作用之前,先简单了解一下maven的执行环境信息。maven在执行不同命令如compile、test,或者是在一个构建的不同phase,会利用不同的classpath对代码执行编译、测试、运行,默认预设了如下四种classpath

    • compile classpath
    • runtime classpath
    • test classpath
    • plugin classpath

    其中 plugin classpath 是插件执行的path,正常应用开发中不会涉及到。另外三种path则和我们息息相关。

    可以通过在 pom.xml 文件中追加如下plugin查看具体工程的各个classpath,

     
            
                
                    org.apache.maven.plugins
                    maven-antrun-plugin
                    1.7
                    
                        
                            compile
                            compile
                            
                                
                                    
                                    
                                    
                                    
    
                                    
                                    
                                    
                                    
                                
                            
                            
                                run
                            
                        
                    
                
            
        
    
    
  2. scope
    依赖范围 就是用来控制依赖在哪一个classpath中使用,同时限定哪些依赖可以向后传递。在maven中,依赖总共有六种scope

    1. compile(编译)

      compile 是默认的scope,当依赖没有明确指定scope时,maven会自动设置成compile 。会出现在所有环境里面(测试、编译、运行)。向后进行传递。

    2. provided(提供)

      provided表示运行环境会提供这个依赖,例如servlet-api相关的依赖,因为在servelet容器中已经有了,所以在其中运行应用时就不需要这个依赖。会出现在编译、测试环境下,但是不会出现在运行环境中。不会向后传递。

    3. runtime(运行)

      runtime表示运行时依赖,指在编译时不需要,在运行时需要的依赖。例如数据库连接的具体实现mysql-connector-java。会出现在运行、测试环境下,但是不会出现在编译环境中。向后传递。

    4. test(测试)

      test表示在应用正常运行时不需要这个依赖,只在编译测试代码和执行测试用例时需要。例如JUnitMockito相关的依赖。出现在测试环境中,不出现在编译和运行环境中。不会向后传递。

    5. system(系统)

      不推荐使用

      system provided范围比较像,也是由运行环境提供,但是一般指用来区分不同操作系统下的依赖。可通过systemPath来具体指定依赖的位置.会出现在编译、测试环境下,但是不会出现在运行环境中。不会向后传递。

    6. import

      import类型的依赖只能出现在模块中,用来引入pom类型的工程。其效果相当于是把这个依赖用它引入的pom工程中有效的中的依赖列表替换掉,具体例子参考Dependency Management。因为是用在模块中,所以import依赖不会影响真正的依赖传递。

  3. 对于及联依赖,可以用如下表来说明

    compile provided runtime test
    compile compile - runtime
    provided provided - provided
    runtime runtime - runtime
    test test - test
    • 左边一列是我们工程直接依赖对应的scope
    • 上边一列是我们直接依赖的工程对应依赖的scope
    • 交叉的部分是我们工程中对依赖的依赖对应的scope

    只有compileruntime可以向后传递

  4. 最后追加一个依赖的例子

     
        
            org.yaml
            snakeyaml
            1.17
        
    
        
            junit
            junit
            4.12
            test
        
    
        
            mysql
            mysql-connector-java
            8.0.19
            runtime
        
    
        
            javax.servlet
            servlet-api
            2.3
            provided
        
    
    
    
    

    对应的各个环境的path

      compile classpath: /Users/chengaofeng/ideaspace/maven-learn/maven-test/target/classes:/Users/chengaofeng/.m2/repository/org/yaml/snakeyaml/1.17/snakeyaml-1.17.jar:/Users/chengaofeng/.m2/repository/javax/servlet/servlet-api/2.3/servlet-api-2.3.jar
      runtime classpath: /Users/chengaofeng/ideaspace/maven-learn/maven-test/target/classes:/Users/chengaofeng/.m2/repository/org/yaml/snakeyaml/1.17/snakeyaml-1.17.jar:/Users/chengaofeng/.m2/repository/mysql/mysql-connector-java/8.0.19/mysql-connector-java-8.0.19.jar:/Users/chengaofeng/.m2/repository/com/google/protobuf/protobuf-java/3.6.1/protobuf-java-3.6.1.jar
      test classpath:    /Users/chengaofeng/ideaspace/maven-learn/maven-test/target/test-classes:/Users/chengaofeng/ideaspace/maven-learn/maven-test/target/classes:/Users/chengaofeng/.m2/repository/org/yaml/snakeyaml/1.17/snakeyaml-1.17.jar:/Users/chengaofeng/.m2/repository/junit/junit/4.12/junit-4.12.jar:/Users/chengaofeng/.m2/repository/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar:/Users/chengaofeng/.m2/repository/mysql/mysql-connector-java/8.0.19/mysql-connector-java-8.0.19.jar:/Users/chengaofeng/.m2/repository/com/google/protobuf/protobuf-java/3.6.1/protobuf-java-3.6.1.jar:/Users/chengaofeng/.m2/repository/javax/servlet/servlet-api/2.3/servlet-api-2.3.jar
    
    
    

依赖管理

  1. 依赖管理 的第一个用处是用来将依赖信息进行集中化管理。

    当我们有许多工程继承自一个parent时,最常见的做法是把这些工程的依赖信息统一放在父pom的dependencyManagement中,在子工程中只指定依赖的group和artifactId,这样就可以保证所有工程中依赖相同的版本。当需要修改版本时,只用修改父pom里面dependencyManagement中的定义,所有子模块就自动依赖到修改后的版本。

    例:

    Project A:

    
      ...
      
        
          group-a
          artifact-a
          1.0
          
            
              group-c
              excluded-artifact
            
          
        
        
          group-a
          artifact-b
          1.0
          bar
          runtime
        
      
    
    
    

    Project B:

    
      ...
      
        
          group-c
          artifact-b
          1.0
          war
          runtime
        
        
          group-a
          artifact-b
          1.0
          bar
          runtime
        
      
    
    
    

    A工程 和 B工程有一个相同的依赖 group-a:artifact-b:1.0,另外各自都有一个特有的依赖。如果用 依赖管理 ,可以将依赖信息放到如下所示的父pom中

    
      ...
      
        
          
            group-a
            artifact-a
            1.0
     
            
              
                group-c
                excluded-artifact
              
            
     
          
     
          
            group-c
            artifact-b
            1.0
            war
            runtime
          
     
          
            group-a
            artifact-b
            1.0
            bar
            runtime
          
        
      
    
    
    

    之后 A工程的pom可以变成下面的内容

    
      ...
      
        
          group-a
          artifact-a
        
     
        
          group-a
          artifact-b
          
          bar
        
      
    
    
    

    B工程的pom转变为

    
      ...
      
        
          group-c
          artifact-b
          
          war
        
     
        
          group-a
          artifact-b
          
          bar
        
      
    
    
    
    • 因为从dependencyManagement中找到匹配的依赖需要四个信息 {groupId, artifactId, type, classifier},其中type的默认值是jarclassifier的默认值是null,所以在上面两个pom中,依赖的类型不是jar时,都额外追加了type
  2. 依赖管理 的另一个重要的作用是控制传递依赖的版本

    例:

    父模块 Project A:

    
     4.0.0
     maven
     A
     pom
     A
     1.0
     
       
         
           test
           a
           1.2
         
         
           test
           b
           1.0
           compile
         
         
           test
           c
           1.0
           compile
         
         
           test
           d
           1.2
         
       
     
    
    
    

    子模块 Project B:

    
      
        A
        maven
        1.0
      
      4.0.0
      maven
      B
      pom
      B
      1.0
     
      
        
          
            test
            d
            1.0
          
        
      
     
      
        
          test
          a
          1.0
          runtime
        
        
          test
          c
          runtime
        
      
    
    
    

    对于子模块 Project B,

    • 对于a,因为直接在dependencies中指定了a的版本1.0,所以a的版本一定会是1.0
    • 对于c,因为在父dependency management中指定了版本1.0,所以c的版本一定是1.0
    • 对于b,如果a或c中依赖了b,因为父dependency management中指定了版本1.0,并且在传递依赖中,dependency management的优先级高于依赖仲裁,所以无论a、c中依赖的b是什么版本,b的版本一定是1.0
    • 对于d,如果a或c中依赖了d,因为当前工程中的dependency management优先级高于父pom中的dependency management,又由于dependency management的优先级高于依赖仲裁,所以d的版本一定是1.0

    由上可以看出,当传递依赖引入同一个依赖的不同版本时,靠 依赖仲裁 的最近原则,会给人带来困惑,依赖顺序会影响到最终的结果,但是引入 依赖管理 后,就可以明确决定依赖的具体版本了。

  3. 依赖导入

    在讨论依赖范围时,我们简单探讨了 import类型,用来在 依赖管理 中引入其他的 pom 类型的工程。下面我们通过一个例子说明如何在dependency management利用 import导入其他工程定义好的 依赖管理。

    1. 例子1:

      Project A:

      
       4.0.0
       maven
       A
       pom
       A
       1.0
       
         
           
             test
             a
             1.2
           
           
             test
             b
             1.0
             compile
           
           
             test
             c
             1.0
             compile
           
           
             test
             d
             1.2
           
         
       
      
      
      

      Project B:

      
        4.0.0
        maven
        B
        pom
        B
        1.0
       
        
          
            
              maven
              A
              1.0
              pom
              import
            
            
              test
              d
              1.0
            
          
        
       
        
          
            test
            a
            1.0
            runtime
          
          
            test
            c
            runtime
          
        
      
      
      
      • A 是一个 pom 类型的工程
      • B 在dependencyManagement中通过import引入了A,其作用类似于把AdependencyManagement中的依赖列表插入到BdependencyManagement中。
    2. 例子2:

      Project X:

      
       4.0.0
       maven
       X
       pom
       X
       1.0
       
       
         
           
             test
             a
             1.1
           
           
             test
             b
             1.0
             compile
           
         
       
      
      
      

      Project Y:

      
       4.0.0
       maven
       Y
       pom
       Y
       1.0
       
       
         
           
             test
             a
             1.2
           
           
             test
             c
             1.0
             compile
           
         
       
      
      
      

      Project Z:

      
        4.0.0
        maven
        Z
        pom
        Z
        1.0
       
        
          
            
              maven
              X
              1.0
              pom
              import
            
            
              maven
              Y
              1.0
              pom
              import
            
          
        
      
      
      
      • X、Y中有一个相同的定义 a
      • 在工程Z中通过import引入了X、Y。因为 Z 中没有重新定义对 a 的依赖,而 X 是在 Y之前声明的,所以 Z 最终采用的是 X 中的 a,即1.1 版本。并且此过程是递归的,如果X的dependencyManagementimport了工程Q,对于Z而言,会认为在Q中的dependencyManagement就是定义在X中的。即 依赖管理 中的 import的效果是直接替换,而不是像 依赖仲裁一样的最近原则,但是当前工程的优先级要高于import进来的

      import 的效果可以简单理解成 把 import 的工程中的dependencyManagement递归的插入到当前位置,如果插入某个依赖时,发现当前工程中有这个依赖的定义了,就跳过此依赖,即直接声明的优先级高于import进来的