文章

Maven 多模块项目构建失败:`${revision}` 被当成字面量版本号的排查与解决

语之溪

·

DevOps平台

·

前言

在本地构建该 Maven 多模块项目时,遇到了一个比较隐蔽的依赖解析问题。

项目执行 mvn validate 时一切正常,所有模块都能被 Maven 正确识别;但真正执行 mvn package 时,却在解析一个公司内部依赖的父 POM 时失败。错误信息显示 Maven 正在尝试下载一个版本号为 ${revision} 的制品:

Could not find artifact
com.bocloud.devops:devops-synergy:pom:${revision}

更直观的是,Maven 实际请求的私服路径中出现了:

com/bocloud/devops/devops-synergy/$%7Brevision%7D/

其中 $%7Brevision%7D 就是 URL 编码后的 ${revision}。

这意味着 Maven 没有把 ${revision} 解析成真实版本号,而是把它直接当作版本字符串去私服查找。

本文记录完整的排查过程、根因分析、本地临时解决方案,以及从发布流程上彻底解决这类问题的方法。


一、项目环境

当前项目是一个 Maven 聚合工程,共有 13 个模块:

devops-flow
├─ devops-flow-core
│  ├─ devops-flow-models
│  ├─ devops-flow-feign
│  ├─ devops-flow-service
│  └─ devops-flow-api
├─ devops-flow-sdk
├─ devops-flow-plugin
├─ devops-flow-runner-consul
├─ devops-flow-runner-k8s
├─ devops-flow-runner-nacos
├─ devops-flow-runner-bes-consul
└─ devops-flow-runner-bes-k8s

本地构建环境:

Apache Maven 3.6.3
JDK 11
Windows 11

项目根 POM 使用 Maven CI Friendly Versions:

<groupId>com.bocloud.devops</groupId>
<artifactId>devops-flow</artifactId>
<version>${revision}</version>
<packaging>pom</packaging>

<properties>
    <revision>3.10.0-patch-guojin</revision>
</properties>

子模块也通过 ${revision} 引用父模块:

<parent>
    <groupId>com.bocloud.devops</groupId>
    <artifactId>devops-flow</artifactId>
    <version>${revision}</version>
</parent>

这种写法本身没有问题,Maven 支持使用 ${revision}、${sha1} 和 ${changelist} 作为 CI Friendly Versions。


二、问题现象

1. validate 可以成功

首先执行:

mvn -U -DskipTests validate

所有 13 个模块都能正常加入 Reactor:

Reactor Build Order:

devops-flow
devops-flow-core
devops-flow-models
devops-flow-feign
devops-flow-sdk
devops-flow-service
devops-flow-api
devops-flow-plugin
devops-flow-runner-consul
devops-flow-runner-k8s
devops-flow-runner-nacos
devops-flow-runner-bes-consul
devops-flow-runner-bes-k8s

最终结果:

BUILD SUCCESS

这至少说明:

  • 根 POM 可以正常解析;
  • 项目自身的 ${revision} 可以解析;
  • 父子模块关系没有问题;
  • 所有 module目录都存在;
  • Maven可以访问公司私服;
  • 当前项目不是在 POM模型构建阶段失败。

2. package 阶段失败

继续执行:

mvn -U -DskipTests package -e

构建前几个模块成功,到 devops-flow-service 时失败:

[INFO] devops-flow ........................................ SUCCESS
[INFO] devops-flow-core ................................... SUCCESS
[INFO] devops-flow-models ................................. SUCCESS
[INFO] devops-flow-feign .................................. SUCCESS
[INFO] devops-flow-sdk .................................... SUCCESS
[INFO] devops-flow-service ................................ FAILURE

核心错误:

Failed to execute goal on project devops-flow-service:
Could not resolve dependencies for project
com.bocloud.devops:devops-flow-service:jar:3.10.0-patch-guojin

进一步展开:

Failed to collect dependencies at
com.bocloud.devops:devops-synergy-feign:jar:3.10.0-patch-guojin

最终根因:

Could not find artifact
com.bocloud.devops:devops-synergy:pom:${revision}

Maven 尝试访问的私服地址是:

.../com/bocloud/devops/devops-synergy/$%7Brevision%7D/
devops-synergy-$%7Brevision%7D.pom

