java运行排错,新码旧jar

源码是新的,运行时却拿了旧 jar

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

http 复制代码
GET /api/v1/knowledge?page=0&size=10

后端一把甩回来:

text 复制代码
java.lang.NoSuchMethodError:
'com.mindflow.application.knowledge.KnowledgePage
com.mindflow.application.knowledge.KnowledgeCrudUseCase.list(int, int, java.lang.String)'

NoSuchMethodError 这个名字有误导性。它不是编译器报的"找不到方法"------源码完全能编译通过。它是 JVM 在运行时发现某个 class 文件的二进制签名和调用方期望的不一致,抛出的一个 Error 子类。Error 意味着这不是业务异常,是 JVM 层面的问题:当前 classpath 上加载到的类,和编译时用的类,不是同一个版本。

刚才改了什么

我们给知识库列表加了一个关键词搜索参数。原来分页接口是两参数:

java 复制代码
KnowledgePage list(int page, int size);

加完变成三个:

java 复制代码
KnowledgePage list(int page, int size, String query);

Controller 也同步改成了三参数调用。逻辑很简单,本地测试也过了:

powershell 复制代码
mvn --settings .mvn/settings.xml -pl mindflow-api -am test

这条命令的意思是:在 Maven 多模块项目里,-pl mindflow-api 指定只在 mindflow-api 这个模块跑测试,-am(also-make)让 Maven 先把它依赖的所有兄弟模块也编译一遍。所以测试时,mindflow-application 等依赖模块都是刚从源码编译出来的最新 class。

但启动的时候,走的不是这条路。


从最浅的地方开始排查

源码对不对?

先确认源码里接口签名和调用方确实都是三参数。打开:

复制代码
mindflow-application/.../KnowledgeCrudUseCase.java
mindflow-application/.../KnowledgeApplicationService.java
mindflow-api/.../KnowledgeController.java

三个文件都一致。说明问题不在编辑器里。

编译产物对不对?

源码是对的,但也许 IDE 没保存,也许增量编译出了问题。用 javap 直接看 target/classes 目录下的 class 文件------

javap 是 JDK 自带的 class 文件反编译工具,它能直接读出 class 里的方法签名,不需要源码。用法很简单:

powershell 复制代码
javap -classpath <class文件所在目录> <全限定类名>

跑一下:

powershell 复制代码
javap -classpath mindflow\backend\mindflow-application\target\classes `
  com.mindflow.application.knowledge.KnowledgeCrudUseCase

输出里确实有:

text 复制代码
public abstract KnowledgePage list(int, int, java.lang.String);

编译产物也是新的。两个最容易想到的方向都排除了。


端口上跑的是谁?

代码和编译都没问题,那就看看运行中的进程。也许上午调试留下的 Java 进程还在,占着端口,请求根本没打到新启动的服务上。

powershell 复制代码
Get-Process java
netstat -ano | Select-String '8080|18080'

第一行列出所有 Java 进程,第二行查 8080 和 18080 端口被哪些进程占用。

果然有旧进程。停掉:

powershell 复制代码
Stop-Process -Id <pid> -Force

这一步解决的是一个调试时很容易忽略的噪音源:你改了代码、重新启动了、觉得自己在看新版本,但浏览器连的还是残留进程。本地开发的"幻觉"大半来自这里。

进程清掉了,重启。错误依旧。

那就不是残留进程的问题。得往下挖。


真正致命的裂缝:两套 classpath

这个项目的后端结构是这样的:

text 复制代码
mindflow-domain
mindflow-application
mindflow-infrastructure
mindflow-ai
mindflow-api          ← 启动入口在这里

mindflow-api 是 Spring Boot 入口,依赖上面四个模块。我们的启动命令长这样:

powershell 复制代码
cd mindflow\backend
mvn --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 里声明依赖的 GAV(groupId、artifactId、version),然后去本地 Maven 仓库找对应的 jar。

而这个项目的 Maven settings 里配置了一个项目级的本地仓库:

xml 复制代码
<localRepository>${user.dir}/.m2repo</localRepository>

${user.dir} 是执行 Maven 命令时的当前目录,也就是 backend/。于是所有通过 Maven 坐标解析出来的 jar 都来自:

text 复制代码
mindflow/backend/.m2repo/

mindflow-api 启动时,会去:

text 复制代码
mindflow/backend/.m2repo/com/mindflow/mindflow-application/0.1.0-SNAPSHOT/mindflow-application-0.1.0-SNAPSHOT.jar

mindflow-application。问题来了------这个 jar 是哪来的?是上一次 mvn install 的时候写进去的。如果你只跑了 mvn testmvn compile,编译产物只到了各模块的 target/classes.m2repo 里的 SNAPSHOT jar 没有任何更新。

javap 确认:

powershell 复制代码
javap -classpath mindflow\backend\.m2repo\com\mindflow\mindflow-application\0.1.0-SNAPSHOT\mindflow-application-0.1.0-SNAPSHOT.jar `
  com.mindflow.application.knowledge.KnowledgeCrudUseCase

