起因
某天我在 bgin 分支提了一个改动,触发 Jenkins 构建后发现失败,报错信息是:
ERROR\] /opt/jenkins/data/workspace/.../KaspaCoinbaseLedgerImportService.java:\[353,53\] cannot find symbol \[ERROR\] symbol: variable SYSTEM_TYPE_KASPA_WALLET \[ERROR\] location: class com.intax.core.constants.KaspaImportServiceConstants 奇怪的是: * 这个常量明明已经在 intax-core 的 bgin 分支上定义了 * 本地编译完全没问题 * 其他环境(metaalpha 等)构建正常,只有 bgin 失败 * 而且失败发生在 intax-data 模块,跟我这次改的 intax-web-tob 一点关系都没有 排查了一通,挖出了一个挺反直觉的 bug,记录下来。 项目背景 这是个多仓的 Maven 工程: * intax-platform(主项目,一个 Maven 父 pom) * intax-core(基础工具库,被其他项目依赖) * intax-data(数据模块,依赖 intax-core) * intax-web-tob(Web 模块,依赖 intax-core) 四个仓独立 git 管理,各自维护 bgin、metaalpha 等多条客户分支。Jenkins 通过参数化构建,根据 BRANCH 和 BUILDENVIRONMENT 参数拉对应分支、用对应 maven profile 打包。 最近有人在 bgin 分支上做了 kaspa-wallet 改造: * intax-core 加了个常量 SYSTEM_TYPE_KASPA_WALLET = "Kaspa Wallet" * intax-data 引用了这个常量 代码本身没毛病,问题出在 Jenkins 编译流程上。 Pipeline 流程梳理 Step 1 --- Checkout Parent Project 拉 intax-platform 主仓代码 Step 2 --- Checkout Submodules 拉 intax-core / intax-data / intax-web-tob 三个子仓代码 Step 3 --- Build Parent Project 在主仓根目录跑 mvn clean install,reactor 模式一次性编译所有模块,jar 装进本地 \~/.m2 Step 4 --- Build Data Module (并行) cp -r 复制源码到 -bgin 后缀目录,单独再编 intax-data 和 intax-web-tob,产出最终 jar Step 5 --- Archive JARs Step 6 --- Deploy Step 7 --- Cleanup 前两步只是 git clone / git pull / git reset --hard,纯拉代码不编译。 第三步才是第一次编译。这一步在主项目根目录跑 mvn clean install,Maven reactor 模式把所有模块(包括 intax-core / intax-data / intax-web-tob)一次性全编了,jar 装进 Jenkins 本地的 \~/.m2/repository/com/ruoyi/ 缓存目录。这一步是成功的,新版 intax-core(带 SYSTEM_TYPE_KASPA_WALLET)进了本地缓存。 第四步单独再编 intax-data 和 intax-web-tob,就在这里出问题。 根因:一个反直觉的矛盾 第四步的 mvn 命令里带了 -U 参数: mvn clean package -U -P${BUILDENVIRONMENT} -DskipTests -U 的语义是"强制去远程仓库刷新 SNAPSHOT 依赖"。 听起来很合理 ------ "我要拿最新的"。但在这套 Pipeline 里,这个参数变成了灾难,原因是一个非常隐蔽的矛盾: ┌─────────────────────────────────┬─────────────────────────────────────────────────────┬───────────────────────────────────────────────────┐ │ 哪里的 intax-core │ 当前状态 │ 谁写进去的 │ ├─────────────────────────────────┼─────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤ │ Jenkins 服务器本地 \~/.m2 │ 第三步刚装进来的新版(带 SYSTEM_TYPE_KASPA_WALLET) │ 这次 Pipeline 自己 │ ├─────────────────────────────────┼─────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤ │ 远程仓库(内网 Nexus / 公共镜像) │ 早就 deploy 上去的旧版(无 SYSTEM_TYPE_KASPA_WALLET) │ 历史上某次构建,可能几周前从别的分支 deploy 上去的 │ └─────────────────────────────────┴─────────────────────────────────────────────────────┴───────────────────────────────────────────────────┘ 注意:mvn install 只装本地,不会往远程推。要往远程推得用 mvn deploy,这个 Pipeline 里没用。 所以每次构建,实际发生的事是这样的: 第三步:在本地默默装一份新版 intax-core ↓ 第四步:启动 mvn -U ↓ "我去远程问问有没有更新的?" ↓ 远程递回那本祖传旧版 intax-core(还是几周前的) ↓ maven 把远程旧版下载下来,覆盖了本地刚装的新版 ↓ intax-data 编译时取依赖 → 从本地缓存拿到那个被覆盖的旧版 ↓ 找不到 SYSTEM_TYPE_KASPA_WALLET → cannot find symbol → 失败 生产者(Step 3)只写本地,消费者(Step 4)又跑去读远程,两边目标完全不一致,导致新版每次都被旧版覆盖。 为什么别的环境没事,只有 bgin 出问题 * metaalpha 等其他分支:这些分支根本没有 kaspa-wallet 的改造,intax-core 不带这个常量,intax-data 也不引用。所以即使本地新版被远程"等价的旧版"覆盖,实际内容一样,不影响编译。 * bgin 分支:只有 bgin 新增了这个常量、新增了对它的使用。intax-data 必须拿到 bgin 版的 intax-core 才能编过,但 maven 反复给它一份旧的 → 永远失败。 也就是说,这个 bug 是潜伏很久了的,bgin 一旦引入"远程没有的新依赖",就立刻暴露。 为什么我的 commit 跟这事一点关系没有 我这次改的是 intax-web-tob 的报表代码,完全没碰 kaspa 相关。Jenkins 日志里 intax-web-tob 那个 stage 是 BUILD SUCCESS,只有 intax-data 那个 stage 报 cannot find symbol。 也就是说,就算我把我的 commit 完全 revert 掉,Jenkins 还是会因为这个 bug 失败。这是 kaspa-wallet 那批 commit + Jenkins Pipeline -U 参数的组合 bug,跟我的提交无关。 修复 两步: 1. 清掉本地缓存里被污染的旧 jar(已被覆盖的现状要清掉,否则下次还是用这个旧的): rm -rf /root/.m2/repository/com/ruoyi/intax-core rm -rf /root/.m2/repository/com/ruoyi/intax-data rm -rf /root/.m2/repository/com/ruoyi/intax-web-tob 2. 改 Jenkinsfile,去掉 3 处 -U(根治,防止下次再污染): stage('Build Parent Project') { dir(env.PARENT_DIR) { * sh 'mvn clean install -U -DskipTests' * sh 'mvn clean install -DskipTests' } } stage('Build Data') { dir(buildDir) { * sh "mvn clean package -U -P${params.BUILDENVIRONMENT} -DskipTests" * sh "mvn clean package -P${params.BUILDENVIRONMENT} -DskipTests" } } stage('Build Web-TOB') { dir(buildDir) { * sh "mvn clean package -U -P${params.BUILDENVIRONMENT} -DskipTests" * sh "mvn clean package -P${params.BUILDENVIRONMENT} -DskipTests" } } 去掉 -U 之后,maven 只用本地缓存里 Step 3 刚装的新版 jar,不会再去远程问。 复盘:几个反直觉的点 1. mvn install 的命名容易误导 英文 install 听起来像"装到正式位置去",实际上只是把 jar 拷贝到本地 \~/.m2/ 缓存。要往远程仓库推得用 mvn deploy,完全不同的命令。 2. mvn -U 不一定让你拿到"更新"的版本 -U 的逻辑是"去远程检查是否有更新的 SNAPSHOT"。它根据 maven-metadata.xml 的 lastUpdated 时间戳决定要不要下载。如果远程那份 jar 是几周前 deploy 的,本地是几分钟前 install 的,理论上本地更新,但 Maven 的实际行为受 timestamp 解析逻辑和 SNAPSHOT 版本号(5.2.0-20251112.143022-7 这种)影响,边缘情况下会出现下载远程旧版覆盖本地新版。 3. 这种 bug 表现得像"代码出问题" 报错 cannot find symbol 长得像源码缺失。但源码是好的(git reset --hard origin/bgin 之后工作区文件验证过)。问题在 Maven 依赖解析层,源码层面看不出来。要定位必须看 Jenkins 日志里 Downloading from ... 这些行,才能发现远程在搞鬼。 4. 多客户分支 + 共享 Maven coords 的天坑 com.ruoyi:intax-core:5.2.0-SNAPSHOT 这个 coords 所有分支共用。bgin 装一份、metaalpha 装一份、main 装一份,远程仓库里永远只有最后 deploy 的那一份,谁的版本谁就赢。如果远程被 main 分支占了,bgin 永远拿不到自己的新代码。 要根治这个,要么: * 不同分支用不同的版本号(比如 5.2.0-bgin-SNAPSHOT) * 或者不要在 CI 流程里依赖远程 SNAPSHOT,Reactor 内部全编(也就是把 Step 4 去掉,Step 3 reactor 一次编完直接产 jar) * 或者像本次的修复一样,只用本地缓存,关掉 -U 总结 ┌──────────┬────────────────────────────────────────────────────────────────────────────────────────────────┐ │ 维度 │ 内容 │ ├──────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤ │ 表象 │ bgin 分支 Jenkins 构建失败 cannot find symbol SYSTEM_TYPE_KASPA_WALLET │ ├──────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤ │ 真因 │ Jenkinsfile 第 4 步 mvn -U 主动去远程仓库刷新 intax-core,远程的旧版覆盖了第 3 步本地刚装的新版 │ ├──────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤ │ 本质矛盾 │ Step 3 只写本地缓存(install),Step 4 又去读远程(-U),生产者和消费者目标不一致 │ ├──────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤ │ 修复 │ 1) 清 \~/.m2 里被污染的旧 jar;2) Jenkinsfile 去掉 -U(3 处) │ ├──────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤ │ 教训 │ mvn install 只装本地 ≠ 推远程;-U 不一定让你拿到真正最新的;多分支共用 Maven coords 是个长期隐患 │ └──────────┴────────────────────────────────────────────────────────────────────────────────────────────────┘ 整件事下来最深的感悟是:CI 排错时,代码层面看不出问题不代表代码有问题,有时候 Maven / Pipeline 这层的"自作聪明"才是真凶。