【Maven专栏系列】依赖管理那些事

2,722 阅读7分钟

这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战

相关阅读

【Maven专栏系列】重新认识一下Maven这款工具

【Maven专栏系列】Maven项目从0到1

前言

依赖管理是Maven的核心功能,也是我们使用过程中用的最多的,对于开发人员来说,可能不需要用Maven去打包部署,但很难避免不去添加或删除依赖。

在没有Maven或类似的构建工具之前,管理单个项目的依赖关系还相对简单,但是管理由数百个模块组成的多模块项目和应用程序的依赖关系则相当复杂,Maven的出现一部分原因就是为了解决这个问题。

依赖传递

如果你的项目依赖于一个依赖项,比如依赖项ABC,而依赖项ABC本身又依赖于XYZ,那么你的项目也依赖于XYZ。这种特性被称作依赖传递。

传递依赖的级别没有限制,多少层级都可以,只有在出现循环依赖情况才会出问题。

循环依赖是指A依赖B,B依赖C,如果在项目C中再添加依赖项A,那就形成了循环依赖。

因为有依赖传递特性,会面临一些问题,而Maven为了解决这些问题,有一些专门的机制。

依赖仲裁:依赖仲裁是当一个项目下的依赖或传递依赖出现多个版本时如果选择的一种机制。而在选择时Maven使用**“就近原则”**,比如下面的情况:

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

A项目中有A->B->C->D2.0和A->E->D1.0两条依赖线,那么在A项目构建时会使用D1.0作为依赖,因为D1.0比D2.0离A项目更近。

如果当两个依赖项的距离一样时,则优先使用定义在前面的依赖。

也可以在A项目中显示指定依赖项的版本,比如在下面的例子中则使用的是D2.0版本:

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

DependencyManagement: 可以通过在DependencyManagement中指定依赖项的版本,对于指定了的依赖项,在遇到传递依赖时会直接使用指定的版本。在上一个例子中我们直接将D2.0添加到A的依赖中,我们也可以直接在DependencyManagement中指定D依赖项的版本。

依赖作用域Scope: 可以指定依赖项在哪个构建阶段使用,后面会单独介绍。

依赖排除: 如果项目X依赖于项目Y,而项目Y依赖于项目Z,那么项目X可以使用exclusion元素显式地将项目Z排除。

可选的依赖关系: 如果项目Y依赖于项目Z,项目Y可以使用optional元素将项目Z标记为可选依赖项。当项目X依赖于项目Y时,X将只依赖于Y而不依赖于Y的可选依赖项Z。项目X可以根据自己的选择添加对Z的依赖项。

虽然Maven有依赖传递特性,但是我们还是应该明确指定我们需要的依赖项版本,比如当我们的项目A依赖于B,B依赖于C,如果我们在A中不直接指定C的版本,当B中对C的版本发生改变时,可能就会引起我们项目A的构建失败。

依赖作用域scope

依赖作用域scope用于限制依赖关系的传递性,并确定依赖关系在什么阶段生效。

在Maven中有6个scope选项:

compile

这是默认scope。表示在所有阶段都可以使用,并且依赖关系会被传播到依赖项目。

provided

希望由JDK或容器在运行时提供依赖项。例如,在JavaEE构建web应用程序时,会将Servlet API和相关Java EE API的依赖scope设置为provided ,因为在web容器中已经提供了这些类。具有此作用域的依赖项被添加到用于编译和测试的classpath,而不是运行时classpath。它不具备传递性的。

runtime

编译不需要此依赖项,运行时需要。Maven在运行时和测试classpath中包含此作用域的依赖项。

test

此范围表明该依赖项对于应用程序的正常使用不是必需的,并且仅在测试编译和执行阶段可用。这个范围是不可传递的。通常,这个范围用于JUnitMockito等测试库。也就是说如果只在src/test/java中使用,不在src/main/java中使用的依赖可以使用此作用域。

system

这个作用域类似于provided作用域,区别是需要直接提供JAR文件,构建时不会在仓库中去搜索。

import

这个作用域与其他的有所不同,仅支持在<dependencyManagement>节点中type节点为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">
    <modelVersion>4.0.0</modelVersion>
    
	<groupId>com.heiz</groupId>
    <artifactId>hello-world-scope</artifactId>
    <packaging>pom</packaging>
    
	<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.heiz</groupId>
                <artifactId>hello-world-package</artifactId>
                <version>1.0.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>18.0</version>
            </dependency>
        </dependencies>
    <dependencyManagement>
