多模块工程没 install,跑起来的代码是上周的:Maven reactor 依赖解析踩坑

老炮踩坑录 · 番外篇 · 翻车现场系列

基于「基础企业档案」真实项目代码,复盘一次"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 构建时解析依赖,有一个固定的顺序:

  1. 参与本次构建的模块(reactor 里的模块)优先,直接用各模块 target/classes 的输出;
  2. 不在 reactor 里的依赖,去本地仓库找;
  3. 本地仓库没有,才去远程仓库下载。

第二条是关键:哪怕是你自己项目的兄弟模块,只要没进 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老炮踩坑录」,看真实案例,少踩坑。

相关推荐
huaweichenai1 小时前
spring boot实现base64上传
java·spring boot·后端
萧瑟余晖1 小时前
Spring Web综合实战与性能调优详解
java·spring
点纭1 小时前
spring ai1.0实战:ChatClient(2)
java·后端·spring
小马过河R1 小时前
EdgeOne深度解析:从原理、适用场景到COS接入的技术逻辑
java·开发语言·前端·spring·cos
java水泥工1 小时前
什么是前端
java·vue·html5
Rain的Java大神之路2 小时前
Java 工程师做 AI,核心机会在「落地」而非「训练」
java·后端·ai编程
薛之谦_2 小时前
JAVA 多线程编程:从基础原理到实战应用
java·开发语言
LMT_Lanmantech2 小时前
Creo许可降本增效:不增购,也能让现有许可发挥更大价值
java·大数据·开发语言
Shaoshing2 小时前
Serializable
java