NoSuchMethodError、ClassNotFoundException、NoClassDefFoundError——这三个报错背后,十有八九是依赖冲突:同一构件的多个版本进入了 classpath,运行时加载的版本和编译时预期的版本不一致。本文从定位到解决完整走一遍。

截图

现象:报错的真面目

典型场景:引入一个新的 SDK 后,启动或运行时报:

java.lang.NoSuchMethodError: com.google.common.collect.ImmutableMap.toImmutableMap(...)

编译能过(编译用的版本里有这个方法),运行时类路径上却加载了另一个旧版本(方法不存在)。这类「编译期 / 运行期版本不一致」就是依赖冲突的标准画像。

第一步:dependency:tree 找出冲突路径

在项目根目录执行:

mvn dependency:tree -Dincludes=com.google.guava:guava

-Dincludes 按 groupId:artifactId 过滤,只显示相关路径,输出类似:

[INFO] com.example:demo:jar:1.0.0
[INFO] - org.example:some-sdk:jar:2.1.0:compile
[INFO]    - com.google.guava:guava:jar:25.1:compile
[INFO] - org.springframework:spring-context:jar:6.1.0:compile
[INFO]    - com.google.guava:guava:jar:33.0:compile

同一个构件出现两条路径、两个版本——冲突坐实。Maven 只会把一个版本放进 classpath,选谁遵循两条仲裁规则:

  1. 最短路径优先:靠根越近的版本胜出
  2. 同深度先声明优先:pom 里靠前的依赖的版本胜出

所以 tree 里「被忽略」的那个版本(omitted for conflict)就是没被选中的。

第二步:三种解决方式,按优先级选择

方式一:统一版本管理(首选)

在 dependencyManagement 里锁定版本,全项目强制一致:

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

子模块和传递依赖都会服从这个版本。注意锁定后要验证兼容性——把 25 升到 33 通常向下兼容,反向降级就要仔细测试了。

方式二:exclusions 排除传递依赖

新引入的 SDK 自带了老版本,而你不需要它的版本:

<dependency>
    <groupId>org.example</groupId>
    <artifactId>some-sdk</artifactId>
    <version>2.1.0</version>
    <exclusions>
        <exclusion>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </exclusion>
    </exclusions>
</dependency>

排除后该 SDK 沿用项目里其他路径的版本。适合「SDK 只是传递带入、并不强依赖该版本」的场景。

方式三:挪声明顺序(应急技巧)

利用「同深度先声明优先」,把期望版本的依赖在 pom 中上移。能用但脆弱——将来调整依赖顺序就会复发,只作为快速验证手段,最终还是要落到方式一或二。

第三步:验证与防回归

解决后重新跑 tree 确认只剩一个版本:

mvn dependency:tree -Dincludes=com.google.guava:guava

更彻底的做法是加 enforcer 插件,把冲突挡在构建阶段:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-enforcer-plugin</artifactId>
    <executions>
        <execution>
            <id>ban-duplicate-versions</id>
            <goals><goal>enforce</goal></goals>
            <configuration>
                <rules>
                    <dependencyConvergence/>
                </rules>
            </configuration>
        </execution>
    </executions>
</plugin>

dependencyConvergence 规则会在依赖版本不收敛时直接构建失败,倒逼每个新依赖在引入时就处理干净。代价是构建变严格,建议从非阻塞的 requireUpperBoundDeps 开始逐步收紧。

排查清单(速记)

  1. 报错类与方法名 → 定位到具体构件
  2. mvn dependency:tree -Dincludes=groupId:artifactId 看冲突路径
  3. dependencyManagement 锁版本(首选)或 exclusions 排除
  4. 重新构建验证 + enforcer 防回归

常用依赖命令可对照速查表:Maven 命令速查。