JAR 全对仍 checksum mismatch:Forge Admin 把 Flyway 从 filesystem 改成 classpath

你刚打完 forge-admin-server.jar,本地 IDE 启动一切正常。把同一个产物丢到服务器,日志却刷:

text 复制代码
FlywayValidateException: Migration checksum mismatch for migration version ...

更膈应的是:你把 JAR 解开一看,里面的 db/migration/*.sql 和库里 forge_schema_history 对得上。按常理不该炸。Forge Admin 在 2026-10-10 的 admin-jar-flyway-deployment 一组提交里,把这件事钉死成配置契约------默认只信 classpath,不信启动目录残留。下面按真实源码与仓库执行记录写;生产侧数字来自仓库内部署执行日志,文中标明出处,不另编用户反馈。

和 2026-10-11 那篇「插件独立 Flyway / FeatureGate」不是同一题:那篇讲插件与主库 历史表不能合租 ;本篇讲 Admin 主库迁移的 扫描来源(JAR 内资源 vs 服务器旧目录)。两篇可以连着读,但查重上互不替代。

现象:三件套同时成立时,人最容易误判

独立部署踩坑时,现场常常是下面三件事 同时 成立(与 code-copilot/memory/pitfalls/db-flyway.md 条目及仓库执行记录一致;推断处标「按源码推演」):

  1. 解压或列举 JAR 内 db/migration,脚本集合、版本号与目标库历史对得上;
  2. 服务器 WorkingDirectory(或旁边挂载)还躺着一份更早拷贝的 db/migration;
  3. Flyway 报错里的 Resolved locally ,指的是它 实际扫到的那份脚本,不一定是你刚上传的 JAR 里那份。

人脑直觉是:「产物对了 = 校验对象就是产物」。Flyway 不这么想。它只认配置里的 locations。旧默认若写的是一串相对 filesystem: 路径,进程当前目录下的旧文件就会变成「本地解析结果」。于是你看到的矛盾变成了:JAR 正确,Validate 仍失败。

仓库踩坑记录(发现日期 2026-10-10)把标题写得很直白:独立 JAR 正确但 filesystem 仍读取服务器残留旧迁移 。解决方向不是先 repair,而是先核对 systemd 工作目录、启动脚本、EnvironmentFile、活动 profile,再核对 实际迁移来源。

仓库 openspec/.../admin-jar-flyway-deployment/execution-log.md 记了一次「只改 EnvironmentFile、不换 JAR」的验收:当时历史表 217 条均成功,脚本名/版本/checksum 与生产 JAR 全部一致,0 pending ;向生效的 env 增加 FORGE_FLYWAY_LOCATIONS=classpath:db/migration 并重启后,日志出现 Successfully validated 217 migrations 与 No migration necessary(版本 1.0.217)。这说明来源修对以后,不必改业务代码也能恢复启动;也说明「校验失败」未必等于「库坏了」或「脚本写错了」。

根因一:旧默认把相对 filesystem 写进了 application.yml

修复前(eee9906b4 父提交中的配置)大致是:

yaml 复制代码
spring:
  flyway:
    enabled: ${FORGE_FLYWAY_ENABLED:true}
    locations: ${FORGE_FLYWAY_LOCATIONS:filesystem:./db/migration,filesystem:../db/migration,filesystem:forge-server/db/migration}
    fail-on-missing-locations: false
    table: forge_schema_history

本地能绿,是因为开发机工作目录碰巧能命中源码树里的 db/migration------相对路径在「从仓库根/模块目录启动」时常常碰巧正确。生产 WorkingDirectory 若仍挂着旧拷贝,或运维习惯把 SQL 目录放在 JAR 旁「方便热修」,Flyway 就会扫到 磁盘上那份 。更糟的是 fail-on-missing-locations: false:某个相对路径不存在时可以静默跳过,排障时更难一眼看出「到底扫了哪几个目录」。

当前默认(forge-server/forge-admin-server/src/main/resources/application.yml):

yaml 复制代码
spring:
  flyway:
    enabled: ${FORGE_FLYWAY_ENABLED:true}
    # 迁移跟随应用版本打包,避免读取启动目录中残留的旧 SQL;外部路径必须显式覆盖。
    locations: ${FORGE_FLYWAY_LOCATIONS:classpath:db/migration}
    fail-on-missing-locations: true
    baseline-on-migrate: true
    baseline-version: 1.0.0
    table: forge_schema_history
    placeholder-replacement: false

关键差异就两句:默认只扫 classpath:db/migration ,以及 目录缺失直接失败 。外部路径只能通过环境变量显式覆盖,不再靠相对路径猜工作目录。placeholder-replacement: false 则避免 SQL 被 Spring 占位符机制改写------迁移文件必须是「落库时的字节」,不能在打包期被悄悄替换。

根因二:YAML 改了,SQL 没进 JAR,一样会挂

只改 application.yml 不够。Admin 模块 POM 必须把源码树里的迁移 原样 拷进产物,并且 关闭 filtering ,防止资源过滤把 ${...} 之类片段改坏:

xml 复制代码
<!-- forge-server/forge-admin-server/pom.xml -->
<resource>
  <directory>${project.basedir}/../db/migration</directory>
  <targetPath>db/migration</targetPath>
  <filtering>false</filtering>
  <includes><include>*.sql</include></includes>
</resource>

README 约定很清楚:SQL 仍维护在 forge-server/db/migration,不要复制第二套到 src/main/resources;Maven resources 阶段复制到 target/classes/db/migration。IDE 首次运行或新增 SQL 之后,要刷新 Maven 项目并构建资源------否则出现另一种「本地玄学」:源码目录里明明有 V1.0.xxx,运行时 classpath 却没有,校验或迁移行为与同事机器不一致。

按源码推演,Docker 侧也应停止「把主机旧 SQL 目录挂进容器覆盖产物」的习惯;仓库该变更集同步删掉了 Admin Docker 的主机 SQL 挂载(Flow 既有挂载未改)。独立 JAR 与容器若一边跟 classpath、一边仍挂旧目录,会把同一套版本策略撕成两半。

怎么改:契约测试钉死默认,运维清单挡住手滑

1)跟版本走的默认,而不是跟目录走的默认

合并进主线的关键提交(短哈希,可用 git show 复核):

Commit 说明
190e6e280 补充独立部署迁移配置与回归校验
e16610a72 默认使用打包迁移并统一部署配置
eee9906b4 合并迁移来源修复到主分支

文档入口:forge-server/scripts/deployment/admin-flyway.md。旧版兼容或要显式钉死来源时,向 实际生效 的 EnvironmentFile 增加一行即可(不要整文件覆盖掉数据源等既有变量):

dotenv 复制代码
FORGE_FLYWAY_LOCATIONS=classpath:db/migration

升级时务必检查:外置配置、JVM 参数、SPRING_FLYWAY_LOCATIONS、命令行 --spring.flyway.locations 是否仍指向旧 filesystem:。它们可能盖过你以为「YAML 已经改了」的默认值。文档明确要求:不要 classpath 与旧目录混扫 ;不要靠 repair、关校验、删历史表、改历史 checksum 掩盖来源错误。

2)用单测锁默认,比「文档写一句」抗回归

FlywayClasspathConfigurationTest(com.mdframe.forge.admin.migration)刻意不启动完整 Spring Boot、不连真实库,只绑定处理后的 application.yml,并隔离宿主环境变量:

java 复制代码
@Test
void defaultsToBundledSqlAndRequiresTheLocationToExist() throws IOException {
    FlywayProperties properties = bind(Map.of());
    assertEquals(List.of("classpath:db/migration"), properties.getLocations());
    assertTrue(properties.isFailOnMissingLocations());
    assertEquals("forge_schema_history", properties.getTable());
}

@Test
void explicitExternalLocationStillOverridesTheDefaultWithoutFallback() {
    // FORGE_FLYWAY_LOCATIONS=filesystem:/explicit-release/db/migration
    // 必须原样生效,且仍保持 fail-on-missing-locations=true
}

@Test
void everySourceSqlIsAvailableUnchangedWithoutExtraClasspathMigrations() {
    // ../db/migration 与 classpath*:db/migration/*.sql 文件名集合一致,逐字节相等
}

仓库执行记录写明该定向测试曾跑到 Tests run: 4, Failures: 0 。另有 Node 侧 admin-flyway.test.mjs 做配置契约断言。有人把默认改回 filesystem,或过滤复制导致 SQL 字节变化,测试会先炸------这比发版后靠人肉回忆「我们是不是改过 locations」可靠得多。

3)上线自查清单(建议收藏)

# 检查项 通过标准
1 目标是不是 Admin unit / WorkingDirectory / ExecStart / EnvironmentFile 指向 Admin,不是门户或其它模块
2 历史表名 forge_schema_history;不要误用门户的 forge_website_schema_history
3 JAR ↔ 历史 JAR 内已执行脚本与历史 checksum 一致;先列出 pending,有 pending 就当升级而非只读探活
4 备份 备份 EnvironmentFile、unit、启动脚本;记录当前 JAR 的 SHA-256
5 覆盖项 去掉旧 filesystem:;或显式 FORGE_FLYWAY_LOCATIONS=classpath:db/migration;禁止混扫
6 重启范围 只重启 Admin;日志出现 Validate 成功与应用启动完成;systemd active ≠ 应用已就绪
7 复查 历史无失败行;若仅改来源且无 pending,历史内容不应变化

回滚也要分清:恢复 EnvironmentFile / unit 是 配置回滚;若该版本已经执行过新迁移,必须按该版本的数据库回滚方案处理,不能只把配置改回去就当库也回去了。

4)顺带区分:同号双文件是另一类启动炸弹

2026-10-16 pitfalls 还记了分支合并后同时存在两个 V1.0.218__...,Flyway 无法启动。那是 版本号唯一性 问题,和本文的 locations 不同,但症状都是「服务起不来」。合并或上游同步后先查重:

bash 复制代码
ls forge-server/db/migration/V*.sql | sed 's/__.*//' | sort | uniq -d

