1. 概述

多模块 Maven 项目往往依赖关系错综复杂,模块之间相互引用越多,依赖图就越深,越容易出现意想不到的问题。

本文将深入探讨 如何解决 Maven 中的构件(artifact)版本冲突,帮助你在实际开发中避免“明明引入了却用不了”的尴尬。

我们将从一个故意引入不同版本依赖的多模块项目入手,逐步演示:

  • 如何通过 排除依赖(exclusion)依赖管理(dependencyManagement) 避免错误版本被引入
  • 如何使用 maven-enforcer-plugin 插件,从源头杜绝传递性依赖带来的隐患

✅ 掌握这些技巧,能让你在面对“jar 包冲突”时更加从容,不再靠“删删改改试出来”。


2. 什么是构件版本冲突

Maven 的强大之处在于能自动解析传递性依赖(transitive dependencies)——即你引入的依赖可能自身又依赖了其他库,Maven 会自动把这些“间接依赖”也拉进来。

但这也埋下了隐患:当多个直接依赖引用了同一个构件的不同版本时,就会发生版本冲突

⚠️ 这种冲突可能导致:

  • ✅ 编译失败(找不到新版本才有的方法)
  • ✅ 运行时异常(加载了错误版本的类)

2.1 项目结构示例

我们构建一个典型的多模块项目来复现问题:

version-collision
    project-a
    project-b
    project-collision

其中:

  • project-a 依赖 Guava 22.0:

    <dependencies>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>22.0</version>
        </dependency>
    </dependencies>
    
  • project-b 依赖更新的 Guava 29.0-jre:

    <dependencies>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>29.0-jre</version>
        </dependency>
    </dependencies>
    
  • project-collision 同时依赖 project-aproject-b

    <dependencies>
        <dependency>
            <groupId>com.baeldung</groupId>
            <artifactId>project-a</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.baeldung</groupId>
            <artifactId>project-b</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    

问题来了:project-collision 最终会使用哪个版本的 Guava?

2.2 验证使用的版本

我们可以在 project-collision 中写个测试,调用 Guava 29.0 才有的方法:

@Test
public void whenVersionCollisionDoesNotExist_thenShouldCompile() {
    assertThat(Futures.immediateVoidFuture(), notNullValue());
}

Futures.immediateVoidFuture() 是 Guava 29.0+ 新增的方法。

2.3 版本冲突导致编译失败

如果 Maven 选择了 project-a 传递进来的 Guava 22.0,那么上述测试将编译失败

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:testCompile (default-testCompile) on project project-collision: Compilation failure
[ERROR] /tutorials/maven-all/version-collision/project-collision/src/test/java/com/baeldung/version/collision/VersionCollisionUnitTest.java:[12,27] cannot find symbol
[ERROR]   symbol:   method immediateVoidFuture()
[ERROR]   location: class com.google.common.util.concurrent.Futures

❌ 这就是典型的版本冲突:你代码里用了新 API,但 classpath 里却是旧版本。

2.4 使用 maven-dependency-plugin 查看依赖树

要诊断问题,最直接的方式是查看完整的依赖树:

mvn dependency:tree -Dverbose

输出如下:

[INFO] com.baeldung:project-collision:jar:0.0.1-SNAPSHOT
[INFO] +- com.baeldung:project-a:jar:0.0.1-SNAPSHOT:compile
[INFO] |  \- com.google.guava:guava:jar:22.0:compile
[INFO] \- com.baeldung:project-b:jar:0.0.1-SNAPSHOT:compile
[INFO]    \- (com.google.guava:guava:jar:29.0-jre:compile - omitted for conflict with 22.0)

-Dverbose 参数会显示被忽略的冲突依赖。

可以看到,Guava 29.0-jre 因与 22.0 冲突而被省略(omitted),最终生效的是 project-a 引入的 22.0 版本。

📌 Maven 默认采用“第一声明优先”(nearest-wins)策略。当多个依赖在依赖树中层级相同时,先声明的依赖其传递依赖会被优先选用


3. 排除传递性依赖

解决版本冲突最直接的方式:在 pom.xml 中排除不需要的传递依赖

在我们的例子中,我们希望使用 project-b 带来的 Guava 29.0,因此可以排除 project-a 传递进来的旧版本:

<dependencies>
    <dependency>
        <groupId>com.baeldung</groupId>
        <artifactId>project-a</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <exclusions>
            <exclusion>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>com.baeldung</groupId>
        <artifactId>project-b</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

再次执行 mvn dependency:tree -Dverbose

[INFO] com.baeldung:project-collision:jar:0.0.1-SNAPSHOT
[INFO] \- com.baeldung:project-b:jar:0.0.1-SNAPSHOT:compile
[INFO]    \- com.google.guava:guava:jar:29.0-jre:compile

✅ 完美!旧版本被成功排除,编译通过。

⚠️ 缺点:需要在每个可能引入冲突的模块中手动排除,维护成本高。


4. 使用 dependencyManagement 统一版本

更优雅的解决方案是使用 dependencyManagement,它允许你在父 POM 中集中声明依赖版本,子模块无需指定版本号,由父模块统一控制。

