在 4 月的 HAOVP 接口验收中,我遇到过一种很容易误导排查的问题:本地源码已经包含修复,但运行中的接口行为仍像旧代码。继续检查业务逻辑时,怎么都解释不通。
最后确认,部分服务容器运行的还是旧 JAR,与本地源码不一致。重新构建并更新运行产物后,相关服务才与源码对齐。
这类问题提醒我,部署中的“版本”不是一个点,而是一条链:
源码版本
-> 编译生成的 JAR
-> 镜像中的 JAR
-> 运行容器实际加载的 JAR
-> 对外表现出来的接口行为任何一层没有更新,都会出现“代码明明改了,服务为什么没变化”。
先判断是不是版本问题
遇到以下现象时,我会先怀疑运行版本,而不是立即继续改业务代码:
- 本地源码中明确存在修复,运行结果却完全没有变化;
- 日志格式或错误信息仍是旧版本内容;
- 某些服务行为已更新,另一些服务仍保持旧行为;
- 重启容器后问题不变;
- 接口文档、源码和运行响应互相矛盾;
- 同一个问题在 IDE 运行时消失,在容器环境中仍然存在。
这些现象不能单独证明 JAR 过期,但足以让版本核对提前,而不是等业务逻辑排查一圈后再检查部署。
HAOVP 的构建链路
HAOVP 的业务服务先由 Maven 构建,在各模块的 target 目录生成 JAR。Dockerfile 再把匹配的模块 JAR 复制到镜像内,作为固定名称的应用文件运行。
一个简化结构是:
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY target/service-*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]示例隐藏了实际模块名称,不包含镜像仓库和服务器路径。
这条链路至少包含四个可能过期的位置:
Git 工作区
Maven target 目录
Docker 镜像层
当前运行容器只执行容器重启,不会自动重新编译源码;只重新编译 JAR,也不会自动替换已经构建好的镜像;只构建镜像,如果容器没有重新创建,运行实例仍可能使用旧镜像。
第一步:确认源码本身
先确认准备验证的修复确实存在于当前工作区:
git status
git log -1 --oneline
git diff这些命令只用于说明检查思路。实际仓库如果有未提交改动,需要同时记录提交版本和工作区差异,不能只写一个提交号就认为源码状态完整。
我会进一步确认:
- 修改发生在哪个模块;
- 构建命令是否会构建该模块;
- 修改是否已经保存;
- 当前分支是否正确;
- 是否存在同名类或旧目录;
- 配置来自本地文件还是配置中心。
有时接口行为不变不是 JAR 旧,而是修改了没有被实际加载的配置或另一个同名实现。
第二步:检查 JAR 是否重新生成
项目文档中的构建顺序是先在后端目录执行 Maven 构建,再重建业务服务:
mvn clean package -DskipTests
docker compose up -d --build service-a service-b服务名是通用占位示例。
clean 会清理旧构建目录,package 重新生成 JAR。跳过测试只能减少构建环节,不能证明代码正确;正式验收前仍要运行适合的编译、测试和接口检查。
构建完成后,我会确认:
- 目标模块的 JAR 是否存在;
- 文件时间是否对应本次构建;
- 构建日志是否真的包含目标模块;
- Maven 是否因为依赖失败而跳过后续模块;
target中是否残留多个能匹配通配符的 JAR;- Dockerfile 的
COPY模式最终会选择哪个文件。
如果目录里同时存在旧版本、原始 JAR 和重新打包 JAR,宽泛通配符可能产生歧义。更稳妥的做法是让构建产物名称可预测,并在镜像构建前验证匹配结果。
第三步:确认镜像真的重建
docker compose restart 只会重启现有容器,不会重新执行 Dockerfile。
需要构建新镜像时,应明确使用构建操作:
docker compose build service-a
docker compose up -d service-a或者使用项目文档中的 up -d --build。
即使带了 --build,仍要观察构建日志。Docker 会使用构建缓存;如果上下文、忽略规则或文件时间判断出现偏差,某些层可能被复用。
我会检查:
- Docker 的构建上下文是否包含新 JAR;
.dockerignore是否排除了目标文件;COPY层是否重新执行;- 新镜像的创建时间和标识是否变化;
- Compose 使用的镜像名是否就是刚构建的镜像;
- 是否有同名旧镜像或远程镜像覆盖本地构建。
不能只看到“build completed”就结束,还要确认目标服务使用了这次产物。
第四步:确认容器被重新创建
镜像更新后,运行容器还需要指向新镜像。
docker compose ps
docker inspect <container>这些命令可以帮助检查容器创建时间、镜像标识和运行状态。公开记录不保留真实容器名、主机路径和内部网络信息。
重点确认:
- 容器是否刚刚重新创建;
- 容器使用的镜像标识是否与新镜像一致;
- 服务是否启动成功,而不是反复重启;
- 是否有多个旧容器仍在对外提供服务;
- 反向代理或网关是否仍把请求转发到旧实例。
只更新一个副本时,如果负载入口仍在新旧实例之间分流,接口可能一会儿是新行为、一会儿是旧行为,看起来像随机故障。
第五步:核对容器内实际文件
当外部信息仍无法确定时,可以检查容器内的实际运行文件,而不是只看宿主机上的 JAR。
检查目标包括:
容器内应用文件是否存在
文件大小与修改时间是否符合预期
Java 进程启动参数指向哪个文件
启动日志是否来自当前版本如果项目有条件,可以在构建时写入版本元数据,例如 Git 提交号、构建时间和应用版本,并通过 Actuator info 或启动日志暴露非敏感版本信息。
build.commit=${GIT_COMMIT}
build.version=${PROJECT_VERSION}这是通用设计示例,不对应当前已部署配置。
版本信息必须避免包含用户名、本地绝对路径、仓库凭据和内部地址。
第六步:让接口行为证明版本
文件时间和镜像标识能说明部署链路,但最后还要验证原问题对应的接口行为。
我会选择最小可判定用例:
- 修复的是公开路径,就验证未登录请求是否按预期放行;
- 修复的是字段映射,就检查关键字段是否正确返回;
- 修复的是权限,就同时测试允许和拒绝场景;
- 修复的是事务时序,就检查响应、数据库副作用和后续查询;
- 修复的是异常处理,就触发明确且安全的错误条件。
版本证据
+ 目标接口复测
+ 关键副作用核对
= 运行版本与修复行为基本对齐只看到容器启动不能证明修复生效;只看到接口偶尔成功,也不能排除仍有旧实例参与流量。
为什么覆盖容器内 JAR 只适合临时排查
验收报告记录了重新构建并覆盖容器内应用 JAR 后,相关服务与本地源码对齐。这个动作能够快速确认“问题是否来自旧产物”,但不适合作为长期部署流程。
直接覆盖运行容器有几个问题:
- 容器重建后修改会丢失;
- 镜像内容与运行容器内容不再一致;
- 其他节点和副本不会自动同步;
- 很难通过镜像标识追溯真实版本;
- 回滚过程不清晰。
确认根因后,更稳定的闭环仍然是:重新构建 JAR、重新构建镜像、重新创建容器,并记录版本。
多服务项目要避免只更新一半
微服务项目中,一次改动可能同时涉及调用方、被调用方和公共模块。
例如:
公共 DTO 变化
-> 服务 A 重新编译
-> 服务 B 也需要重新编译
-> 两边镜像都要更新如果只更新一端,可能出现字段缺失、序列化失败、接口签名不一致或业务判断错误。
我会根据改动范围列出受影响服务:
- 公共模块变更影响哪些依赖方;
- 配置中心的变更是否需要重启;
- 数据库迁移是否先于应用发布;
- 前端是否依赖新的响应字段;
- 网关路由和白名单是否同时变化;
- 回滚时新旧版本能否兼容。
部署单个服务之前,先理解改动传播范围,可以减少“某个服务是新版本,另一个服务仍按旧协议运行”的问题。
把版本核对加入验收流程
这次排查后,我会把版本核对放在接口验收之前:
记录源码版本和工作区状态
-> 清理并构建目标模块
-> 核对 JAR
-> 构建镜像
-> 重新创建容器
-> 核对镜像与运行文件
-> 检查启动和健康状态
-> 执行接口用例验收报告中也应记录非敏感版本信息。这样出现失败时,可以先判断是代码缺陷、配置问题、数据前提,还是部署了错误版本。
这类问题的真正根因
旧 JAR 问题表面上是“忘了重新构建”,更深层原因是构建和部署链路缺少可见的版本证据。
如果源码、JAR、镜像和容器都没有统一版本标识,排查只能依赖文件时间和行为猜测。更完整的改进方向包括:
- 构建产物携带提交版本;
- 镜像使用不可变版本标签或摘要;
- 启动日志输出应用版本;
- 健康或信息端点提供非敏感构建信息;
- 部署记录保存服务与镜像映射;
- 发布后自动执行最小冒烟测试;
- 回滚使用明确的上一版本,而不是临时复制文件。
当运行行为与源码不一致时,先沿着“源码—JAR—镜像—容器—流量入口”逐层核对,通常比继续修改业务代码更快。部署版本可追溯,接口验收得到的结果才知道是在验证哪一份代码。