老炮踩坑录 · 番外篇 · 翻车现场系列
基于「基础企业档案」真实项目代码,复盘一次"IDEA 里正常、打成 jar 就回到旧逻辑"的怪事
关键词:多模块 · mvn install · 本地仓库 · reactor · -am
👋 欢迎阅读
🏠个人主页: 知守观
📘我的专栏: 老炮踩坑录
💻当前内容:Maven 依赖解析踩坑
前言
上一篇讲了"基础企业档案" 项目换 IDEA 版本启动失败的事故。启动问题解决之后,项目能跑了,我顺手拿它改点代码,验证记忆里的一些逻辑。
然后踩了第二个坑。
common 模块里有个工具方法,我改了它的实现。改动很小,一行的事。在 IDEA 里 Run,行为符合预期,自测通过。
但是打包,丢到测试环境,结果跟没改一样。
同一份源码,IDEA 里是新逻辑,jar 包里是旧逻辑。
改动和现象
改动的是基础企业档案项目里的一个工具类:
typescript
public class AreaCodeUtils {
public static String normalize(String code) {
if (code == null) {
return "";
}
return code.trim();
}
}
这是改完的版本,改动前没有 trim(),入参什么样就原样返回。web 模块有个 Service 调用它处理行政区划编码。
在 IDEA 里跑:传带空格的编码,输出是 trim 过的,新逻辑生效。
打成 jar 跑:传同样的入参,空格原样带出来,还是旧逻辑。
排查:旧代码到底从哪进的包
第一个怀疑对象是测试环境------是不是部署脚本没换包。本地 java -jar 跑了一遍刚打出来的 jar,还是旧行为。环境因素可以排除,说明jar包本身就是旧的。
把 jar 解开看。Spring Boot 打出来的 fat jar,业务模块的依赖在 BOOT-INF/lib/ 下面,找到 base-archives-common-0.0.1-SNAPSHOT.jar,把里面的 AreaCodeUtils.class 拖进 IDEA 看反编译结果:
typescript
public static String normalize(String code) {
if (code == null) {
return "";
}
return code;
}
没有调用 trim方法。打进包里的确实是旧代码。
打包这个动作发生在本机,旧代码的来源只可能在本机。于是去看本地仓库:
swift
dir "D:\Maven\repertory\cn\xiaomayi\base-archives-common\0.0.1-SNAPSHOT"
输出:
csharp
目录: D:\Maven\repertory\cn\xiaomayi\base-archives-common\0.0.1-SNAPSHOT
Mode LastWriteTime Length Name
---- ----------------- ------ --------------------
-a---- 2026/10/5 14:23 15234 base-archives-common-0.0.1-SNAPSHOT.jar
-a---- 2026/10/5 14:23 2841 base-archives-common-0.0.1-SNAPSHOT.pom
dir 是 Get-ChildItem 的别名,默认输出里就有 LastWriteTime 这一列,即文件最后修改时间。jar 的时间停在上周。我改代码是今天。
本地仓库里的 common jar,是上周 install 的那一份。我今天的改动只存在于源码和 target/classes 里,从来没有进过本地仓库。
原因:reactor 里有没有它,决定 Maven 去哪拿
先解释下 reactor 是什么。
多模块工程执行 mvn 命令时,Maven 会先算一遍:这次构建有哪些模块参与、按什么依赖顺序构建。这个动态算出来的模块清单就叫 reactor------每次构建都可能不一样,根目录全量构建时所有模块都在,加 -pl 就只留指定的模块。
reactor 内外的解析方式完全不同。Maven 构建时解析依赖,有一个固定的顺序:
- 参与本次构建的模块(reactor 里的模块)优先,直接用各模块
target/classes的输出; - 不在 reactor 里的依赖,去本地仓库找;
- 本地仓库没有,才去远程仓库下载。
第二条是关键:哪怕是你自己项目的兄弟模块,只要没进 reactor,一样按外部依赖处理,去本地仓库拿 jar。
我当时用的构建命令:
csharp
mvn clean package -pl base-archives-web -DskipTests
-pl 指定只构建 web 模块。这次构建的 reactor 里只有 web 自己,common 不在场。web 对 common 的依赖按顺序落到第二步------从本地仓库拿,拿到的就是上周那份旧 jar,原样打进 fat jar。
reactor 里有谁不用猜,Maven 每次构建开头都会打印出来。我当时那条命令打出来的列表:
csharp
[INFO] Reactor Build Order:
[INFO]
[INFO] base-archives
[INFO] base-archives-api
[INFO] base-archives-web
只有两个模块,common 不在,不参与本次构建。改动的代码在 base-archives-common/target/classes 里编译得好好的,但没有任何一个环节把它送到 web 的打包路径上。
两种改法
csharp
# 方案一:改了 common/api,先 install 进本地仓库
mvn clean install -pl base-archives-common,base-archives-api -DskipTests
# 方案二:构建 web 时带上 -am,把它依赖的模块拉进同一次构建
mvn clean package -pl base-archives-web -am -DskipTests
两种改法的区别值得说清楚。
-am(--also-make)把 web 依赖的 common、api 一起拉进这次构建,common 进了 reactor,按解析顺序直接用 target/classes,新代码进包。但 -am 的产物只服务这一次构建,不会写进本地仓库。下次你再单独构建 web,本地仓库那份旧 jar 还在原地等着。
install 才能真正更新本地仓库。-am 管这一次打包,install 管本地仓库的长期状态。
-am管这一次打包,install 管本地仓库的长期状态。
后来翻项目团队内部的协作约定文档,发现根目录的 CLAUDE.md 里一直写着这条命令,注释是"修改 common/api 后需执行"。写下这行字的人踩过同样的坑,把做法记在了文档里。我没看,或者说看了没当回事。
IDEA 为什么是对的
整个事情里最让人想不通的是:IDEA 里明明跑出了新逻辑。
IDEA 对同一工程内的 Maven 模块,默认做工作区解析------web 对 common 的依赖,直接指向 base-archives-common/target/classes,跳过本地仓库。所以 IDEA 里 Run,加载的是刚编译的新代码。打包走的是 Maven 的解析逻辑,两套路径,两个结果。
这个行为有前提:模块得作为 Maven 模块正确导入同一个工程。如果 common 没导进工程,或者在设置里开了 Delegate IDE build/run to Maven,解析路径又会变。具体到哪些配置组合走哪条路,我没法穷举,跟 IDEA 版本也有一定的关系。
能确认的只有一件事:"IDEA 里是对的"这个结论,证明不了打包产物是对的。
"IDEA 里是对的",证明不了打包产物是对的。两套解析路径,两个结果。
怎么确认自己跑的是哪份代码
三个办法,成本从低到高。
1、看本地仓库 jar 的修改时间。早于你的代码改动时间,依赖它打出来的包就是旧的。
2、想知道某个类实际从哪加载,一行代码:
less
System.out.println(AreaCodeUtils.class.getProtectionDomain().getCodeSource().getLocation());
输出指向本地仓库的 jar,还是指向 target/classes,直接见分晓。排查"加载的到底是哪份"这类问题,这一行比翻半天配置快。
3、较真的时候,对比 class 哈希。jar 本质是 zip,改后缀就能解压:
vbnet
Get-FileHash base-archives-common\target\classes\cn\xiaomayi\achives\common\AreaCodeUtils.class
Get-FileHash 解压目录\cn\xiaomayi\achives\common\AreaCodeUtils.class
哈希不一致,就不是同一份代码,没什么好争的。
这个坑为什么难排查
全程不报错。应用正常启动,接口正常响应,唯独行为是旧版本的。没有异常、没有堆栈,日志里找不到任何线索。
IDEA 验证通过给了人确定感。"代码没问题" 这个结论一旦建立,怀疑方向就被推到部署、环境、缓存上,很少人回头怀疑打包时的依赖解析问题。
用旧 jar 对着新源码 debug 时,断点行号对不上,单步走着会跳,变量值和源码对不上。这个症状大多数 Java 工程师都见过,第一反应通常是 IDEA 出问题了,清缓存、重启,折腾一圈,问题原样保留。
自查清单

老炮点评
"改了代码"和"改的代码被运行",中间隔着编译、install、打包好几个环节。Maven 把每个环节都做得很顺,顺到 install 这一步经常被忘掉------反正 IDEA 里是对的。
部署文档里那些看着多余的命令,值得逐条把它当回事。因为写下它们的人多半交过学费,命令能留在文档里,说明坑确实踩过。
上一篇番外篇讲了feign-core换IDEA版本启动失败,这篇讲多模块install,两篇都是 "企业基础档案" 项目里的真实排查。
下期预告:《同事的 SNAPSHOT 更新了,我三天后才拉到》
同事的 common 模块 deploy 了新 SNAPSHOT,让我本地验证。我这边怎么构建都还是旧代码,
mvn clean清了一遍又一遍,没用。updatePolicy、maven-metadata.xml、
-U参数,还有一个很少人提的_remote.repositories文件。下期讲 SNAPSHOT 的更新机制,以及为什么-U有时候也不管用。
如果本文对你有帮助,欢迎:
👍 点赞 | ⭐ 收藏 | 👤 关注| 💬 留言
我是老炮,Java 老兵,仍在一线。关注「Java老炮踩坑录」,看真实案例,少踩坑。