这已经明确说明:问题不是找不到 3.10.0-patch-guojin,而是 Maven 正在查找字面量 ${revision}。


三、依赖链分析

问题发生在 devops-flow-service 模块。

该模块依赖了:

<dependency>
    <groupId>com.bocloud.devops</groupId>
    <artifactId>devops-synergy-feign</artifactId>
</dependency>

版本由根 POM 的 dependencyManagement 管理:

<properties>
    <devops-synergy.version>
        3.10.0-patch-guojin
    </devops-synergy.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.bocloud.devops</groupId>
            <artifactId>devops-synergy-feign</artifactId>
            <version>${devops-synergy.version}</version>
        </dependency>
    </dependencies>
</dependencyManagement>

因此,当前项目实际请求的是:

com.bocloud.devops:
devops-synergy-feign:
3.10.0-patch-guojin

这个版本本身没有问题。

完整的失败链路是:

devops-flow-service
└─ devops-synergy-feign:3.10.0-patch-guojin
   └─ parent: devops-synergy-core:3.10.0-patch-guojin
      └─ parent: devops-synergy:${revision}

真正出错的是最后一级:

devops-synergy-core
→ devops-synergy:${revision}

四、检查 Maven 本地仓库

通过 Maven 配置确认,本机使用了一个自定义本地仓库目录,而不是默认的:

C:\Users\<用户名>\.m2\repository

可以通过以下命令查看 Maven实际使用的 settings:

mvn help:effective-settings

或者使用调试模式:

mvn -X validate

然后在本地仓库中找到:

com/bocloud/devops/
devops-synergy-core/
3.10.0-patch-guojin/
devops-synergy-core-3.10.0-patch-guojin.pom

打开这个 POM后,发现其父版本仍然是:

<parent>
    <groupId>com.bocloud.devops</groupId>
    <artifactId>devops-synergy</artifactId>
    <version>${revision}</version>
</parent>

这就是直接原因。

这个 POM是从公司私服下载下来的,因此问题实际上存在于私服中的 Maven制品描述文件,而不是当前 devops-flow 项目的源码。


五、为什么 ${revision} 在当前项目能用,在私服依赖里却不能用

这是这个问题最容易让人困惑的地方。

1. 在同一个 Reactor 中,${revision} 可以正常工作

当前项目的根 POM定义了:

<properties>
    <revision>3.10.0-patch-guojin</revision>
</properties>

子模块的父版本使用:

<version>${revision}</version>

当整个多模块项目一起构建时,Maven拥有完整的 Reactor模型,可以把子模块和父模块关联起来,因此能够正确解析版本。

所以:

mvn validate

以及项目自身前几个模块的编译都没有问题。

2. 消费私服中的模块时,情况不一样

当另一个项目依赖:

devops-synergy-feign:3.10.0-patch-guojin

Maven首先读取它的 POM。

该 POM声明父工程为:

devops-synergy-core:3.10.0-patch-guojin

然后 Maven读取 devops-synergy-core 的 POM,又发现其父工程版本为:

<version>${revision}</version>

此时 Maven必须先定位父 POM,才能继续构建模型。

但它还没有拿到父 POM,因此无法从父 POM的 <properties> 中读取:

<revision>3.10.0-patch-guojin</revision>

于是就出现了一个先后依赖问题:

要解析 ${revision}
    ↓
需要先读取父 POM
    ↓
要读取父 POM
    ↓
需要先知道父 POM版本
    ↓
父 POM版本又是 ${revision}

最终 Maven只能把 ${revision} 当作字面量版本号去仓库查找。


六、为什么传 -Drevision 也没有解决

为了验证是否可以通过命令行覆盖版本,我尝试了:

mvn -U "-Drevision=3.10.0-patch-guojin" -DskipTests package

但错误仍然存在:

Could not find artifact
com.bocloud.devops:devops-synergy:pom:${revision}

这说明当前项目的用户属性没有介入外部依赖 POM的父模型定位过程。

简单说:

-Drevision=...

可以帮助当前 Reactor中的项目进行版本解析,但不能可靠修复一个已经发布到私服、且自身父版本写错的外部 POM。

因此,这不是一个适合靠构建参数解决的问题。


七、根因:发布到了私服的不是正确的 Flattened POM

