Maven的依赖机制

Maven的知识点

Posted by qin4zhang on February 8, 2020

注意

想法及时记录,实现可以待做。

依赖管理是Maven的核心功能。 管理单个项目的依赖关系很容易。 管理包含数百个模块的多模块项目和应用程序的依赖关系也有可能。 Maven在定义,创建和维护具有良好定义的类路径和库版本的可复制构建方面大有帮助。

依赖传递

Maven通过自动包含传递性依赖关系,避免了发现和指定您自己的依赖关系所需的库的需要。

从指定的远程存储库中读取依赖项的项目文件可简化此功能。 通常,这些项目的所有依赖项都将在您的项目中使用,就像该项目从其父项或从其依赖项继承的任何依赖项一样。

依赖关系的级别数没有限制。仅当发现循环依赖性时才会出现问题。

有了传递性依赖关系,所包含库的图可以很快变得很大。 因此,还有一些其他功能可以限制包括哪些依赖项:

  • 依赖调解 这确定了遇到多个版本作为依赖项时将选择工件的哪个版本。 Maven选择了“最近的定义”。 也就是说,它使用依赖关系树中最接近您的项目的版本。 您始终可以通过在项目的POM中明确声明版本来保证版本。 请注意,如果两个依赖关系版本在依赖关系树中的深度相同,则第一个声明将获胜。
    • “最近的定义”意味着所使用的版本将在依赖关系树中最接近您的项目。考虑以下依赖关系树:
          A
          ├── B
          │   └── C
          │       └── D 2.0
          └── E
              └── D 1.0
      

      在文本中,A, B和C的依赖关系被定义为A -> B -> C -> D 2.0和A -> E -> D 1.0,那么当构建A时将使用D 1.0,因为从A到E到D的路径更短。您可以在a中显式地向d2.0添加一个依赖项,以强制使用d2.0,如下所示:

          A
          ├── B
          │   └── C
          │       └── D 2.0
          ├── E
          │   └── D 1.0
          │
          └── D 2.0 
      
  • 依赖管理 这样,项目作者可以在传递依赖项或未指定版本的依赖项中直接指定要使用的工件的版本。 在上一节的示例中,即使A没有直接使用依赖项,也直接将其添加到A。相反,A可以在其dependencyManagement部分中将D包括为依赖项,并直接控制D曾经使用的版本 ,或者它是否被引用过。

  • 依赖范围 这样就可以仅包含适合于当前构建阶段的依赖项。这将在下面更详细地描述。

  • 排除依赖 如果项目X依赖于项目Y,并且项目Y依赖于项目Z,则项目X的所有者可以使用“ exclusion”元素将项目Z作为依赖项显式排除。

  • 可选依赖 如果项目Y依赖于项目Z,则项目Y的所有者可以使用“ optional”元素将项目Z标记为可选依赖项。 当项目X取决于项目Y时,X将仅取决于Y,而不取决于Y的可选依赖项Z。然后,项目X的所有者可以选择显式添加对Z的依赖项。 (将可选依赖项视为“默认情况下不包括”可能会有所帮助。)

尽管可传递依赖项可以隐式包含所需的依赖项,但是最好明确指定源代码直接使用的依赖项。 最佳实践证明了它的价值,尤其是在项目的依赖项更改其依赖项时。

例如,假设您的项目指定一个依赖是另一个项目B, 项目B指定依赖项目C .如果你直接使用组件项目C,但是项目C不在你的指定项目A中,当项目B由于依赖项目C突然更新/删除会导致构建失败。

dependency:analyze插件目标可以分析依赖的关系。

依赖范围

依赖关系范围用于限制依赖关系的可传递性,并确定何时将依赖关系包括在类路径中。

这里有6种范围:

  • compile 这是默认范围,如果未指定则使用。编译依赖项在项目的所有类路径中均可用。此外,这些依赖项会传播到相关项目。
  • provided 这个很像compile,但是它表明了希望JDK或者容器在运行时提供依赖。比如,在J2EE中,Servlet API和有关J2EE的都是provided,因为web容器都提供了这些依赖。依赖A如果设置了这种范围,那么它仅用于项目的编译和单元测试,在运行时不会在classpath中。它不会被传递。
  • runtime 这个范围表明依赖在编译器可不需要,只在运行执行的时候需要。Maven在运行时和单元测试期包含了这种依赖,但是编译器不会包含。
  • test 这个范围表明了依赖不是应用的正常使用需要,只是为了测试期,这种也不会被依赖传递。这种通常用于单元测试类,比如JUintMockito
  • system 这个跟provided很像,该依赖于三种classpath的关系和provided依赖范围完全一致。区别在于system依赖范围必须通过systemPath元素显示的指定依赖文件的路径。
  • import 导入依赖范围,该依赖范围不会对三种classpath产生影响,只有在部分中的pom类型依赖项才支持此范围,它指示要替换为指定POM的部分中的有效依赖项列表的依赖项。由于它们被替换,具有导入范围的依赖项实际上不参与限制依赖项的传递性。