已执行的那份 文件名与内容都不动;冲突的另一方改到当前最大版本 + 1。不要用删已执行脚本或改 checksum 的方式「合并」同号文件。

和你自己项目怎么对齐(按源码推演)

若你的 Spring Boot 服务也是「开发靠相对路径、生产靠 fat JAR」,发布前可在自己环境验证三步(以下为推演检查项,请用本机配置实际打印确认):

  1. 打印绑定后的 spring.flyway.locations 实际值,以及是否被环境变量/命令行覆盖;
  2. 看进程 WorkingDirectory 下是否还有一份 db/migration,内容是否与 JAR 内不一致;
  3. jar tf your-app.jar | grep 'db/migration' 与 forge_schema_history(或你的历史表)是否同版本集合。

Forge Admin 的产品选择是:默认让迁移跟随应用版本进 JAR,目录缺失失败,外部路径必须显式说出来。本地、独立 JAR、Docker 共用同一套资源约定,减少「我以为扫的是产物」的错觉。对开源后台来说,这比再写一篇 Flyway 入门更值钱------读者要的是能复用的排障顺序,不是工具百科。

小结

  • 故障反差:JAR 内 SQL 与库历史一致,仍可能 checksum mismatch------因为 Resolved locally 指向了服务器残留 filesystem。
  • 修复要点:默认 classpath:db/migration + fail-on-missing-locations: true + POM 原样打包 SQL + 契约单测。
  • 运维要点:清掉旧覆盖、只重启 Admin、用 7 步清单,而不是 repair 遮羞;有 pending 就按升级流程备份与审查 SQL。