devops-synergy 项目同样使用了 CI Friendly Versions。

它的源码 POM可能类似:

<parent>
    <groupId>com.bocloud.devops</groupId>
    <artifactId>devops-synergy</artifactId>
    <version>${revision}</version>
</parent>

这种写法在源码仓库里是正常的。

问题在于,发布到 Maven私服后,供其他项目消费的 POM不应该继续保留这个无法独立解析的 ${revision}。

正常情况下,发布后的 POM应当是:

<parent>
    <groupId>com.bocloud.devops</groupId>
    <artifactId>devops-synergy</artifactId>
    <version>3.10.0-patch-guojin</version>
</parent>

也就是把 CI Friendly占位符解析成具体版本。

项目公共父 POM中已经配置了 flatten-maven-plugin:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>flatten-maven-plugin</artifactId>
    <version>1.5.0</version>

    <executions>
        <execution>
            <id>flatten</id>
            <phase>process-resources</phase>
            <goals>
                <goal>flatten</goal>
            </goals>
        </execution>

        <execution>
            <id>flatten.clean</id>
            <phase>clean</phase>
            <goals>
                <goal>clean</goal>
            </goals>
        </execution>
    </executions>

    <configuration>
        <flattenedPomFilename>
            pom-xml-flattened
        </flattenedPomFilename>
        <updatePomFile>true</updatePomFile>
        <flattenMode>
            resolveCiFriendliesOnly
        </flattenMode>
    </configuration>
</plugin>

其中:

<flattenMode>resolveCiFriendliesOnly</flattenMode>

用于解析:

${revision}
${sha1}
${changelist}

而:

<updatePomFile>true</updatePomFile>

用于让后续安装和发布阶段使用生成的 flattened POM。

既然插件已经配置,私服中仍出现原始 ${revision},说明发布过程大概率存在以下情况之一:

  • 使用了 deploy:deploy-file 手动上传原始 pom.xml;
  • CI没有经过正常的 Maven生命周期;
  • 发布时跳过了 flatten插件;
  • 发布任务只执行了插件目标,没有执行 process-resources;
  • 使用了旧的构建产物;
  • 上传脚本明确指定了源码中的原始 POM;
  • flatten生成了正确 POM,但 deploy阶段没有使用它;
  • 多次覆盖同版本时,将正确 POM重新覆盖成了原始 POM。

八、本地临时解决方案

由于暂时无法立即修改私服制品,我们先对 Maven本地缓存中的错误 POM进行修复。

找到:

devops-synergy-core-3.10.0-patch-guojin.pom

把:

<version>${revision}</version>

改成:

<version>3.10.0-patch-guojin</version>

完整修改如下。

修改前:

<parent>
    <groupId>com.bocloud.devops</groupId>
    <artifactId>devops-synergy</artifactId>
    <version>${revision}</version>
</parent>

修改后:

<parent>
    <groupId>com.bocloud.devops</groupId>
    <artifactId>devops-synergy</artifactId>
    <version>3.10.0-patch-guojin</version>
</parent>

然后重新执行:

mvn -DskipTests package

构建成功:

BUILD_EXIT_CODE=0

13 个模块全部完成打包,5 个 runner可执行 JAR也都正常生成。


九、为什么本地修改有效

修改后,Maven解析依赖链时得到的是:

devops-flow-service
└─ devops-synergy-feign:3.10.0-patch-guojin
   └─ devops-synergy-core:3.10.0-patch-guojin
      └─ devops-synergy:3.10.0-patch-guojin

所有坐标都变成了确定值,不再需要通过尚未加载的父 POM来解析 ${revision}。

因此 Maven可以直接从本地仓库或私服中找到:

com.bocloud.devops:
devops-synergy:
3.10.0-patch-guojin

依赖模型顺利建立,后续编译和打包都恢复正常。


十、本地方案的限制

直接修改 Maven本地仓库只能作为应急手段。

它有以下限制:

1. 只对当前电脑有效

其他开发人员、测试环境和 CI流水线仍然会从私服下载错误 POM,因此还会遇到同样问题。

2. 删除本地仓库后会失效

如果清理:

devops-synergy-core/3.10.0-patch-guojin

或者清空整个 Maven本地仓库,修正后的 POM就会丢失。