如下表所示,每个范围(import除外)以不同的方式影响传递依赖。 如果将依赖项设置为左列中的范围,则该依赖项在上一行中具有范围的传递性依赖项将导致在主项目中具有相交处列出的范围的依赖项。 如果未列出作用域,则表示该依赖项被省略。

  compile provided runtime test
compile compile(*) - runtime -
provided provided - provided -
runtime runtime - runtime -
test test - test -

(*)注释:打算将其改为运行时范围,以便必须明确列出所有编译依赖项。 但是,如果您依赖的库从另一个库扩展了一个类,则两者都必须在编译时可用。 因此,即使编译时间相关性是可传递的,它们仍保留为编译范围。

依赖管理

依赖性管理部分是用于集中依赖性信息的机制。 当您有一组从公共父项继承的项目时,可以将有关依赖项的所有信息放入公共POM中,并在子POM中具有对工件的更简单引用。 通过一些示例可以最好地说明该机制。 鉴于这两个扩展相同父级的POM:

项目A:

<project>
  ...
  <dependencies>
    <dependency>
      <groupId>group-a</groupId>
      <artifactId>artifact-a</artifactId>
      <version>1.0</version>
      <exclusions>
        <exclusion>
          <groupId>group-c</groupId>
          <artifactId>excluded-artifact</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>group-a</groupId>
      <artifactId>artifact-b</artifactId>
      <version>1.0</version>
      <type>bar</type>
      <scope>runtime</scope>
    </dependency>
  </dependencies>
</project>

项目B:

<project>
  ...
  <dependencies>
    <dependency>
      <groupId>group-c</groupId>
      <artifactId>artifact-b</artifactId>
      <version>1.0</version>
      <type>war</type>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>group-a</groupId>
      <artifactId>artifact-b</artifactId>
      <version>1.0</version>
      <type>bar</type>
      <scope>runtime</scope>
    </dependency>
  </dependencies>
</project>

这两个示例POM共享一个公共依存关系,每个都有一个非一般的依存关系。 此信息可以这样放置在父POM中:

<project>
  ...
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>group-a</groupId>
        <artifactId>artifact-a</artifactId>
        <version>1.0</version>
 
        <exclusions>
          <exclusion>
            <groupId>group-c</groupId>
            <artifactId>excluded-artifact</artifactId>
          </exclusion>
        </exclusions>
 
      </dependency>
 
      <dependency>
        <groupId>group-c</groupId>
        <artifactId>artifact-b</artifactId>
        <version>1.0</version>
        <type>war</type>
        <scope>runtime</scope>
      </dependency>
 
      <dependency>
        <groupId>group-a</groupId>
        <artifactId>artifact-b</artifactId>
        <version>1.0</version>
        <type>bar</type>
        <scope>runtime</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
</project>

然后,两个子POM变得更加简单:

<project>
  ...
  <dependencies>
    <dependency>
      <groupId>group-a</groupId>
      <artifactId>artifact-a</artifactId>
    </dependency>
 
    <dependency>
      <groupId>group-a</groupId>
      <artifactId>artifact-b</artifactId>
      <!-- This is not a jar dependency, so we must specify type. -->
      <type>bar</type>
    </dependency>
  </dependencies>
</project>
<project>
  ...
  <dependencies>
    <dependency>
      <groupId>group-c</groupId>
      <artifactId>artifact-b</artifactId>
      <!-- This is not a jar dependency, so we must specify type. -->
      <type>war</type>
    </dependency>
 
    <dependency>
      <groupId>group-a</groupId>
      <artifactId>artifact-b</artifactId>
      <!-- This is not a jar dependency, so we must specify type. -->
      <type>bar</type>
    </dependency>
  </dependencies>
</project>

注释:在其中两个依赖项引用中,我们必须指定元素。 这是因为用于将依赖项引用与dependencyManagement部分进行匹配的最小信息集实际上是{groupId,artifactId,type,classifier}。 在许多情况下,这些依赖项将引用没有分类器的jar工件。 由于类型字段的默认值为jar,默认分类器为null,因此这使我们可以简化设置为{groupId,artifactId}的标识。

依赖管理部分的第二个非常重要的用途是控制在传递性依赖性中使用的工件的版本。例如,请考虑以下项目:

