编译能过、运行崩溃,是
NoSuchMethodError最迷惑人的地方。它不是说你的方法拼错了,而是:编译时用的库版本有这个方法,运行时加载的却是另一个没有该方法的版本。类存在,方法不存在,所以报的是NoSuchMethodError而不是NoClassDefFoundError。
Issue & Env(案发现场与环境)
- 现象:代码在 IDE 里编译通过,运行或部署到服务器后启动/调用时报错,逻辑本身看起来没有语法问题。
- 环境:Java 8+,Maven / Gradle 工程,常出现在直接或间接依赖发生版本冲突时。
StackTrace(堆栈日志)
JDK 11 之后,报错里带完整方法签名:
Exception in thread "main" java.lang.NoSuchMethodError:
'void com.google.gson.GsonBuilder.setStrictness(com.google.gson.Strictness)'
at com.example.json.JsonConfig.createGson(JsonConfig.java:24)
at com.example.utils.JsonUtils.newParser(JsonUtils.java:41)
at com.example.app.ParserFactory.build(ParserFactory.java:17)
at com.example.app.ReportApp.main(ReportApp.java:10)
也有旧式不含引号的写法:
java.lang.NoSuchMethodError: org.apache.commons.lang3.StringUtils.isBlank(Ljava/lang/CharSequence;)Z
at com.example.util.ValidateUtil.isBlank(ValidateUtil.java:18)
at com.example.service.UserService.validate(UserService.java:47)
at com.example.controller.UserController.register(UserController.java:33)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.lang.reflect.Method.invoke(Method.java:498)
关键在于:它告诉你的不是「类不存在」,而是「某个特定签名的方法在运行时加载的类里找不到」。
Reproduce(复现代码)
场景很典型:service-app 同时依赖 library-b 和 library-c,二者分别传递引入了 common-utils 的不同版本。
service-app
├─ library-b └─ common-utils:2.0
└─ library-c └─ common-utils:1.4
library-b 用到了 common-utils 2.0 才有的 getName() 方法:
// 编译时:classpath 里是 common-utils 2.0,有 getName()
String name = someUtils.getName();
但最终打包/运行时,common-utils:1.4 被 Maven 的「最近优先」或 Gradle 的「最高版本」策略选中,方法就没了,运行抛 NoSuchMethodError。
Root Cause(根因分析)
Java 字节码在记录方法调用时,存的是完整签名:类名 + 方法名 + 参数类型 + 返回类型。编译阶段 javac 对着某一个版本的库解析出这个签名,写进 .class。到了运行期,JVM 按同样的名字和签名去当前 classpath 里找方法,找得到就调,找不到就抛 NoSuchMethodError。
编译和运行看到的是两套 classpath,这是问题根源。Maven / Gradle 的依赖解析规则(Maven 取树中最近的那个,Gradle 默认取最高版本)会决定最终只有一个版本进入运行时,但它未必是编译时用的那个。很多人只在自己的 pom.xml 里声明了一个版本,却没注意到某个间接依赖偷偷把另一个版本拖了进来,导致运行时被顶替。
The Fix(解决方案)
先别改业务代码,先看依赖树,找出同一个构件出现了几个版本:
mvn dependency:tree -Dverbose
# Gradle 用:
# ./gradlew dependencies
# 或定位具体构件:
# ./gradlew dependencyInsight --dependency common-utils
找到冲突后,用 <dependencyManagement>(Maven)或 resolutionStrategy.force(Gradle)把版本锁成一致的,让编译和运行用同一个版本:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>common-utils</artifactId>
<version>2.0</version>
</dependency>
</dependencies>
</dependencyManagement>
对确定不要的错误传递依赖,直接在引入处排除:
<exclusions>
<exclusion>
<groupId>com.example</groupId>
<artifactId>common-utils</artifactId>
</exclusion>
</exclusions>
Spring Boot 项目优先通过 BOM 的 <properties> 覆盖版本,而不是到处写裸版本,避免多个相关依赖被拆散。改完依赖一定做一次干净构建:
mvn clean package
因为增量构建可能残留引用旧方法签名的字节码,clean 能避免这种「改了版本还报一样的错」的假象。