3. 强制更新可能覆盖本地文件

执行:

mvn -U package

可能让 Maven重新从私服检查或下载 POM,从而覆盖本地修改。

是否覆盖取决于:

  • Maven仓库更新策略;
  • _remote.repositories 记录;
  • 私服响应;
  • IDE重新导入行为;
  • 制品是 release还是 snapshot。

本地修复后,在正式制品修好之前,应谨慎使用 -U。

4. IDEA重新导入可能再次暴露问题

IDEA Maven重新加载时也可能访问远程仓库。

如果重新加载后错误恢复,需要再次检查本地 POM是否被覆盖。

5. 不能解决 CI问题

本地修改不会进入 Git,也不会跟随代码提交,所以 CI仍然失败。

不要把整个 Maven本地仓库提交到项目,也不要把第三方错误 POM复制进源码目录充当正式解决方案。


十一、推荐的正式修复方案

正式方案应该在 devops-synergy 项目中修复发布流程,并重新发布正确的 flattened POM。

第一步:在源码项目中正常构建

在 devops-synergy 根目录执行:

mvn clean package -DskipTests

检查生成的文件:

devops-synergy-core/pom-xml-flattened

确认其中的父版本已经变成:

<parent>
    <groupId>com.bocloud.devops</groupId>
    <artifactId>devops-synergy</artifactId>
    <version>3.10.0-patch-guojin</version>
</parent>

不能继续出现:

<version>${revision}</version>

第二步:使用正常生命周期发布

执行:

mvn clean deploy -DskipTests

不要仅执行:

mvn deploy:deploy

也不要通过未经验证的脚本直接上传原始 POM。

第三步:核对私服中的 POM

发布完成后,从私服重新下载或直接检查:

com/bocloud/devops/
devops-synergy-core/
<新版本>/
devops-synergy-core-<新版本>.pom

确认父版本已解析为具体值。

第四步:使用新版本发布

如果条件允许,建议不要覆盖已经发布的 release版本。

例如将:

3.10.0-patch-guojin

提升为:

3.10.0-patch-guojin.1

或符合团队规范的新版本。

然后在 devops-flow 中更新:

<devops-synergy.version>
    3.10.0-patch-guojin.1
</devops-synergy.version>

这样做的好处是:

  • 避免私服代理缓存旧 POM;
  • 避免开发机继续命中旧缓存;
  • 避免 Maven认为 release版本不可更新;
  • 避免同一版本内容不一致;
  • 便于追踪修复版本;
  • CI环境不需要特殊清理。

十二、为什么不建议覆盖同一个 Release 版本

Maven release制品通常应该是不可变的。

也就是说:

groupId + artifactId + version

一旦发布,内容就不应再发生变化。

如果先发布了错误的:

devops-synergy-core:3.10.0-patch-guojin

然后又覆盖相同版本,可能产生以下问题:

  • 开发人员 A拿到旧 POM;
  • 开发人员 B拿到新 POM;
  • CI节点继续使用旧缓存;
  • 私服代理节点缓存不同;
  • 构建结果依赖构建机器;
  • 同一版本无法保证可重复构建;
  • 问题排查时很难确认实际使用的制品内容。

所以,正式修复最好发布新版本。


十三、发布前的自动检查建议

为了避免类似错误再次进入私服,可以在 CI发布前增加检查。

方案一:检查 flattened POM

Linux环境:

if grep -R '\${revision}' \
  --include='pom-xml-flattened' .; then
  echo "错误:flattened POM 中仍包含 revision 占位符"
  exit 1
fi

PowerShell环境:

$matches = Get-ChildItem -Recurse -File `
    -Filter 'pom-xml-flattened' |
    Select-String -SimpleMatch '${revision}'

if ($matches) {
    Write-Error 'flattened POM 中仍包含 ${revision}'
    exit 1
}

方案二:检查将要发布的所有 POM

如果团队允许在 dependency版本中保留某些属性,那么重点检查父版本:

Get-ChildItem -Recurse -File -Filter 'pom-xml-flattened' |
    Select-String -Pattern `
    '<parent>[\s\S]*?<version>\$\{revision\}</version>'

更稳妥的方式是用 XML解析器读取每个 POM的:

project.parent.version

如果值仍是:

${revision}

就阻止发布。