</project>

这个配置的含义是,在当前这个项目中,会将hello-world-package项目中的dependencyManagement中的依赖项引入到当前项目,类似于一种配置信息的包含引入。

DependencyManagement

DependencyManagement的作用可以总结为两点:

  • 对公共依赖项统一管理,简化子项目配置
  • 对依赖项目的传递性依赖版本选择进行控制

下面有两个例子分别体现这两点作用。

举例:简化子项目配置

项目A中的依赖

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

项目B中的依赖

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

    <dependency>
      <groupId>groupId-a</groupId>
      <artifactId>artifact-c</artifactId>
      <version>1.0</version>
    </dependency>	
  </dependencies>
</project>

项目A和B有一个共同的依赖项,又各有一个特有的依赖项,这些信息可以抽取到父POM中的DependencyManagement中:

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

然后在A,B子项目中的POM则会变得很简单:

项目A

<project>
  ...
  <dependencies>
    <dependency>
      <groupId>group-a</groupId>
      <artifactId>artifact-b</artifactId>
      <type>bar</type>
    </dependency>
    <dependency>
      <groupId>groupId-a</groupId>
      <artifactId>artifact-a</artifactId>
    </dependency>	
  </dependencies>
</project>

项目B

<project>
  ...
  <dependencies>
    <dependency>
      <groupId>group-a</groupId>
      <artifactId>artifact-b</artifactId>
      <type>bar</type>
    </dependency>

    <dependency>
      <groupId>groupId-a</groupId>
      <artifactId>artifact-c</artifactId>
    </dependency>	
  </dependencies>
</project>

在子项目中依赖和DependencyManagement匹配是通过{groupId,artifactId,type,classifier}

classifier通常用于区分项目对于不同版本JDK对应的Jar,在许多情况下,并不会按照JDK版本区分,所以通知我们只需要{groupId, artifactId},而type的类型默认是jar,所以在依赖项的typejar时也可以为空。

举例:传递依赖版本控制

项目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>

当maven在项目B上运行时,将使用artifactId a、b、c和d的1.0版本,而不管它们的POM中指定的具体版本。

  • a和c都在项目B的dependencies中定义,所以会遵循就近原则,使用1.0版本。
  • 因为项目B的parent项目A的dependencyManagement中配置b的版本为1.0,如果b是a或c的依赖项(或传递依赖项),也使用1.0版本,scope是compile,是因为dependencyManagement的优先级高于就近原则
  • 而d因为就定义在项目B的dependencyManagement中,如果d是a或c的依赖项(或传递依赖项),那么将选择1.0版本。

BOM

BOM是Bill Of Materials的缩写,翻译为物料清单,一开始我不是很理解的。

通俗点讲bom就是一种特殊的pom文件,专门用来管理项目中年以来项的版本,并且可以对这些版本做个性化定义。

一般都会将一个项目的根pom文件定义为bom,并在其中定义一些可能在子项目中需要使用到依赖的版本号。

下面就是一个bom 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>
  <groupId>com.heiz</groupId>
  <artifactId>bom</artifactId>
  <version>1.0.0</version>
  <!-- packaging配置为pom -->
  <packaging>pom</packaging>
  
    <!-- 定义子模块中会使用到的项目版本号 -->
  <properties>
	<spring.version>4.3.4.RELEASE</spring.version>
    <spring.boot.version>1.4.2.RELEASE</spring.boot.version>
  </properties>
 
  <dependencyManagement>
    <dependencies>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-beans</artifactId>
			<version>${spring.version}</version>
		</dependency>
    </dependencies>
  </dependencyManagement> 
 <!-- 配置改项目下的子模块artifactId -->
  <modules>
    <module>bom-child</module>
  </modules>
</project>

在这个BOM文件中,定义了spring.versionspring.boot.version,那么在dependencyManagement或者子项目的pom文件中,便可以使用${spring.version}获取到在BOM中定义的版本。

在一个项目中使用某个已存在的bom文件可以通过继承import两种方式。

小结

本期内容我们主要介绍了Maven依赖的一些管理方式和特性。依赖因为有依赖传递的特性,首先可以指定scope进行设置是否传递,对传递依赖通过就近原则选择版本,而通过dependencyManagement则可以指定版本;以及特殊的pom文件BOM。


以上就是本期内容,如果对你有帮助,点个赞是对我最大的鼓励。