一个知识库列表接口挂了。前端照常请求

GET /api/v1/knowledge?page=0&size=10
后端一把甩回来
java.lang.NoSuchMethodError:'com.mindflow.application.knowledge.KnowledgePagecom.mindflow.application.knowledge.KnowledgeCrudUseCase.list(int, int, java.lang.String)'
NoSuchMethodError 这个名字有误导性。它不是编译器报的"找不到方法"——源码完全能编译通过。
它是 JVM 在运行时发现某个 class 文件的二进制签名和调用方期望的不一致抛出的一个 Error 子类。Error 意味着这不是业务异常是 JVM 层面的问题当前 classpath 上加载到的类和编译时用的类不是同一个版本。
我们给知识库列表加了一个关键词搜索参数。原来分页接口是两参数
KnowledgePage list(int page, int size);
加完变成三个
KnowledgePage list(int page, int size, String query);
Controller 也同步改成了三参数调用。逻辑很简单本地测试也过了
mvn --settings .mvn/settings.xml -pl mindflow-api -am test
这条命令的意思是在 Maven 多模块项目里-pl mindflow-api 指定只在 mindflow-api 这个模块跑测试-amalso-make让 Maven 先把它依赖的所有兄弟模块也编译一遍。所以测试时mindflow-application 等依赖模块都是刚从源码编译出来的最新 class。
但启动的时候走的不是这条路。
先确认源码里接口签名和调用方确实都是三参数。
打开
mindflow-application/.../KnowledgeCrudUseCase.javamindflow-application/.../KnowledgeApplicationService.javamindflow-api/.../KnowledgeController.java
三个文件都一致。说明问题不在编辑器里。
源码是对的但也许 IDE 没保存也许增量编译出了问题。用 javap 直接看 target/classes 目录下的 class 文件——
javap 是 JDK 自带的 class 文件反编译工具它能直接读出 class 里的方法签名不需要源码。用法很简单
javap -classpath <class文件所在目录> <全限定类名>
跑一下
javap -classpath mindflowbackendmindflow-applicationtargetclasses ` com.mindflow.application.knowledge.KnowledgeCrudUseCase
输出里确实有
public abstract KnowledgePage list(int, int, java.lang.String);
编译产物也是新的。两个最容易想到的方向都排除了。
代码和编译都没问题那就看看运行中的进程。也许上午调试留下的 Java 进程还在占着端口请求根本没打到新启动的服务上。
Get-Process javanetstat -ano | Select-String '8080|18080'
第一行列出所有 Java 进程第二行查 8080 和 18080 端口被哪些进程占用。
果然有旧进程。停掉
Stop-Process -Id <pid> -Force
这一步解决的是一个调试时很容易忽略的噪音源你改了代码、重新启动了、觉得自己在看新版本但浏览器连的还是残留进程。本地开发的"幻觉"大半来自这里。
进程清掉了重启。错误依旧。
那就不是残留进程的问题。得往下挖。
这个项目的后端结构是这样的
mindflow-domainmindflow-applicationmindflow-infrastructuremindflow-aimindflow-api ← 启动入口在这里
mindflow-api 是 Spring Boot 入口依赖上面四个模块。我们的启动命令长这样
cd mindflowbackendmvn --settings .mvn/settings.xml -f mindflow-api/pom.xml spring-boot:run
-f mindflow-api/pom.xml 的意思是只把这个子模块当"单模块项目"来启动不去碰父 pom。当时这么设计是为了避开父 pom 找不到 main class 的配置问题。
但 spring-boot:run 作为一个 Maven goal它解析依赖的方式和 mvn test 不一样——它不会自动编译兄弟模块。它只看 mindflow-api 的 pom 里声明依赖的 GAVgroupId、artifactId、version然后去本地 Maven 仓库找对应的 jar。
而这个项目的 Maven settings 里配置了一个项目级的本地仓库
<localRepository>${user.dir}/.m2repo</localRepository>${user.dir} 是执行 Maven 命令时的当前目录也就是 backend/。于是所有通过 Maven 坐标解析出来的 jar 都来自
mindflow/backend/.m2repo/
mindflow-api 启动时会去
mindflow/backend/.m2repo/com/mindflow/mindflow-application/0.1.0-SNAPSHOT/mindflow-application-0.1.0-SNAPSHOT.jar
取 mindflow-application。问题来了——这个 jar 是哪来的是上一次 mvn install 的时候写进去的。如果你只跑了 mvn test 或 mvn compile编译产物只到了各模块的 target/classes.m2repo 里的 SNAPSHOT jar 没有任何更新。
用 javap 确认
javap -classpath mindflowbackend.m2repocommindflowmindflow-application.1.0-SNAPSHOTmindflow-application-0.1.0-SNAPSHOT.jar ` com.mindflow.application.knowledge.KnowledgeCrudUseCase
输出
public abstract KnowledgePage list(int, int);
只有两参数。而 target/classes 和源码里是三参数。
现在两条加载路径都清楚了
| 场景 | mindflow-application 从哪来 | 版本 |
|---|---|---|
| 跑测试 mvn -pl mindflow-api -am test | reactor 编译产出的 target/classes | 最新 |
| 启动服务 mvn -f mindflow-api/pom.xml spring-boot:run | .m2repo 里的 SNAPSHOT jar | 上一次 install 的 |
启动时KnowledgeController在 mindflow-api 自身随启动模块一起编译是三参数版本KnowledgeCrudUseCase在 mindflow-application从 .m2repo 加载却是两参数旧版本。
JVM 的类加载没有"重新检查"这一步。Controller 调用 list(page, size, query) 时它字节码里写死了方法签名。到实际调用点JVM 去 KnowledgeCrudUseCase 的 class 里找这个签名找不到抛出 NoSuchMethodError。
本质上是构建路径和启动路径共用了一个本地仓库但两个路径对"最新版本"的定义不一样。
先清掉还在跑旧版本的进程
Stop-Process -Id <pid> -Force
然后从 backend 根目录跑一次 install把当前源码编译打包写入 .m2repo
cd mindflowbackendmvn --settings .mvn/settings.xml install -DskipTests '-Dspring-boot.repackage.skip=true'
install 是 Maven 生命周期里比 compile 和 test 更靠后的一个阶段它会先编译、测试这里跳过了然后把 jar 复制到本地仓库。-Dspring-boot.repackage.skip=true 跳过 Spring Boot 的 repackage打 fat jar因为这里只需要普通 jar 让依赖解析通过不需要可执行包。
用 javap 再次验证 .m2repo 里的 jar
public abstract KnowledgePage list(int, int, java.lang.String);
三参数了。启动
mvn --settings .mvn/settings.xml -f mindflow-api/pom.xml spring-boot:run
验证
Invoke-WebRequest -Uri 'http://localhost:8080/api/v1/knowledge?page=0&size=1&q=Smoke' -UseBasicParsing
返回 200。问题消了。
以后遇到 NoSuchMethodError按这个顺序走几乎不会浪费时间
javap 看 target/classes——排除编译问题。javap 看 .m2repo 里 SNAPSHOT jar 的签名。mvn install 更新重启。核心认知是NoSuchMethodError 从来不怪源码。它告诉你的是运行时 classpath 上同名的类有两个不同的版本在共存。
以后只要改了 mindflow-application、mindflow-domain、mindflow-infrastructure 这类被 mindflow-api 依赖的模块启动前先跑
cd mindflowbackendmvn --settings .mvn/settings.xml install -DskipTests '-Dspring-boot.repackage.skip=true'
如果只是跑测试仍然可以
mvn --settings .mvn/settings.xml -pl mindflow-api -am test
两套路径的差异这次是 bug下次就是常识。