方案三:增加消费者验证

发布到临时仓库后,使用一个独立目录测试:

mvn dependency:get `
    -Dartifact=com.bocloud.devops:devops-synergy-feign:<版本>

或者创建一个最小 Maven项目,仅依赖该制品,然后执行:

mvn dependency:tree

这种方式非常重要,因为:

源项目自己能构建

不等于:

发布后的制品能被其他项目消费

本次问题正是典型案例:devops-synergy 在自身 Reactor中可能完全正常,但发布后的子模块 POM无法脱离原始工程独立解析。


十四、额外发现:可能不止一个制品存在类似问题

在本机 Maven仓库中搜索:

rg -l -F '${revision}' `
  '<Maven本地仓库>\com\bocloud\devops' `
  -g '*.pom'

发现不止一个公司内部制品的 POM保留了 ${revision}。

例如某些 POM的父版本或模块间依赖版本仍然写成:

<version>${revision}</version>

这意味着问题可能不是 devops-synergy 一个项目的个例,而是多个项目使用了相同的发布流程:

源码使用 ${revision}
        ↓
构建时可以正常解析
        ↓
发布时没有使用 flattened POM
        ↓
原始 POM进入私服
        ↓
其他项目消费时失败

因此建议对当前版本系列的内部制品做一次统一扫描,而不是只修复当前报错的一个依赖。

优先检查被大量服务依赖的公共模块:

  • *-common
  • *-models
  • *-feign
  • 多模块项目的中间父 POM
  • BOM或 dependency management项目
  • SDK模块
  • API模块

尤其要关注“中间父 POM”。

本次直接失败的并不是最外层根 POM,而是:

devops-synergy-core

这种 packaging=pom 的中间聚合/父模块。


十五、排查过程中容易踩的几个坑

坑一:只执行 validate

validate 成功只能说明项目模型和模块结构可以建立,并不能证明所有依赖都能完整解析。

依赖解析通常会在 compile、test、package 或某些插件执行阶段才真正发生。

因此至少应该执行:

mvn -DskipTests package

坑二:看到 ${revision} 就修改当前项目

错误信息里虽然出现 ${revision},但不代表当前项目的 revision 配置有问题。

一定要顺着:

Failed to collect dependencies at ...
Failed to read artifact descriptor for ...
Non-resolvable parent POM ...

找到究竟是哪个制品的哪个 POM包含未解析占位符。

坑三:认为是私服网络问题

本次 Maven能够从同一个私服下载其他 POM和 JAR,而且请求中明确出现:

$%7Brevision%7D

所以这不是:

  • 网络超时;
  • DNS失败;
  • 认证失败;
  • 私服不可用;
  • 防火墙拦截。

它是制品坐标错误。

坑四:把 ${revision} 对应版本上传到奇怪的目录

不能在私服中真的创建:

${revision}

版本目录来绕过问题。

这会把错误的数据模型继续传播,而且产生不可维护的非法版本。

坑五:只安装根父 POM

本机中其实已经存在:

devops-synergy:3.10.0-patch-guojin

但 Maven仍然失败。

原因是 devops-synergy-core 的父坐标写成了:

devops-synergy:${revision}

Maven不会自动猜测:

${revision} = 3.10.0-patch-guojin

所以仅仅安装具体版本的根父 POM并不能解决坐标不匹配。

坑六:把离线构建失败误认为原问题没解决

本地修复后,正常联网构建已经成功。

之后尝试:

mvn -o -DskipTests package

离线构建仍可能因为某些 Spring Boot Maven Plugin依赖没有完整缓存而失败,例如:

spring-boot-buildpack-platform
maven-shade-plugin
plexus-utils
jna

这是另一个问题:

本地仓库没有完整缓存插件及其依赖

它和最初的:

devops-synergy:pom:${revision}

不是同一个问题。

判断修复是否成功,应以正常联网构建的最终结果为准:

BUILD_EXIT_CODE=0

十六、推荐的 Maven 排查流程

以后再遇到类似问题,可以按以下顺序处理。

第一步:确认 Maven 和 JDK版本

mvn -version
java -version

确认:

  • Maven使用了哪个 JDK;
  • Maven home来自哪里;
  • Java版本是否符合项目要求;
  • Maven版本是否与团队环境一致。

第二步:确认模块模型

mvn validate

如果这里失败,优先检查:

  • 根 POM;
  • parent;
  • relativePath;
  • modules;
  • properties;
  • Maven settings;
  • 私服访问。

第三步:执行真正构建

mvn -U -DskipTests package -e

如果日志还不够:

mvn -U -DskipTests package -X

第四步:找最底层 Caused by

Maven日志通常很长,但最重要的是最后几层:

LifecycleExecutionException
└─ DependencyResolutionException
   └─ DependencyCollectionException
      └─ ArtifactDescriptorException
         └─ UnresolvableModelException
            └─ ArtifactNotFoundException

本次真正有用的是最底层:

Could not find artifact
com.bocloud.devops:devops-synergy:pom:${revision}

第五步:检查依赖树

可以运行:

mvn dependency:tree

如果只看某个依赖:

mvn dependency:tree `
  "-Dincludes=com.bocloud.devops:devops-synergy-feign"