输出:

text 复制代码
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

本质上是构建路径和启动路径共用了一个本地仓库,但两个路径对"最新版本"的定义不一样。


修掉它

先清掉还在跑旧版本的进程:

powershell 复制代码
Stop-Process -Id <pid> -Force

然后从 backend 根目录跑一次 install,把当前源码编译打包,写入 .m2repo

powershell 复制代码
cd mindflow\backend
mvn --settings .mvn/settings.xml install -DskipTests '-Dspring-boot.repackage.skip=true'

install 是 Maven 生命周期里比 compiletest 更靠后的一个阶段:它会先编译、测试(这里跳过了),然后把 jar 复制到本地仓库。-Dspring-boot.repackage.skip=true 跳过 Spring Boot 的 repackage(打 fat jar),因为这里只需要普通 jar 让依赖解析通过,不需要可执行包。

javap 再次验证 .m2repo 里的 jar:

text 复制代码
public abstract KnowledgePage list(int, int, java.lang.String);

三参数了。启动:

powershell 复制代码
mvn --settings .mvn/settings.xml -f mindflow-api/pom.xml spring-boot:run

验证:

powershell 复制代码
Invoke-WebRequest -Uri 'http://localhost:8080/api/v1/knowledge?page=0&size=1&q=Smoke' -UseBasicParsing

返回 200。问题消了。


一条快速排查线

以后遇到 NoSuchMethodError,按这个顺序走几乎不会浪费时间:

  1. 看源码签名是否一致------排除手误。
  2. javaptarget/classes------排除编译问题。
  3. 查端口上的进程------排除"请求打到了旧服务"。
  4. 如果是 Maven 多模块、项目级本地仓库,javap.m2repo 里 SNAPSHOT jar 的签名。
  5. 确认 jar 是旧的,mvn install 更新,重启。

核心认知是:NoSuchMethodError 从来不怪源码。它告诉你的是,运行时 classpath 上同名的类,有两个不同的版本在共存。

给这个项目定个习惯

以后只要改了 mindflow-applicationmindflow-domainmindflow-infrastructure 这类被 mindflow-api 依赖的模块,启动前先跑:

powershell 复制代码
cd mindflow\backend
mvn --settings .mvn/settings.xml install -DskipTests '-Dspring-boot.repackage.skip=true'

如果只是跑测试,仍然可以:

powershell 复制代码
mvn --settings .mvn/settings.xml -pl mindflow-api -am test

两套路径的差异这次是 bug,下次就是常识。

相关推荐
wuqingshun3141591 小时前
JAVA中的注解原理是什么?
java
Python+992 小时前
Java 编程语言入门指南
java·开发语言
程序喵大人2 小时前
【C++进阶】STL容器与迭代器 - 05 map 和 set 为什么按键保持有序
开发语言·c++·容器·迭代器·stl
完美火龙篇 四月的友2 小时前
SkillOpt 架构拆解:把 Skill 文本当参数,用执行轨迹训练 Agent
开发语言·php
冻柠檬飞冰走茶3 小时前
PTA基础编程题目集 7-15 计算圆周率(C语言实现)
c语言·开发语言·数据结构·算法
前端炒粉3 小时前
简易实现ssr
开发语言·前端·javascript
库克克3 小时前
【C++】 unordered_map 与unordered_set
开发语言·c++
huahailing10243 小时前
Spring Boot 集成 XXL-Job 完整实现方案(支持动态CRUD)
java·spring boot·后端
薛定谔的猫-菜鸟程序员3 小时前
基于 Electron 的本地短视频解析与下载工具:架构设计与工程实践
java·electron·音视频