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-a
和project-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