Maven 制品发布:从 GAV 到可恢复仓库交付
Maven 的发布对象不只有一个 JAR。一个 GAV 往往同时关联 POM、主制品、Sources、Javadoc、Classifier、校验文件和仓库元数据。deploy 返回成功,只能说明插件完成了当前请求;它不能自动证明上传地址正确、Release 不可覆盖、消费者解析到同一字节,或凭证没有被项目配置扩大作用域。
固定 Maven、JDK 与插件集合
实验基线采用 Apache Maven 3.9.16。Maven 3.9 系列运行时至少需要 JDK 8,但业务项目的编译目标由 Compiler Plugin、Toolchains 和 maven.compiler.release 另行决定。Maven 4 仍处在预览阶段,不应因为版本号更大就直接替换生产发布链。
团队仓库优先提交 Maven Wrapper,让 CI 和开发者通过同一个入口解析发行版。Wrapper 的配置、下载地址和校验值都要接受评审;没有 Wrapper 时,才由受控构建镜像提供固定 Maven。
java -version
./mvnw --version
./mvnw --batch-mode help:effective-pomWindows 使用 mvnw.cmd。流水线不能在 Wrapper 失败后静默回退到 Runner 全局 Maven,否则 Core、插件解析、本地仓库和 TLS 信任库可能同时改变。Maven Core 固定后,还要在父 POM 的 pluginManagement 固定 Compiler、Surefire、Jar、Source、Javadoc、Deploy 等关键插件版本;未固定插件版本同样会让构建漂移。
GAV 只是身份入口,不是完整字节证明
groupId:artifactId:version[:packaging[:classifier]] 描述逻辑坐标。正式 Release 应让一个坐标长期对应同一组字节,仓库端必须拒绝覆盖。版本以 -SNAPSHOT 结尾时,远端通常保存带时间戳与构建号的快照,再通过 maven-metadata.xml 解析当前构建;POM 中看见同一个 Snapshot 字符串,不代表两次构建下载的是同一内容。
构建前先确认 Effective POM 和依赖图,因为 Parent、Profile、属性与插件默认值可能改变最终制品。构建输出还应记录源码 Commit、JDK、Maven Core、插件版本和归一化时间配置。
./mvnw --batch-mode help:effective-pom > artifacts/effective-pom.xml
./mvnw --batch-mode dependency:tree > artifacts/dependency-tree.txt
./mvnw --batch-mode clean verify
sha256sum target/*.jar > artifacts/SHA256SUMSproject.build.outputTimestamp 可以减少归档时间造成的非业务差异,但它不会让不同源码生成相同制品,也不会消除 JDK、插件和依赖漂移。两个干净环境构建同一 Commit 后摘要不同,应该先比较输入和归档内容,不能在上传前选择“看起来正常”的一份。
用隔离本地仓库验证生产者和消费者
install 把制品写入本地仓库,deploy 才写远端。正式发布前,可用临时 maven.repo.local 验证生产者与消费者,避免用户 Home 中既有缓存掩盖缺失 POM、错误 Packaging 或依赖声明。
set -eu
LAB="$(mktemp -d)"
mkdir -p "$LAB/producer/src/main/java/com/example/lab"
cat > "$LAB/producer/pom.xml" <<'XML'
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.lab</groupId>
<artifactId>repository-proof</artifactId>
<version>0.0.0-lab.1</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.outputTimestamp>${env.NORMALIZED_BUILD_TIME}</project.build.outputTimestamp>
</properties>
</project>
XML
cat > "$LAB/producer/src/main/java/com/example/lab/BuildInfo.java" <<'JAVA'
package com.example.lab;
public final class BuildInfo {
public static final String VALUE = "lab-1";
private BuildInfo() {}
}
JAVA
cd "$LAB/producer"
mvn -Dmaven.repo.local="$LAB/m2" --batch-mode clean install
sha256sum "$LAB/m2/com/example/lab/repository-proof/0.0.0-lab.1/"*.jar消费者在另一个目录声明同一 GAV,并使用相同隔离仓库执行 dependency:tree 与测试。日志应明确出现 com.example.lab:repository-proof:jar:0.0.0-lab.1。完全离线时,实验需要预先准备经过校验的插件和依赖缓存;否则“离线失败”只能证明本地仓库不完整。
反向实验可以修改 BuildInfo.VALUE,仍使用相同 Release GAV 在另一个隔离仓库构建,再比较两个 JAR 摘要。输出不同,说明坐标相同并不保证字节相同,也证明远端 Release 仓库为什么必须拒绝覆盖。
distributionManagement 只负责上传位置
正式项目在 POM 的 distributionManagement 中保存逻辑仓库 ID 与 URL。Release 和 Snapshot 应进入不同仓库,分别应用不可变、保留、扫描与权限策略。
<distributionManagement>
<repository>
<id>corp-releases</id>
<url>https://packages.example.internal/maven/releases</url>
</repository>
<snapshotRepository>
<id>corp-snapshots</id>
<url>https://packages.example.internal/maven/snapshots</url>
</snapshotRepository>
</distributionManagement>repositories 决定普通依赖从哪里下载,pluginRepositories 决定构建插件从哪里解析,mirrors 可以改写请求目标,distributionManagement 决定项目向哪里上传。依赖下载成功不能证明当前身份有发布权限,Effective POM 中看到正确上传地址也不能证明 Settings 的凭证 ID 能匹配。
需要临时覆盖上传目标时,Deploy Plugin 3.x 支持 altDeploymentRepository、altReleaseDeploymentRepository 与 altSnapshotDeploymentRepository,格式是 id::url。旧插件曾使用不同格式,因此命令、插件版本和文档必须一起锁定。
./mvnw org.apache.maven.plugins:maven-deploy-plugin:3.1.4:help \
-Ddetail=true \
-Dgoal=deploy
./mvnw --settings "$MAVEN_SETTINGS" --batch-mode clean deploy \
-DaltReleaseDeploymentRepository=corp-releases::https://packages.example.internal/maven/releases \
-DaltSnapshotDeploymentRepository=corp-snapshots::https://packages.example.internal/maven/snapshots临时覆盖用于受控迁移和验证,不应把用户名密码拼进 URL。命令行会进入进程列表、CI 日志和任务元数据,泄露后的清理比临时 Settings 更困难。
settings.xml 承载身份,不进入源码
认证信息放在 CI 当前 Job 的临时 settings.xml。server.id 必须与 POM 或命令中的 Repository ID 完全相同;ID 不匹配时,Maven 仍会访问 URL,只是不附带预期凭证,表面上很像网络或仓库故障。
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd">
<servers>
<server>
<id>corp-releases</id>
<username>${env.MAVEN_REPO_USER}</username>
<password>${env.MAVEN_REPO_PASSWORD}</password>
</server>
<server>
<id>corp-snapshots</id>
<username>${env.MAVEN_REPO_USER}</username>
<password>${env.MAVEN_REPO_PASSWORD}</password>
</server>
</servers>
</settings>发布账号只应向目标 Release 或 Snapshot 路径写入,不能删除仓库、修改保留策略或管理用户。构建 Job 没有这份 Settings,发布 Job 使用写身份,回读验证使用另一个只读身份。这样 401、403 和回读失败才能对应不同责任域。
help:effective-settings -DshowPasswords=false 有助于诊断 Mirror、Profile 和 Server 合并结果,但输出仍可能包含内部域名、用户名与 Proxy 拓扑,只能进入受限日志。Maven 密码加密降低静态明文暴露,不等于 Secret 托管;掌握主密码和安全配置的主体仍可能解密。
Release 与 Snapshot 要分仓、分权、分保留
Release 是长期消费者契约。远端应拒绝同 GAV 覆盖,保留周期覆盖产品支持、审计和恢复窗口。Snapshot 是持续变化的协作指针,通过元数据选择当前构建;它适合联调,不适合作为生产部署、审批或事故证据的身份。
消费端可以对校验与更新策略做明确约束:
<repository>
<id>corp-public</id>
<url>https://packages.example.internal/maven/public</url>
<releases>
<enabled>true</enabled>
<checksumPolicy>fail</checksumPolicy>
</releases>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
<checksumPolicy>fail</checksumPolicy>
</snapshots>
</repository>updatePolicy=always 会增加元数据请求和仓库负载,只适合确实需要持续联调的环境。生产构建应使用 Release 或唯一 RC/Prerelease 坐标,并把依赖解析结果保存在构建证据中。checksumPolicy=fail 能发现传输损坏或仓库内容变化,不能证明发布者身份,也不会自动让可覆盖仓库变得不可变。
Snapshot 在两台 Runner 上解析不同,先保存两边的 maven-metadata.xml、本地文件摘要、Mirror、Profile 和更新策略,不要先删缓存抹掉现场。如果它已经进入审批或部署,恢复方向是切换到唯一版本,不是强求可变 Snapshot 充当不可变证据。
发布链路要独立回读
普通分支运行 verify,受保护 Tag 或审批后的发布 Job 才运行 deploy。发布前保存主 JAR、POM、附加制品和摘要;发布后使用全新 maven.repo.local 与只读 Settings 下载同一 GAV,避免命中刚构建的本地缓存。
./mvnw --batch-mode clean verify
./mvnw --batch-mode --settings "$MAVEN_SETTINGS" clean deploy
./mvnw --batch-mode \
-Dmaven.repo.local="$RUNNER_TEMP/readback-m2" \
--settings "$MAVEN_READONLY_SETTINGS" \
dependency:get \
-Dartifact=com.example.lab:repository-proof:1.4.0回读时比较 JAR 与 POM,确认附加制品和元数据齐全,并核对仓库返回的校验材料。多模块项目还要保存 Reactor 构建顺序和每个模块版本,避免父 POM 已发布、子模块失败后留下无法消费的半套版本。
发布任务的并发锁应以目标坐标或发布线为边界。取消旧任务不保证安全,因为旧任务可能已经上传部分模块。新任务开始前先查询远端 GAV、元数据和摘要,再判断是补齐、冻结还是提升新版本。
故障从身份、策略和字节三层判断
401 通常表示没有提供有效身份,应检查 distributionManagement.id、server.id、Settings 路径和 Secret 注入。403 常表示身份已识别但不具备目标写权限,也可能是 Snapshot 发往 Release 仓库、覆盖策略拒绝或路径不在授权范围。开启调试日志前先确认脱敏与访问边界。
同 GAV 已存在时,不要通过删除远端文件强行重发。先回读现有 JAR、POM 与摘要,确认它对应哪个 Commit 和发布任务。内容正确但元数据不完整,需要按仓库能力补偿;内容错误则阻止新消费者、通知已知使用者,并发布一个新的修复版本。
仓库删除不是业务回滚。制品可能已经进入本地仓库、企业代理、构建缓存、镜像和下游应用。恢复记录要包含错误 GAV、摘要、发布身份、已知消费者和代理传播范围。清理策略不能删除仍被生产、支持分支或灾备恢复点引用的 Release。
退出与升级都要保留坐标证据
升级 Maven Core 或关键插件时,使用固定样例比较 Effective POM、依赖图、归档内容、摘要、Deploy 请求和远端元数据。新版本已经写入远端后,回退工具只会影响下一次运行,不会撤销已有 GAV。
迁移仓库产品时,先冻结写入,导出 GAV 清单与摘要,把 Release、Snapshot、POM、附加制品和元数据按策略迁移,再从空本地仓库执行代表性消费。DNS 或 Mirror 切换只改变请求去向,不能证明目标仓库字节完整。
可验收的 Maven 发布链应同时证明:Wrapper 与插件版本固定,GAV 和构建摘要可追溯,Release 拒绝覆盖,Snapshot 不进入生产,临时 Settings 只在受保护 Job 出现,distributionManagement 与 server.id 精确匹配,远端回读不命中本地缓存。做到这些,deploy 才是交付链中的一个受控步骤,而不是把构建目录推向未知仓库的最后一条命令。
