[Maven/Java/依赖冲突] Java 包冲突问题的系列解决方案
0 序
- 前置知识: Java 包冲突(依赖冲突)
- Java 包冲突(依赖冲突)是项目里非常常见但也很头疼的问题,尤其在微服务或历史项目里。
例如:针对
ClassNotFoundException: com.google.protobuf.GeneratedMessageV3这种具体的报错,结合mvn dependency:tree这类工具来排查,其实有一套非常标准且高效的流程。
本文从诊断 → 解决 → 预防三个层面系统梳理一下:
1 解决方法
1.0 先理解包冲突问题的报错本质
com.google.protobuf.GeneratedMessageV3这个类只在protobuf-java 3.x及以上版本存在,2.x 版本(如 2.5.0、2.6.1)里根本没有这个类。
所以报错原因只有两种:
- 项目里引入了
protobuf-java 2.x版本,覆盖了 3.x。- 或根本没有引入
protobuf-java依赖。
1.1 先定位:找到冲突源头
方法1. 命令行排查
以 commons-lang 为例:
- Maven 项目 | 用
dependency:tree精准过滤
mvn dependency:tree > dep.txt
# 方式一:直接过滤 protobuf 相关依赖
mvn dependency:tree -Dincludes=commons-lang
mvn dependency:tree -Dincludes=com.google.protobuf:protobuf-java
# 方式二:如果记不清 artifactId,用 groupId 模糊匹配
mvn dependency:tree -Dincludes=com.google.protobuf
mvn clean compile dependency:tree -Dverbose -Dincludes="com.google.protobuf"
# 方式三:加 verbose 参数,显示被 Maven 忽略/冲突的版本(强烈推荐)
mvn dependency:tree -Dverbose -Dincludes=com.google.protobuf
执行后你会看到类似这样的输出:
[INFO] com.yourcompany:your-service:jar:1.0.0
[INFO] +- com.aliyun:aliyun-sdk-oss:jar:3.15.0:compile
[INFO] | \- com.google.protobuf:protobuf-java:jar:3.19.6:compile
[INFO] +- org.apache.hadoop:hadoop-common:jar:2.8.5:compile
[INFO] | \- com.google.protobuf:protobuf-java:jar:2.5.0:compile (version managed from 2.5.0)
- 关键看三点:
| 关注点 | 说明 |
|---|---|
| 最终生效版本 | Maven 依赖调解后,实际打包进来的版本是哪个(上面例子里,如果 hadoop-common 路径更短,就会用 2.5.0) |
| 引入来源 | 是哪个上游依赖把旧版本带进来的(如 hadoop-common 带来了 2.5.0) |
| 冲突标记 | 加了 -Dverbose 后,被舍弃的版本会显示为 (omitted for conflict with 3.19.6) 或类似标记 |
- 快速定位"谁抢占了版本"
如果你看到最终生效的是
2.5.0,但你的代码或某个 SDK 需要 3.x,那么:
# 查看更详细的依赖路径和冲突信息
mvn dependency:tree -Dverbose | grep -A 5 -B 5 "protobuf-java"
你会看到类似:
[INFO] | \- com.google.protobuf:protobuf-java:jar:2.5.0:compile
[INFO] \- com.google.protobuf:protobuf-java:jar:3.21.12:compile (omitted for conflict with 2.5.0)
这说明 3.21.12 被舍弃了,因为 2.5.0 在依赖树中路径更短或声明更靠前。
- 补充:Gradle 项目
./gradlew dependencies
方法2. IDE 工具
- IntelliJ IDEA:安装 Maven Helper 插件,在
pom.xml的 Dependency Analyzer 标签页里直接看冲突 - 搜索
NoSuchMethodError、ClassNotFoundException、LinkageError等报错对应的类
方法3. 运行时定位
-
启动时加 JVM 参数:
-verbose:class,看类是从哪个 jar 加载的 -
用
jar tf xxx.jar | grep ClassName确认某个类是否存在于某个包中
1.2 核心解决方法 (必读)
方法1:排除传递依赖、从引入旧版本的依赖中排除(最常用)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
</exclusions>
</dependency>
方法2:基于 dependencyManagement 统一版本管理(推荐)
Maven dependencyManagement:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
</dependencyManagement>
或用 BOM(Bill of Materials):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.7.18</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
方法3:强制锁定为指定版本(force/import)
- (Gradle版)强制指定版本
configurations.all {
resolutionStrategy {
force 'com.google.guava:guava:32.1.3-jre'
}
}
Maven 也可以用
<dependencyManagement>配合<scope>import</scope>达到类似效果。
方法4:使用 Shade 插件重定位包(终极方案)
- 当同一个项目必须【同时使用】两个【不兼容版本】时,用 Maven Shade Plugin 把其中一个版本的包路径重命名:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<relocations>
<relocation>
<pattern>com.google.common</pattern>
<shadedPattern>shaded.com.google.common</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
方法5:升级/降级协调版本
很多时候冲突是因为 A 依赖 log4j 2.17,B 依赖 log4j 2.20,统一升级到兼容版本即可。查看各依赖的官方文档确认兼容矩阵。
方法 6:类加载隔离(重量级)
1)OSGi:模块化隔离,适合插件化架构
2)自定义 ClassLoader:在容器或框架层面隔离(如 Tomcat 的 WebAppClassLoader)
3)Spring Boot 的 Layered Jar:不同层用不同 ClassLoader
方法2 统一版本管理 vs 方法3 强制指定版本
相似之处
| 维度 | 说明 |
|---|---|
| 目标 | 都为了解决依赖版本冲突,让指定版本最终生效 |
| 结果 | 打包后 classpath 中只保留一个版本 |
| 侵入性 | 都不需要【改业务代码】 |
本质区别
| 维度 | 方法2:统一版本管理 (dependencyManagement / BOM) |
方法3:强制指定版本 (force / resolutionStrategy) |
|---|---|---|
| 所属工具 | Maven 原生 | Gradle 原生(Maven 无真正 force 机制) |
| 作用机制 | 声明式:为依赖提供默认版本号,参与 Maven 依赖调解 | 命令式:无视依赖调解规则,直接暴力覆盖所有其他版本 |
| 是否强制拉入 | ❌ 依赖树中没有则不会引入 | ✅ 即使没有任何依赖声明,也可能强制拉入 |
| 路径优先级 | 遵循"路径最近者优先",只是给了一个版本号 | 无视路径长短,强制胜出 |
| 子模块继承 | ✅ 子模块自动继承版本声明 | ❌ 需在每个 configurations.all 中声明 |
| 可预测性 | 高,符合 Maven 约定 | 低,可能隐藏潜在的兼容性问题 |
小结
-
dependencyManagement是"立规矩"(大家都按这个版本来,但你得自己引入依赖); -
force是"搞独裁"(不管你引没引、谁先引的,统统换成我指定的版本)。 -
对比图:
1.3 预防与长期管理
| 实践 | 说明 |
|---|---|
定期执行 mvn dependency:analyze |
找出未使用或声明但未使用的依赖 |
| 使用 BOM 统一管理版本 | Spring Boot / Spring Cloud 都提供了 BOM |
| 最小化依赖引入 | 优先用 exclusions 排除不需要的传递依赖 |
| 禁止直接引入多个版本的同一库 | CI 阶段加依赖冲突检测 |
| 建立团队的依赖白名单 | 统一技术栈版本 |
1.4 快速决策树
运行时报 NoSuchMethodError / ClassNotFoundException?
├─ 同一个类存在于多个 jar → 用 dependency:tree 确认
│ ├─ 版本不兼容 → 排除低版本 or 统一版本
│ └─ 必须共存 → Shade 重定位
└─ 类只存在于一个 jar 但加载顺序不对 → 调整依赖声明顺序 or ClassLoader 隔离
Z FAQ for Java 包冲突
Q: 一个典型的包冲突问题案例的解决过程?
问题描述
mvn clean compile dependency:tree -Dverbose -Dincludes="com.google.protobuf"
---
[INFO] --- dependency:3.11.0:tree (default-cli) @ xxx-metadata ---
[INFO] com.xxx:xxx-metadata:jar:2.6.0
[INFO] +- com.xxx:data-query-connector:jar:2.6.0:compile
[INFO] | \- org.apache.rocketmq:rocketmq-tools:jar:5.1.2:compile (version managed from 5.1.2)
[INFO] | \- org.apache.rocketmq:rocketmq-acl:jar:5.3.1:compile (version managed from 5.1.2)
[INFO] | \- com.google.protobuf:protobuf-java-util:jar:3.20.1:compile
[INFO] +- org.apache.orc:orc-core:jar:1.5.10:compile
[INFO] | \- (com.google.protobuf:protobuf-java:jar:2.5.0:compile - omitted for duplicate)
[INFO] +- com.google.protobuf:protobuf-java:jar:2.5.0:compile
[INFO] \- com.xxx:xxx-common-dw:jar:2.6.0:compile
[INFO] \- org.apache.hadoop:hadoop-client:jar:3.3.6:compile
[INFO] +- org.apache.hadoop:hadoop-yarn-api:jar:3.3.6:compile
[INFO] | \- (com.google.protobuf:protobuf-java:jar:2.5.0:compile - omitted for duplicate)
[INFO] \- org.apache.hadoop:hadoop-mapreduce-client-jobclient:jar:3.3.6:compile
[INFO] +- org.apache.hadoop:hadoop-mapreduce-client-common:jar:3.3.6:compile
[INFO] | \- (com.google.protobuf:protobuf-java:jar:2.5.0:compile - omitted for duplicate)
[INFO] \- (com.google.protobuf:protobuf-java:jar:2.5.0:compile - omitted for duplicate)
这种情况下,有哪些解决方案?
其次,假设 com.xxx:xxx-metadata:jar:2.6.0、com.xxx:data-query-connector:jar:2.6.0、com.xxx:xxx-common-dw:jar:2.6.0、com.xxx:xxx-common-dubbo:jar:2.6.0 确需使用多个不同版本的 com.google.protobuf:protobuf-java:jar (2.5.0、3.25.8),又该如何?
- 分析与解决过程:
从贴的
dependency:tree输出,可以直接把问题定位清楚,然后给出两套方案:当前场景的务实解法 和 多版本必须共存时的终极解法。
一、先解读 dependency:tree
关键信息提取:
| 依赖路径 | protobuf 相关 | 最终生效 |
|---|---|---|
| 直接依赖 | protobuf-java:2.5.0 |
✅ 生效版本 2.5.0 |
orc-core:1.5.10 |
需要 protobuf-java:2.5.0 |
匹配,没问题 |
hadoop-client:3.3.6 全家桶 |
需要 protobuf-java:2.5.0 |
匹配,没问题 |
rocketmq-acl:5.3.1 → protobuf-java-util:3.20.1 |
需要 protobuf-java:3.x |
❌ 被 2.5.0 覆盖 |
根因:rocketmq-acl 引入了 protobuf-java-util:3.20.1,它编译时依赖 protobuf-java:3.x(里面有 GeneratedMessageV3 类)。但你的项目直接声明了 protobuf-java:2.5.0,Maven 依赖调解后 3.x 被 2.5.0 覆盖,导致运行时 protobuf-java-util 找不到 3.x 的类。
二、如果是我——当前场景的解决方案(按推荐顺序)
方案 1:强制锁定 protobuf-java 到 3.x(最省事,先试)
在 xxx-metadata 的 pom.xml 中加入:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.25.8</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>3.25.8</version>
</dependency>
</dependencies>
</dependencyManagement>
然后 排除掉直接引入的 2.5.0(删除你 pom 里直接声明的 protobuf-java:2.5.0 依赖)。
⚠️ 风险点:orc-core:1.5.10 和 hadoop-client:3.3.6 是否兼容 3.x?
hadoop-client:3.3.6实际上已经支持 protobuf 3.x(Hadoop 3.x 内部就是 3.x),tree 里显示 2.5.0 可能是 pom 声明残留orc-core:1.5.10大概率不兼容 3.x(它生成的代码基于 proto2)
👉 验证方法:改完版本后,跑一下 orc 相关的单元测试,如果 ORC 读写正常,就直接用这个方案。
方案 2:排除 rocketmq 的 protobuf-java-util(如果 rocketmq 功能不依赖它)
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-acl</artifactId>
<version>5.3.1</version>
<exclusions>
<exclusion>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
</exclusion>
<exclusion>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
</exclusion>
</exclusions>
</dependency>
然后显式引入 protobuf-java:2.5.0 和 protobuf-java-util:2.5.0(如果有)。
⚠️ 风险:rocketmq-acl 5.3.1 的认证功能可能依赖 protobuf 序列化,排除后可能报其他错。需要测试 RocketMQ 的 ACL 功能。
方案 3:降级 RocketMQ 版本
RocketMQ 5.x 早期版本(如 5.0.x)可能依赖较低版本的 protobuf。可以试:
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-tools</artifactId>
<version>5.0.4</version>
</dependency>
三、假设必须多版本共存(2.5.0 和 3.25.8 都要用)
你说 xxx-metadata、data-query-connector、xxx-common-dw、xxx-common-dubbo 都需要不同版本,这就是必须共存的场景。只有一条路:Shade 重定位。
架构思路
xxx-metadata (主项目)
├── xxx-protobuf-v3-shaded (自定义 shaded jar,给 rocketmq/dubbo 用)
│ ├── protobuf-java 3.25.8 → 重定位为 shaded.protobuf
│ └── protobuf-java-util 3.25.8 → 重定位为 shaded.protobuf
├── orc-core → 用原始 protobuf-java 2.5.0
├── hadoop-client → 用原始 protobuf-java 2.5.0
└── xxx-common-dw → 用原始 protobuf-java 2.5.0
具体实施步骤
Step 1:创建 xxx-protobuf-v3-shaded 模块
<!-- xxx-protobuf-v3-shaded/pom.xml -->
<project>
<groupId>com.xxx</groupId>
<artifactId>xxx-protobuf-v3-shaded</artifactId>
<version>2.6.0</version>
<dependencies>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.25.8</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>3.25.8</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<relocations>
<relocation>
<pattern>com.google.protobuf</pattern>
<shadedPattern>com.xxx.shaded.protobuf</shadedPattern>
</relocation>
</relocations>
<!-- 不生成依赖缩减,保留所有类 -->
<minimizeJar>false</minimizeJar>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Step 2:主项目引入 shaded 版本,排除原始 3.x
<!-- xxx-metadata/pom.xml -->
<dependencies>
<!-- 引入 shaded 版本 -->
<dependency>
<groupId>com.xxx</groupId>
<artifactId>xxx-protobuf-v3-shaded</artifactId>
<version>2.6.0</version>
</dependency>
<!-- rocketmq 排除 protobuf -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-acl</artifactId>
<exclusions>
<exclusion>
<groupId>com.google.protobuf</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 锁定 2.5.0 给 orc/hadoop 用 -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>2.5.0</version>
</dependency>
</dependencies>
Step 3:代码层面
RocketMQ 相关的代码如果直接引用了 com.google.protobuf 的类,需要改为引用 com.xxx.shaded.protobuf。如果 RocketMQ 内部自己用(不暴露 protobuf 类型到 API),则无需改代码,因为 shade 已经把 rocketmq 依赖的 protobuf 类一起重定位了。
四、决策建议
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| orc-core 能兼容 3.x | dependencyManagement 锁定 3.25.8 |
零侵入,最干净 |
| orc-core 不兼容 3.x,rocketmq 功能简单 | 排除 rocketmq 的 protobuf-util | 改动小 |
| 多模块确实需要不同版本 | Shade 重定位 | 唯一能彻底隔离的方案 |
五、快速验证清单
- 改完后执行:
mvn clean package -U - 检查 fat jar 里:
jar tf target/xxx-metadata.jar | grep protobuf确认两个版本都在 - 启动时加
-verbose:class | grep protobuf确认类加载来源 - 跑通 ORC 读写 + RocketMQ 发送消息 两个场景的测试
Y 推荐文献
X 参考文献
本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!

浙公网安备 33010602011771号