项目A:

<project>
 <modelVersion>4.0.0</modelVersion>
 <groupId>maven</groupId>
 <artifactId>A</artifactId>
 <packaging>pom</packaging>
 <name>A</name>
 <version>1.0</version>
 <dependencyManagement>
   <dependencies>
     <dependency>
       <groupId>test</groupId>
       <artifactId>a</artifactId>
       <version>1.2</version>
     </dependency>
     <dependency>
       <groupId>test</groupId>
       <artifactId>b</artifactId>
       <version>1.0</version>
       <scope>compile</scope>
     </dependency>
     <dependency>
       <groupId>test</groupId>
       <artifactId>c</artifactId>
       <version>1.0</version>
       <scope>compile</scope>
     </dependency>
     <dependency>
       <groupId>test</groupId>
       <artifactId>d</artifactId>
       <version>1.2</version>
     </dependency>
   </dependencies>
 </dependencyManagement>
</project>

项目B:

<project>
  <parent>
    <artifactId>A</artifactId>
    <groupId>maven</groupId>
    <version>1.0</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>
  <groupId>maven</groupId>
  <artifactId>B</artifactId>
  <packaging>pom</packaging>
  <name>B</name>
  <version>1.0</version>
 
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>test</groupId>
        <artifactId>d</artifactId>
        <version>1.0</version>
      </dependency>
    </dependencies>
  </dependencyManagement>
 
  <dependencies>
    <dependency>
      <groupId>test</groupId>
      <artifactId>a</artifactId>
      <version>1.0</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>test</groupId>
      <artifactId>c</artifactId>
      <scope>runtime</scope>
    </dependency>
  </dependencies>
</project>

当在项目B上运行maven时,将使用a,b,c和d的版本1.0,而不考虑其POM中指定的版本。

  • a和c都被声明为项目的依赖项,因此由于依赖项中介,因此使用了1.0版。 两者都具有运行时作用域,因为它是直接指定的。
  • b是在B的父级的依赖项管理部分中定义的,并且由于依赖项管理优先于传递性依赖项的依赖项中介,因此如果在a或c的POM中引用版本1.0,则将选择该版本。 b也将具有编译范围。
  • 最后,由于d是在B的依赖性管理部分中指定的,因此d应该是a或c的依赖性(或可传递依赖性),因此将选择版本1.0-再次是因为依赖性管理优先于依赖性中介,并且因为当前POM的声明采用 优先于其父母的声明。

依赖导入

上一节中的示例描述了如何通过继承指定托管依赖项。 但是,在较大的项目中,可能无法完成此操作,因为一个项目只能从单个父项继承。 为了适应这种情况,项目可以从其他项目中导入托管依赖项。 这是通过将POM工件声明为具有“import”范围的依赖项来实现的。

项目B:

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>maven</groupId>
  <artifactId>B</artifactId>
  <packaging>pom</packaging>
  <name>B</name>
  <version>1.0</version>
 
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>maven</groupId>
        <artifactId>A</artifactId>
        <version>1.0</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>test</groupId>
        <artifactId>d</artifactId>
        <version>1.0</version>
      </dependency>
    </dependencies>
  </dependencyManagement>
 
  <dependencies>
    <dependency>
      <groupId>test</groupId>
      <artifactId>a</artifactId>
      <version>1.0</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>test</groupId>
      <artifactId>c</artifactId>
      <scope>runtime</scope>
    </dependency>
  </dependencies>
</project>

假设A是前面示例中定义的POM,则最终结果将相同。除了d之外,A的所有托管依赖项都将合并到B中,因为它是在此POM中定义的。

项目X:

<project>
 <modelVersion>4.0.0</modelVersion>
 <groupId>maven</groupId>
 <artifactId>X</artifactId>
 <packaging>pom</packaging>
 <name>X</name>
 <version>1.0</version>
 
 <dependencyManagement>
   <dependencies>
     <dependency>
       <groupId>test</groupId>
       <artifactId>a</artifactId>
       <version>1.1</version>
     </dependency>
     <dependency>
       <groupId>test</groupId>
       <artifactId>b</artifactId>
       <version>1.0</version>
       <scope>compile</scope>
     </dependency>
   </dependencies>
 </dependencyManagement>
</project>

项目Y:

<project>
 <modelVersion>4.0.0</modelVersion>
 <groupId>maven</groupId>
 <artifactId>Y</artifactId>
 <packaging>pom</packaging>
 <name>Y</name>
 <version>1.0</version>
 
 <dependencyManagement>
   <dependencies>
     <dependency>
       <groupId>test</groupId>
       <artifactId>a</artifactId>
       <version>1.2</version>
     </dependency>
     <dependency>
       <groupId>test</groupId>
       <artifactId>c</artifactId>
       <version>1.0</version>
       <scope>compile</scope>
     </dependency>
   </dependencies>
 </dependencyManagement>