version-collision 父模块的 pom.xml 中添加:

<dependencyManagement>
   <dependencies>
      <dependency>
         <groupId>com.google.guava</groupId>
         <artifactId>guava</artifactId>
         <version>29.0-jre</version>
      </dependency>
   </dependencies>
</dependencyManagement>

此时再看依赖树:

[INFO] com.baeldung:project-collision:jar:0.0.1-SNAPSHOT
[INFO] +- com.baeldung:project-a:jar:0.0.1-SNAPSHOT:compile
[INFO] |  \- com.google.guava:guava:jar:29.0-jre:compile (version managed from 22.0)
[INFO] \- com.baeldung:project-b:jar:0.0.1-SNAPSHOT:compile
[INFO]    \- (com.google.guava:guava:jar:29.0-jre:compile - version managed from 22.0; omitted for duplicate)

✅ 无论 project-a 声明的是哪个版本,最终都会被“管理”为 29.0-jre。

📌 优势

  • ✅ 版本集中管理,避免散落在各处
  • ✅ 子模块无需关心具体版本,只需声明依赖
  • ✅ 有效解决跨模块版本不一致问题

5. 使用 maven-enforcer-plugin 禁止传递依赖

更进一步,我们可以使用 maven-enforcer-plugin 插件,强制禁止使用传递性依赖,让所有依赖必须显式声明。

在父 POM 中添加插件配置:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-enforcer-plugin</artifactId>
    <version>3.0.0-M3</version>
    <executions>
        <execution>
            <id>enforce-banned-dependencies</id>
            <goals>
                <goal>enforce</goal>
            </goals>
            <configuration>
                <rules>
                    <banTransitiveDependencies/>
                </rules>
            </configuration>
        </execution>
    </executions>
</plugin>

启用后,如果 project-collision 想使用 Guava,就必须显式声明

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>29.0-jre</version>
</dependency>

否则构建会失败。

优点

  • 彻底杜绝“意外引入”旧版本的风险
  • 依赖关系清晰,一目了然

⚠️ 缺点

  • 增加了 pom.xml 的声明负担
  • 需要团队统一规范

6. 依赖收敛(Dependency Convergence)

maven-enforcer-plugin 还提供了一个强大的内置规则:dependencyConvergence,用于强制所有依赖路径上的版本必须一致

6.1 默认行为

pom.xml 中启用该规则:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-enforcer-plugin</artifactId>
            <version>3.3.0</version>
            <executions>
                <execution>
                    <id>enforce</id>
                    <configuration>
                        <rules>
                            <dependencyConvergence/>
                        </rules>
                    </configuration>
                    <goals>
                        <goal>enforce</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

此时若存在版本冲突(如 Guava 22.0 和 29.0),构建将直接失败:

[ERROR] Rule 0: org.apache.maven.enforcer.rules.dependency.DependencyConvergence failed with message:
[ERROR] Dependency convergence error for com.google.guava:guava:jar:22.0 paths to dependency are:
[ERROR] +-com.baeldung:project-collision:jar:0.0.1-SNAPSHOT
[ERROR]   +-com.baeldung:version-collision-project-a:jar:0.0.1-SNAPSHOT:compile
[ERROR]     +-com.google.guava:guava:jar:22.0:compile
[ERROR] and
[ERROR] +-com.baeldung:project-collision:jar:0.0.1-SNAPSHOT
[ERROR]   +-com.baeldung:version-collision-project-b:jar:0.0.1-SNAPSHOT:compile
[ERROR]     +-com.google.guava:guava:jar:29.0-jre:compile

✅ 这正是我们想要的——任何版本不一致都视为构建错误,防患于未然。

6.2 排除特定构件

如果确定某些构件的版本差异不会影响运行,可以将其排除:

<dependencyConvergence>
    <excludes>
        <exclude>com.google.guava:guava</exclude>
    </excludes>
</dependencyConvergence>

这样即使 Guava 版本不一致,构建也不会失败。

6.3 仅包含特定构件

也可以反向操作,只对特定构件启用收敛检查:

<dependencyConvergence>
    <includes>
        <include>com.google.guava:guava</include>
    </includes>
</dependencyConvergence>

这样只有 Guava 需要版本收敛,其他依赖不受影响。


7. 总结

解决 Maven 构件版本冲突,核心思路有三:

方案 适用场景 推荐指数
exclusion 排除依赖 临时修复、单点问题 ⭐⭐⭐
dependencyManagement 多模块项目,统一版本管理 ⭐⭐⭐⭐⭐
maven-enforcer-plugin + dependencyConvergence 高质量要求项目,杜绝隐患 ⭐⭐⭐⭐

📌 建议

  • 多模块项目务必使用 dependencyManagement 统一管理第三方依赖版本
  • 对稳定性要求高的项目,推荐启用 dependencyConvergence 规则
  • exclusion 作为兜底手段,避免过度使用

本文所有示例代码均可在 GitHub 获取:https://github.com/baeldung/tutorials/tree/master/maven-modules/version-collision


原始标题:How to Resolve a Version Collision of Artifacts in Maven | Baeldung