Maven 的依赖管理

918 阅读5分钟

这是我参与 8 月更文挑战的第7天,活动详情查看: 8月更文挑战

Java使用Maven进行构建,通过在pom.xml中配置项目中的依赖,可以让Maven自动管理依赖。Maven中的依赖具有传递依赖,传递依赖可以说是把双刃剑,一方面通过依赖传递可以减少缺包的情况,也让使用方不用明确知道依赖的依赖是什么,但在另一方面传递依赖会导致依赖冲突。 了解Maven的依赖管理特点,可以让我们知道遇到依赖冲突时该怎么处理。

依赖配置 (dependency)

依赖配置的标签

<dependencies>
        <dependency>
            <groupId></groupId>
            <artifactId></artifactId>
            <version></version>
            <scope>compile</scope>
            <type>jar</type>
            <optional>true</optional>
            <exclusions>
                <exclusion>
                    <groupId></groupId>
                    <artifactId></artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
  • groupId:工程组
  • arfifactId:工程名
  • version:工程版本号
  • scope:依赖范围,默认值compile
  • type:依赖类型,默认值jar(大多数情况下不需要配置)
  • optional:标记依赖是否可选
  • exclusions:用来排除传递性依赖,当不需要依赖项目的某依赖时,可使用该属性排除掉传递性依赖。

依赖范围 (scope)

Maven在编译,测试,运行时使用的classpath路径都不一致,依赖范围就是用来控制依赖与这三种classpath的关系。

scope 依赖范围的取值有:

  • compile:编译依赖范围,默认值;
  • test:测试依赖范围,使用此范围的依赖只对于测试classpath路径有效,对于编译及运行时无法使用此依赖,例如:JUint 只在编译测试代码以及运行测试代码时才需要;
  • provided:已提供依赖范围。针对编译,测试的classpath有效,运行时无效。例如:servlet-api,测试编译时需要,但在tomcat容器中有提供,在运行时不需要Maven重复运行一遍;
  • runtime:运行时依赖范围。针对测试,运行的classpath有效,编译时无效;例如JDBC驱动的实现;
  • system:系统依赖范围。依赖范围与provided一致,引入外部依赖时会用到,需要配合systemPath元素一起使用,引入本机的Jar包依赖;
  • import:导入依赖范围。(Maven 2.0.9版本以上)

传递性依赖

传递性依赖是指依赖的依赖具有传递性,Maven会解析各个直接依赖的pom,将那些必要的间接依赖,以传递性依赖的形式引入当前的项目中。

假设A项目依赖于B项目,B项目又依赖于C项目,那么就可以说A是B的第一直接依赖,B对于C是第二直接依赖,A对于C是传递性依赖。第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围。

传递性依赖的范围关系如下表所示,最左边的一列是第一直接依赖,最上边的一行是第二直接依赖,中间的表示传递性依赖的范围。

-compiletestprovidedruntime
compilecompile--runtime
testtest--test
providedprovided-providedprovided
runtimeruntime--runtime

从表中可以发现,compile范围只有在第一,第二直接依赖范围都是compile时,才会被传递依赖;

依赖调解

当项目中的依赖存在依赖冲突时,Maven是如何选择的呢? 例如 存在这样的依赖:

  1. A->B->C->X(1.0)
  2. A->D->X(2.0)

此时Maven会采用最短路径优先的原则去选择依赖,这里2的依赖路径更短,所以会选择X(2.0)。

而当两个依赖路径长度相同时,Maven会采用第一声明者优先的原则,即哪个先在pom中声明就决定了谁会被解析使用。

可选依赖 (optional)

当依赖中的optional 属性为true时,表示该依赖为可选依赖。当被声明为可选依赖时,这个依赖将不会被传递。这种情况一般说明该依赖只用于当前项目中。

如果其他项目依赖的项目中存在可选依赖时,又需要该依赖,那么就需要显示声明了。(毕竟没有传递依赖)

依赖关系的命令

查看已解析的依赖:mvn dependency:list
查看依赖的依赖树关系:mvn dependency:tree
分析依赖:mvn dependency:analyze

使用分析依赖命令时,可注意warning后的语句。有两种情况:

  • Used undeclered dependencies: 表示项目中有用到,但没有显示声明。这种尽量将依赖显示声明,防止依赖升级,导致传递性依赖失效;
  • Unused declared dependencies:表示项目中未使用,但显示声明的依赖,这类依赖不应该简单地直接删除。因为dependency:analyze 语句只会分析编译主代码和测试代码需要用到的依赖,执行测试和测试运行的依赖就分析不了了。

项目中的依赖冲突

  • 直接依赖冲突
  • 传递依赖冲突

依赖冲突的几种处理方法

  • 使用scope范围避免传递依赖;
  • 在dependency标签中使用 exclusions 标签去除冲突的依赖
<dependency>
    <groupId></groupId>
    <artifactId></artifactId>
    <exclusions>
        <exclusion> <!--排除不需要的依赖 -->
            <groupId></groupId>
            <artifactId></artifactId>
        </exclusion>
    </exclusions>
</dependency>
  • 如果想要排除某个包,但这个包在多个依赖中都会传递依赖,使用第二种方式会有多个相同配置,不太妥当。可以在dependency插件中配置依赖复制的隔离包。使用该配置,不影响编译,在copy-dependency依赖时不会将排除的包给复制到指定路径下。
<plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-dependency-plugin</artifactId>
        <version>3.0.1</version>
        <executions>
                <execution>
                    <id>copy</id>
                    <phase>package</phase>
                    <goals>
                         <goal>copy</goal>
                     </goals>
                    <configuration>
                    <!--排除groupIds,以逗号分隔 -->
                       <excludeGroupIds>org.htmlparser,commons-codec,org.apache.ant</excludeGroupIds>
                       <!--排除ArtifactIds,以逗号分隔 -->
                    <excludeArtifactIds>guava,javaee,javaee-16</excludeArtifactIds>
                    </configuration>
                </execution>
        </executions>
</plugin>