</project>

项目Z:

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>maven</groupId>
  <artifactId>Z</artifactId>
  <packaging>pom</packaging>
  <name>Z</name>
  <version>1.0</version>
 
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>maven</groupId>
        <artifactId>X</artifactId>
        <version>1.0</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>maven</groupId>
        <artifactId>Y</artifactId>
        <version>1.0</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
</project>

在上面的示例中,Z从X和Y导入托管依赖关系。但是,X和Y都包含依赖关系a。 在这里,将使用a的1.1版,因为先声明了X,而在Z的dependencyManagement中未声明a。

此过程是递归的。例如,如果X导入另一个POM Q,则在处理Z时,将简单地显示所有Q的托管依赖项都在X中定义。

BOM POMs

导入用于定义相关工件的“库”时最有效,这些工件通常是多项目构建的一部分。 一个项目使用这些库中的一个或多个工件非常普遍。 但是,有时使用工件与库中分发的版本保持同步来保持项目中的版本有时很困难。 下面的模式说明了如何创建“物料清单”(BOM)以供其他项目使用。

项目的根是BOM POM。它定义了将在库中创建的所有工件的版本。希望使用该库的其他项目应将此POM导入其POM的dependencyManagement部分。

<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">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.test</groupId>
  <artifactId>bom</artifactId>
  <version>1.0.0</version>
  <packaging>pom</packaging>
  <properties>
    <project1Version>1.0.0</project1Version>
    <project2Version>1.0.0</project2Version>
  </properties>
 
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.test</groupId>
        <artifactId>project1</artifactId>
        <version>${project1Version}</version>
      </dependency>
      <dependency>
        <groupId>com.test</groupId>
        <artifactId>project2</artifactId>
        <version>${project2Version}</version>
      </dependency>
    </dependencies>
  </dependencyManagement>
 
  <modules>
    <module>parent</module>
  </modules>
</project>

父子项目将BOM POM作为其父项。这是正常的多项目pom。

<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">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.test</groupId>
    <version>1.0.0</version>
    <artifactId>bom</artifactId>
  </parent>
 
  <groupId>com.test</groupId>
  <artifactId>parent</artifactId>
  <version>1.0.0</version>
  <packaging>pom</packaging>
 
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.12</version>
      </dependency>
      <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>1.1.1</version>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <modules>
    <module>project1</module>
    <module>project2</module>
  </modules>
</project>

接下来是实际的项目POM。

<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">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.test</groupId>
    <version>1.0.0</version>
    <artifactId>parent</artifactId>
  </parent>
  <groupId>com.test</groupId>
  <artifactId>project1</artifactId>
  <version>${project1Version}</version>
  <packaging>jar</packaging>
 
  <dependencies>
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
    </dependency>
  </dependencies>
</project>
 
<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">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.test</groupId>
    <version>1.0.0</version>
    <artifactId>parent</artifactId>
  </parent>
  <groupId>com.test</groupId>
  <artifactId>project2</artifactId>
  <version>${project2Version}</version>
  <packaging>jar</packaging>
 
  <dependencies>
    <dependency>
      <groupId>commons-logging</groupId>
      <artifactId>commons-logging</artifactId>
    </dependency>
  </dependencies>
</project>

接下来的项目说明了现在如何在无需指定从属项目的版本的情况下将库用于另一个项目。

<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">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.test</groupId>
  <artifactId>use</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>
 
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.test</groupId>
        <artifactId>bom</artifactId>
        <version>1.0.0</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>com.test</groupId>
      <artifactId>project1</artifactId>
    </dependency>
    <dependency>
      <groupId>com.test</groupId>
      <artifactId>project2</artifactId>
    </dependency>
  </dependencies>
</project>

最后,在创建导入依赖项的项目时,请注意以下几点:

  • 不要尝试导入在当前POM的子模块中定义的POM。尝试执行该操作将导致构建失败,因为它将无法找到POM。
  • 永远不要将导入POM的POM声明为目标POM的父(或祖父母等)。无法解决循环问题,将引发异常。
  • 当引用其POM具有传递依赖项的工件时,项目需要将这些工件的版本指定为托管依赖项。 不这样做会导致构建失败,因为构件可能没有指定版本。 (在任何情况下,这都应被视为最佳实践,因为它可以防止工件的版本从一个版本更改为另一个版本)。