这篇写一个 Java 项目上线后很容易把人拖进深夜的问题:本地编译没问题,测试环境一启动就报 NoSuchMethodError,或者某个自动配置突然 ClassNotFoundException。这类问题别先怀疑 JVM,先把 Maven 依赖树打出来。
本文适用于 Java 17/21、Spring Boot 3.x、Maven 多模块项目。资料只用来核对事实:Maven 依赖冲突会按路径近者优先等规则仲裁,Spring Boot 通过依赖管理/BOM 帮你对齐大量版本。正文按生产排查复盘写,不搬官方文档。

业务场景:订单服务突然启动失败
一个订单服务升级了内部 legacy-sdk,本地单测通过,打包也成功。发布到预发后,服务启动到一半报 java.lang.NoSuchMethodError,指向 Jackson 的某个方法。业务同学第一反应是“代码没动这里”,但依赖冲突往往就是这样:你没改调用点,却改了运行时类路径。
Java 应用最终跑起来看的不是某个 pom.xml 里你以为的版本,而是 Maven 解析后的完整 classpath。只要一个传递依赖把旧版本带进来,就可能让编译期和运行期出现差异。
问题复现:同名类,不同方法签名
最常见的复现方式是:A 依赖需要新版库,B 依赖传递带入旧版库。编译时 IDE 可能拿到了新版,打包或运行时却被旧版覆盖。于是类存在,方法不存在,就变成 NoSuchMethodError;类整个不在,则变成 ClassNotFoundException 或 NoClassDefFoundError。
这类问题不要靠猜。先确认运行包里到底带了哪个 jar,再看 Maven 是从哪条路径把它解析进来的。

踩坑原因:只看直接依赖,不看传递依赖
Maven 项目里,直接依赖只是表面。真正麻烦的是传递依赖:你引入一个 SDK,它再引入 HTTP 客户端、JSON 库、日志组件、字节码工具。版本一多,就会触发 Maven 的依赖仲裁。
路径更近的版本可能赢,dependencyManagement 里声明的版本可能统一管理,父 POM 和 BOM 又会影响结果。你不理解这些规则,就很容易在一次“小升级”里把运行时类路径改乱。
代码案例:用 BOM 对齐,用 exclusion 切断污染源
下面这张图展示了我最常见的 review 建议:不要让每个模块各写各的版本;Spring Boot 项目优先使用 Boot 的依赖管理,再对明确冲突的传递依赖做排除。

com.foo legacy-sdk 1.8.0 com.fasterxml.jackson.core jackson-databind
排除不是越多越好。每一次 exclusion 都要写清楚原因:哪个传递依赖污染了版本,最终由哪个 BOM 或 dependencyManagement 接管。否则半年后没人敢升级。
诊断步骤:我会这样查
第一步,保留完整异常栈。 重点看缺的是类还是方法,包名属于哪个 jar。NoSuchMethodError 往往说明类存在但版本不对。
第二步,确认运行包。 Spring Boot fat jar 可以解开看 BOOT-INF/lib;普通部署可以看容器目录。不要只看 IDE。
第三步,打印依赖树。 使用 mvn dependency:tree -Dincludes=groupId:artifactId 找到冲突 jar 从哪几条路径进入项目。
第四步,看 effective POM。 多模块项目尤其要看父 POM、BOM、profile、dependencyManagement 最终合成后的结果。
第五步,做最小修复。 能用 Boot BOM 对齐的先对齐;必须排除传递依赖时,只排除明确污染源;不要全局乱钉版本。
第六步,把检查进 CI。 至少对核心模块跑依赖树检查、重复类检查、启动冒烟测试。依赖冲突靠人工记忆守不住。
上线检查:依赖升级也要灰度
- 升级前保存依赖树快照,升级后做 diff。
- 确认 Spring Boot、Spring Cloud、核心中间件客户端版本矩阵兼容。
- 确认 fat jar 中最终只出现期望版本的关键库。
- 启动冒烟覆盖 JSON 序列化、HTTP 客户端、日志、数据库访问等基础路径。
- 灰度阶段关注启动失败、类加载异常、序列化异常和接口 5xx。
我的经验总结
Maven 依赖冲突最怕“我感觉这个版本应该没问题”。Java 生产问题里,感觉通常不如一棵依赖树有用。先拿证据,再动 POM。
我更喜欢让版本来源集中:Spring Boot 项目就尊重 Boot 的依赖管理,公司内部 SDK 也要提供清晰 BOM。真正成熟的 Java 工程,不是永远不冲突,而是冲突发生时能在十分钟内知道谁带进来的、谁覆盖了谁、怎么安全修掉。