如果这篇帮你少踩一个「本地绿、生产红」的坑,麻烦点个赞和收藏;评论区可以丢你卡过的 Flyway 报错原文(打码连接串),我按报错选题写下一篇。系列会继续只写 Forge Admin 真实 diff 里的坑,不写空泛发版公告。

仓库地址:gitee.com/ForgeLab/fo...

相关推荐
下页、再停留1 小时前
【C#桌面客户端系列学习-14】SQLite数据库之增删改查完整示例
数据库·sqlite
鬼手点金1 小时前
如何选择模型
java·前端·人工智能·git·深度学习
小马不起床1 小时前
Django 后端实战指南_16:动作分类、动作库、动作收藏查询接口
数据库·python·django
ycydynq1 小时前
django搜索功能
数据库·python·django
呆萌很1 小时前
DISTINCT 和 GROUP BY 核心区别
数据库·sql
古法老登瞎折腾1 小时前
大数据智能运维实战(三):实现第一个运维 MCP 工具——查询节点与组件状态
java·大数据·运维·人工智能·spring ai·mcp
小蒜学长1 小时前
基于数据挖掘的京东商城商品推荐系统设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·推荐系统·个性化推荐
小小AK1 小时前
金蝶其他入库到MES送检签收:库存同步策略实战教程
数据库·制造
LuminousCPP1 小时前
数据库学习笔记(二):MySQL 基础操作入门|在 CentOS Stream 10 上连接、建库与建表
数据库·经验分享·笔记·学习·mysql·centos