前言
在本地构建该 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-guojinMaven首先读取它的 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=013 个模块全部完成打包,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
fiPowerShell环境:
$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
发布新版本
消费者更新版本十七、最终处理结果
本次处理结果如下:
- 当前
devops-flow根 POM及其${revision}配置正常。 - Maven私服连接正常。
真正的问题来自私服中的:
devops-synergy-core:3.10.0-patch-guojin它的已发布 POM仍包含:
<version>${revision}</version>本地将其父版本改为:
<version>3.10.0-patch-guojin</version>重新执行:
mvn -DskipTests package- Maven构建成功,13 个模块全部通过。
- 5 个 runner模块的可执行 JAR全部生成。
- 没有修改
devops-flow项目源码或项目 POM。 - Git工作区保持干净。
- 正式修复仍需在
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能否成功生成同样重要。
能在源项目里构建成功,不代表发布到私服后一定能被其他项目正常使用。