多模块项目中还可以指定模块:

mvn -pl devops-flow-core/devops-flow-service `
  dependency:tree

第六步:检查本地 POM

不要只看 JAR是否存在,还要看:

artifact-version.pom

因为 Maven解析传递依赖、父工程和 dependency management时,依赖的是 POM,而不是只依赖 JAR。

第七步:区分临时方案和正式方案

临时方案:

修复本地缓存 POM

正式方案:

修复源项目发布流程
重新生成 flattened POM
发布新版本
消费者更新版本

十七、最终处理结果

本次处理结果如下:

  1. 当前 devops-flow 根 POM及其 ${revision} 配置正常。
  2. Maven私服连接正常。
  3. 真正的问题来自私服中的:

    devops-synergy-core:3.10.0-patch-guojin
  4. 它的已发布 POM仍包含:

    <version>${revision}</version>
  5. 本地将其父版本改为:

    <version>3.10.0-patch-guojin</version>
  6. 重新执行:

    mvn -DskipTests package
  7. Maven构建成功,13 个模块全部通过。
  8. 5 个 runner模块的可执行 JAR全部生成。
  9. 没有修改 devops-flow 项目源码或项目 POM。
  10. Git工作区保持干净。
  11. 正式修复仍需在 devops-synergy 项目中重新发布正确的 flattened POM。

十八、总结

这类问题表面上看是:

Maven找不到依赖

实际上是:

发布到私服的 POM没有完成版本扁平化

最关键的判断依据是请求路径中出现:

$%7Brevision%7D

它说明 Maven正在查找字面量 ${revision}。

整个问题可以概括为:

源码 POM 使用 ${revision}
          ↓
源项目在同一 Reactor 中构建正常
          ↓
发布时未使用正确的 flattened POM
          ↓
私服中的子模块 POM仍包含 ${revision}
          ↓
消费者无法定位该子模块的父 POM
          ↓
依赖解析失败

本地修改缓存 POM可以快速恢复开发,但它只能救急。真正可靠的解决方式是:

规范 flatten-maven-plugin 配置
        +
使用完整 Maven 生命周期发布
        +
发布前检查 flattened POM
        +
使用新版本而不是覆盖 Release
        +
通过独立消费者验证发布结果

对于大型 Maven多模块微服务项目,尤其是大量使用内部 common、feign、models 和中间父 POM的项目,发布后的 POM是否可独立消费,与 JAR能否成功生成同样重要。

能在源项目里构建成功,不代表发布到私服后一定能被其他项目正常使用。

现在已有 72 次阅读,0 条评论,0 人点赞
作者:语之溪
作者
Maven 多模块项目构建失败:`${revision}` 被当成字面量版本号的排查与解决
当前文章累计共 18319 字,阅读大概需要 10 分钟。
Nacos 在服务注册和配置管理中的作用
2026年1月17日 - 0评论
旧 JAR 与源码不一致时,如何定位部署版本问题
2026年5月2日 - 0评论
Spring Security 和 JWT 在微服务认证中的协作方式
2026年2月7日 - 0评论
评论:共0条
发表
功能 探索 消息

足迹

你还不曾留下足迹..

音乐单

随机文章

夜晚

💬
你还不曾留言过
博主 不再显示
博主
未知作品 歌曲封面
立即安装