前阵子把自己的一个开源组件正式发布到了 Maven 中央仓库,整个过程走下来最大的感受是:网上零散教程不少,但要么停在“注册账号”这一步,要么直接跳到“执行 deploy”,中间那些命名空间验证、GPG 签名、staging 校验、中央仓库同步,每一步都有不少隐藏细节。这篇文章我把自己从零到一发布的完整路径梳理一遍,包括两个可走的发布通道、pom.xml 和 settings.xml 的落地写法、常见的报错与排查手段。如果你是开源作者,或者在公司里维护公共组件库、想让同事用一行坐标把包引过去,这篇指南应该能帮你节省大量试错时间。
1. 发布到 Maven 中央仓库的整体思路,怎么设计一条可行路径
1.1 Maven 到底在干什么,什么才算一个组件
很多刚开始接触 Maven 的朋友会问:maven 是干嘛的?简单说,Maven 是一个 Java 项目的构建和依赖管理工具,它把项目打包、编译、测试、依赖拉取这些重复劳动接管了。而 Maven 中央仓库,相当于 Java 生态的公共制品库,全世界开发者的 jar 包都放在那里,任何人只需要在项目的 pom.xml 里声明三要素,就能把对应的组件拉到本地来用。
这三要素就是 groupId、artifactId、version。groupId 标识组织或作者,artifactId 标识具体的组件名,version 标识版本。生活化类比一下:groupId 像快递地址里的城市和街道,artifactId 像收件人的名字,version 像这个人的第几代手机型号。缺了任何一环,Maven 都不知道该从仓库里取哪个包。
把自己写的组件发布到中央仓库,意味着你的坐标可以在全球范围内的任意 Maven 项目里被引用。别人在自己的 pom.xml 里写一行依赖,Maven 就会自动去中央仓库下载,连带这个组件依赖的其他第三方库也会一起拉下来。这就是公共组件的威力:发布一次,全生态受益,不用再让使用者手动下载 jar 包丢到 lib 目录,也避免了“下载到的 jar 版本和本地不一致”这种传统做法里最令人头疼的问题。
我之前在公司里维护过一段时间内部工具库,那时候用的都是私有 Nexus 仓库,相对简单,随便传。但公共组件完全不同,中央仓库对内容的审核极其严格,这也是本文后面要花大量篇幅讲各种校验规则的原因。
1.2 发布的两条通道:OSSRH 老流程与 Central Portal 新流程
发布到中央仓库,现在有两条主流通道。初学者最困惑的就是这里,因为网上教程经常各说各话,有的让你去 Sonatype JIRA 开工单,有的让你去 central.sonatype.com 直接注册,其实都对,只是时代不同、通道不同。
最早也是最经典的通道,是 Sonatype 的 OSSRH 服务配合 Nexus Staging 仓库。流程是这样的:先去 issues.sonatype.org 提交一个工单,申请以某个 groupId 发布组件,等管理员审核通过后,Maven 会把组件部署到 s01.oss.sonatype.org 对应的 staging 仓库,然后你登录网页端,执行 Close 校验,等校验通过后再执行 Release,组件才会自动同步到中央仓库。这套流程运行了十几年,大量老项目都在用,很多教程讲的也是它。
后来 Sonatype 推出了新的 Central Portal,也就是 central.sonatype.com。这个门户把发布体验大大简化了:注册账号后,直接在页面上创建 namespace,验证域名所有权,之后可以通过网页上传,也可以通过 Maven 命令行直接部署。校验和发布的过程也更自动,不少环节不需要人工去点 Close、Release,系统会直接帮你处理。如果是从没发布过组件的新项目,我建议直接走 Central Portal,省心很多。老账号继续用 OSSRH 也没有问题,两种通道并不冲突。
两条通道对比下来,大概是这样:
| 对比项 | OSSRH 老通道 | Central Portal 新通道 |
|---|---|---|
| 申请位置 | issues.sonatype.org 开工单 | central.sonatype.com 注册后创建 |
| 命名空间验证 | 提交工单后按提示验证域名 | 自动化域名所有权验证 |
| 部署地址 | s01.oss.sonatype.org 的 staging 仓库 | central.sonatype.com/api/v1/publisher/upload |
| 发布操作 | 手动 Close + Release | 页面触发或命令行部署后自动处理 |
| 适合场景 | 老项目、已有 groupId 的项目 | 新组件、希望流程更简单的开发者 |
我第一次发布时是先接触的老通道,等搞完才知道新通道已经很好用了,后来第二个组件就全程用 Central Portal。本文后面的配置讲解我会两条路都覆盖到,因为老通道的知识点在很多遗留项目里依然会用到。
1.3 为什么中央仓库要求这么严:源码、javadoc 和 GPG 签名
在私有仓库里传 jar 包,传上去就能用,没人管你。但中央仓库不一样,它承载的是全 Java 生态的信任,所以对上传物有硬性要求:必须提供源码包 sources.jar,必须提供 javadoc 包,必须用 GPG 对文件做数字签名,同时 pom 里必须写清楚 license、developers、scm 这些元数据。
很多人不理解为什么非要源码和 javadoc。其实道理很简单:公共组件被无数项目引用,使用者遇到问题时需要直接看源码定位问题,而不是对着反编译代码猜;javadoc 则是为了让 IDE 里悬浮提示、自动补全时有完整的 API 文档。如果没有这两个包,Central 的规则校验直接就不通过,组件根本无法进入发布阶段。
GPG 签名更好理解:你生成一对公私钥,发布时用私钥给 jar 包和 pom 文件都签上名,然后把公钥传到公开的 keyserver 上。任何人拿到你的组件,都可以用公钥验证文件确实是你发布的、没有被篡改。这就像你在合同上盖了印章,而印鉴是在公证处做过备案的。中央仓库在接收组件时,也会用你上传的公钥去校验签名。这一环是很多新手最容易卡住的地方,因为涉及 GPG 工具、密钥 ID、keyserver 传播,细节非常多,我在后面单独开一节详细讲。
2. 发布前的准备:账号、命名空间、GPG 和 Maven 配置
2.1 发布前需要准备好的材料清单
我整理一下发布前必须准备的东西,先列个清单,你对照检查,缺哪个补哪个,免得发布到一半才发现少了东西。
| 材料 | 说明 | 备注 |
|---|---|---|
| JDK 8+ | 构建项目的基础环境 | 我习惯用 JDK 17 构建,兼容性更好 |
| Maven 3.6+ | 构建和部署工具 | 老版本也能用,但建议新版 |
| GnuPG 命令行工具 | 生成密钥和签名 | macOS/Linux 自带或 brew install,Windows 装 Gpg4win |
| Sonatype 账号 | 认证身份 | Central Portal 支持多种登录方式,OSSRH 用 JIRA 账号 |
| 域名或 GitHub 账号 | 验证 groupId 所有权 | 比如 io.github.yourname,需要能证明你拥有它 |
| 待发布的组件源码和 pom | 你要发布的东西 | 确保本地 mvn clean package 能过 |
| 私钥备份 | GPG 私钥要妥善保存 | CI 自动发布时需要用到 |
这里面最容易忽略的是最后一项。很多人第一遍发布成功后就把 GPG 私钥丢在笔记本里不管了,结果换电脑或者配置 CI 自动发布时,发现私钥找不回来,还得重新生成、重新验证,非常被动。
2.2 申请命名空间:两种通道的具体操作
命名空间也就是 groupId 的开发者可控制部分。中央仓库不允许随便使用一个自己没有所有权的 groupId,你申请了 io.github.yourname,就要证明 yourname 真的归你所有,否则任何人都可以冒充一个组织发布恶意组件,那生态就乱了。
Central Portal 新通道的操作相对轻松。注册并登录 central.sonatype.com,进入 Publishing 相关页面,点击 Add Namespace,输入你想申请的 groupId,比如 io.github.yourname。系统会要求验证所有权,通常有两种方式:如果你填的 groupId 正好对应 GitHub 用户名,可以导入 GitHub 仓库列表来自动验证;不然就在 DNS 解析里添加一条 TXT 记录,验证完成后删除即可。整个流程是自动化的,快的话几分钟到几十分钟就通过。
OSSRH 老通道则要走人工工单。到 issues.sonatype.org 创建新 issue,项目选择 Community Support,填写 Summary 和 Description,说明你想发布什么组件、期望的 groupId 是什么。机器人会在几分钟内回复,告诉你去做域名验证或者其他确认操作。人工审核通常需要一到三个工作日,耐心等就行。我当时第一次等了两天,每天刷新工单页面,后来发现这种等待属于常态,不用太焦虑。
值得提醒的是,groupId 一旦确定并被大家使用了,尽量不要再去更换。因为旧坐标会被很多人的 pom.xml、文档、依赖锁定文件引用,你改了坐标就等于把以前的使用者全部抛弃了。所以在申请之前想清楚:是用个人命名空间 io.github.you,还是用公司域名 com.yourcompany,还是某个开源组织的专属 groupId。
2.3 生成并同步 GPG 密钥,这一步最容易踩坑
GPG 签名在中央仓库发布里的地位非常高,但很多人恰恰在这里被劝退。其实流程本身不复杂:生成密钥、导出公钥、上传公钥到 keyserver、发布时配置 Maven 使用私钥签名。
生成密钥用命令:
gpg --full-generate-key交互过程会让你选择密钥类型和长度,我选的是 RSA and RSA,长度 4096。然后输入姓名和邮箱,这里有个小细节:邮箱最好和你 Sonatype 账号绑定的邮箱保持一致,虽然中央仓库校验时不一定强制邮箱一致,但保持一致能避免后续很多身份关联上的疑问。最后设置一个口令,这个口令要牢牢记住,后面 Maven 签名时要用,它不是登录 Sonatype 的密码,是私钥的解锁口令。
生成后查看密钥 ID:
gpg --list-secret-keys --keyid-format=long输出里会有一行类似sec rsa4096/1A2B3C4D5E6F7A8B,其中1A2B3C4D5E6F7A8B就是你的密钥 ID。导出公钥:
gpg --armor --export 1A2B3C4D5E6F7A8B然后把公钥上传到公开 keyserver:
gpg --keyserver keyserver.ubuntu.com --send-keys 1A2B3C4D5E6F7A8B这里有个现象:不同 keyserver 之间的公钥同步,并不是在瞬间完成的。有时候你刚 send-keys 完,中央仓库那边去查询还查不到,会报签名验证失败。遇到这种情况基本只能等,或者换一个同步更快的 keyserver 再发送一次。我在实际发布时遇到过这个问题,当时以为配置错了,排查了半天,其实只是公钥还没来得及从 keyserver A 传播到 keyserver B,睡一觉第二天再试就好了。
私钥的导出备份也建议做,命令是:
gpg --export-secret-keys 1A2B3C4D5E6F7A8B > private.key文件保存到离线或者加密容器里,后面 CI 发布时如果要重新 import,就用gpg --batch --import private.key导入。
2.4 settings.xml 的正确配置姿势,密码别乱塞
Maven 的全局配置文件位于~/.m2/settings.xml,发布时它负责提供服务器认证信息。最核心的其实是server配置,其中id必须和 pom.xml 里 distributionManagement 的 repositoryid完全一致,Maven 靠这个 id 去匹配该用哪组用户名密码。
老通道 OSSRH 的 settings.xml 可以这样写:
<settings> <servers> <server> <id>ossrh</id> <username>你的Sonatype账号</username> <password>你的Sonatype密码</password> </server> </servers> <profiles> <profile> <id>ossrh</id> <properties> <gpg.passphrase>你的GPG私钥口令</gpg.passphrase> </properties> </profile> </profiles> </settings>这里有两个关键点。第一,server的 id 不要觉得自己随便定一个就好,它是密钥匹配键,pom 里写了什么 id,settings.xml 里就要有对应的 id。第二,明文密码确实不好,Maven 支持 master password 加密机制,但配置起来复杂度高于本文范围;如果你的电脑是私人机器,并且没有把 settings.xml 提交到代码仓库,直接使用明文问题不大。真正危险的是把 settings.xml 传到了公开的 GitHub 仓库,那密码就相当于裸奔了。
Central Portal 新通道的配置思路类似,只是部署地址换成了新地址。你可以在 Central Portal 后台生成访问凭据,然后在 settings.xml 里配置:
<server> <id>central</id> <username>你的用户名或访问令牌名称</username> <password>你的访问令牌</password> </server>pom.xml 里镜像仓库 id 也用central,这样 Maven 就能匹配上。顺便说一句,settings.xml 里配置多套 server 是允许的,老通道新通道的 server 可以同时存在,互不干扰,切换项目时只要 pom 里的 id 不同就能自动匹配到对应配置。
3. 核心实操:从 pom.xml 配置到命令行成功发布
3.1 一份能通过中央仓库校验的 pom.xml 长什么样
前面所有准备做好,接下来就是把 pom.xml 配置到“中央仓库能接受”的水平。这不是随便一个能构建的 pom 就行,中央仓库对元数据有硬性校验,缺一项 Close 阶段就过不去。
先看一份最小可用的完整配置:
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>io.github.yourname</groupId> <artifactId>your-component</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <name>your-component</name> <description>A brief description of your component</description> <url>https://github.com/yourname/your-component</url> <licenses> <license> <name>Apache License, Version 2.0</name> <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url> </license> </licenses> <developers> <developer> <name>Your Name</name> <email>you@example.com</email> </developer> </developers> <scm> <connection>scm:git:git://github.com/yourname/your-component.git</connection> <developerConnection>scm:git:ssh://git@github.com/yourname/your-component.git</developerConnection> <url>https://github.com/yourname/your-component</url> </scm> <distributionManagement> <repository> <id>ossrh</id> <url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url> </repository> <snapshotRepository> <id>ossrh</id> <url>https://s01.oss.sonatype.org/content/repositories/snapshots/</url> </snapshotRepository> </distributionManagement> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> <version>3.3.1</version> <executions> <execution> <id>attach-sources</id> <goals><goal>jar-no-fork</goal></goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-javadoc-plugin</artifactId> <version>3.6.3</version> <executions> <execution> <id>attach-javadocs</id> <goals><goal>jar</goal></goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-gpg-plugin</artifactId> <version>3.2.4</version> <executions> <execution> <id>sign-artifacts</id> <phase>verify</phase> <goals><goal>sign</goal></goals> </execution> </executions> </plugin> </plugins> </build> </project>老通道的 publicationManagement 指向s01.oss.sonatype.org,新通道则是https://central.sonatype.com/api/v1/publisher/upload,这个地址会随账号不同而变化,建议以 Central Portal 后台显示的为准。
licenses、developers、scm 这三项是经常被忽略的。licenses 是必须的,没有 license 的组件任何人都不知道你是否允许别人使用、修改,中央仓库不可能让这种组件进去;developers 记录谁维护这个组件,出了问题好找人;scm 记录源码仓库地址,让使用者能回溯到你的代码。缺任何一项,Close 校验都会明确报错。
一个非常容易出现的问题:如果你用了 Spring Boot 的spring-boot-maven-plugin构建项目,默认会生成一个 Fat Jar,也就是把依赖都打进去的可执行 jar。中央仓库原则上不接受这种 fat jar,因为里面包含了大量第三方类的副本,很容易造成类冲突,也让源码包和 javadoc 包的规定失去意义。如果你确实要发 Spring Boot 项目,一般需要额外保留一个普通 jar,或者干脆把组件拆出来单独构建。
3.2 发布命令与完整流程:deploy 之后的 Close 和 Release
配置完成后,先在本地执行一次完整构建,确认没问题:
mvn clean package这一步只是打包,还不会触发 GPG 签名,也不会部署到远端。接下来执行真正的发布命令:
mvn clean deploy -DskipTests这里让我解释一下-DskipTests的用意。中央仓库并不要求你跳过测试,但发布流程中测试增加耗时,而且如果你的测试依赖某些环境变量,在发布机器上还可能失败,导致整个发布中断。我一般发布前先在本地完整跑一遍测试,真正执行 deploy 时就跳过,组件质量靠发布前的测试保证,发布动作本身追求稳定可控。
执行过程中,Maven 会依次执行 clean、compile、test(被 skip)、package、sign、deploy。签名阶段会提示输入 GPG 私钥口令,如果你在 settings.xml 里通过 profile 配置了gpg.passphrase,这里会直接用配置里的口令,不会弹交互提示。如果弹出交互输入,说明你的配置没生效,检查一下 settings.xml 的 profile 是否激活。看到 BUILD SUCCESS 后,组件已经上传到远端了。这里我踩过坑:一开始以为上传完就结束了,其实还差最关键的两步。
如果是老通道 OSSRH,现在要登录 OSSRH 的网页端,在 Staging Repositories 里找到你刚上传的仓库。选择它,点击 Close 按钮。Close 的意思是冻结这个 staging 仓库,并触发中央仓库的规则校验。校验包括:是否含有 source jar、javadoc jar、GPG 签名文件、pom 元数据是否合法。校验一旦失败,页面会列出具体错误,你得根据错误修复后重新上传和执行 Close。校验通过后,再点击 Release,组件才会真正同步到中央仓库。
新通道 Central Portal 简化了很多,你在部署后回到 central.sonatype.com 的发布列表,能看到组件状态从 Validating 到 Publishing 再到 Published,基本上自动推进,不需要手动 Close 和 Release。系统校验失败时也会显示错误信息,修复后重新部署即可。
关于版本号,这里有一个非常关键的概念:SNAPSHOT 版本和 Release 版本。带-SNAPSHOT后缀的版本默认发布到快照仓库,它不会被同步到中央仓库的正式索引,别人也无法通过正式 release 坐标稳定引用。要发布正式版本,版本号必须是干净的,比如1.0.0、2.3.1,不能带-SNAPSHOT。我见过有人用mvn deploy发布1.0.0-SNAPSHOT,然后在 Staging 里发现找不到,还以为上传失败,其实是版本号本身就不对。
3.3 发布后如何验证:别急着庆祝,先检查三件事
发布完成后,直接去 search.maven.org 搜索你的坐标,八成会搜不到。这是因为中央仓库的搜索引擎索引有延迟,组件同步到存储节点后,建立搜索索引通常需要几分钟到几小时。这时候不要以为自己发布失败,先做三层验证。
第一层,查 Central Portal 或 OSSRH 上的状态。老通道应该能看到 Staging 仓库状态是 Released,新通道能看到 Published。
第二层,直接请求制品库的路径,看文件是否真实存在。中央仓库文件地址格式是固定的,比如:
https://repo1.maven.org/maven2/io/github/yourname/your-component/1.0.0/your-component-1.0.0.jar如果能在浏览器直接访问下载,说明制品确实已经同步到中央仓库了。
第三层是本地依赖验证。在一个全新的空目录里创建测试项目,引入你的坐标,执行:
mvn dependency:get -Dartifact=io.github.yourname:your-component:1.0.0注意,测试项目不要复用你本地的 Maven 仓库缓存。有条件的话可以在 CI 环境或者另一台机器上做一次干净拉取,确保没有从本地仓库误读到旧包。
这层验证很有价值。我遇到过一种情况:自己电脑上测试一切正常,后来发现是因为之前mvn install把包装进了本地仓库,Maven 优先从本地仓库获取了,根本没有触发远端下载。用干净环境验证之后,才是真正确认发布成功。
4. 发布过程中最常见的坑和排查方法
4.1 认证与权限类报错:401、403 到底哪里配错了
发布过程中遇见的第一类高频问题,就是身份认证报错。典型表现是 deploy 执行到最后,Maven 提示:
Return code is: 401, ReasonPhrase: Unauthorized.这类问题的排查思路其实很清晰。401 通常就是用户名密码不对,或者 Maven 根本没有找到对应的 server 配置。先检查 settings.xml 里的server id是否和 pom.xml 中 distributionManagement 里 repository 的 id 完全一致。这个 id 匹配是 Maven 的机制:pom 里声明部署到哪个仓库,Maven 到 settings.xml 里找相同 id 的认证信息。两边不一致,Maven 就没有可用的认证信息,直接一个 401 甩过来。
密码方面,如果走老通道用的是 JIRA 账号密码,有些人会把 JIRA 密码和登录 Central Portal 的密码混为一谈,导致认证失败。新通道的 token 一般是一串长字符串,复制的时候注意别把前后空格也带进去了。我遇到过最隐蔽的问题:密码里含有&或<这类 XML 特殊字符,直接写进 settings.xml 导致 XML 解析错误或配置被截断,后来用 XML 转义或者把特殊字段放到环境变量里引用才解决。
403 的情况不太一样,通常表示你连到了服务器,但权限不足。最常见的原因是命名空间验证还没有通过,你用io.github.yourname申请了 namespace,但验证流程还没完成,或者被拒绝了。这时回到 Central Portal 或工单页面看状态,pending 状态就等,Rejected 就检查域名所有权证明材料。
4.2 签名问题:gpg: no secret key 和公钥同步延迟
GPG 相关的报错可以排在发布失败原因的前三名。先看一个很常见的:
gpg: no secret key gpg: signing failed: No secret key出现这个问题的原因很直接:Maven 在执行 gpg 签名时找不到你的私钥。首先确认执行命令的当前用户是否是生成密钥的用户,如果公司电脑有多个系统账户,你的 GPG 密钥在另一个用户下,那当然找不到。用gpg --list-secret-keys查看一下私钥是否存在,注意sec行代表私钥存在,如果只有pub行那只是公钥,Maven 签名需要私钥。
CI 环境里这个问题更典型。很多人在本地发布了成功,然后把代码推给 CI 自动发布,结果 CI 机器上没有私钥。正确的做法是先导出私钥,再导入 CI 环境:
gpg --export-secret-keys 1A2B3C4D5E6F7A8B > private.key gpg --batch --import private.key导入后还有一个易被忽略的问题:GnuPG 2.x 版本和某些 CI 环境没有终端交互,运行gpg --sign时会报Inappropriate ioctl for device,因为 GPG 默认想弹出终端让用户输入口令。解决办法是确保你在签名时能提供 passphrase,可以在 settings.xml 里配置,或者在 CI 中设置gpg.passphrase属性。另外,GPG 会锁定主目录的某个目录,多进程并发签名时可能报“resource temporarily unavailable”,CI 里并发构建多个模块时适当控制并发数。
至于公钥同步问题,前面讲过,Keyserver 之间的同步不是实时的。老通道在 Close 阶段中央仓库会验证签名,需要去 key server 查你的公钥。如果刚刚发送公钥,查不到是很正常的。我的排查方式:先用公钥服务器网页端查询,看你的密钥 ID 是否已经被收录;如果收录了,再去新通道或 staging 校验;如果没收录,换一个 keyserver 重新--send-keys,然后再等一段时间。
4.3 校验失败:缺 sources.jar、javadoc.jar,还有 license 的坑
Central 的规则校验是出了名的严格。老通道的 Close 阶段如果失败,页面上会列出具体错误。排第一的错误基本是:
Missing: io.github.yourname:your-component:1.0.0 - 缺少 sources.jar - 缺少 javadoc.jar这种情况往往是 pom 里的 source 插件、javadoc 插件没配置,或者配置了但没有正确绑定到生命周期。注意,maven-source-plugin的 goal 最好用jar-no-fork,不要用jar,后者在构建某些模块时会因为生命周期 fork 问题导致重复执行或者不执行。javadoc 插件在 Java 17 以上环境有另外一个经典问题:javadoc 生成过程中遇到文档语法警告会直接失败,报一堆error: unexpected text。解决办法是在插件的<configuration>里加<doclint>none</doclint>,关闭严格文档检查。
还有一种我花了不少时间才定位的问题:pom 里写的 license 名称和 URL 与实际使用的许可证不匹配。中央仓库的校验器会检查 license URL 是否指向一个可访问的许可证文本页面,填一个https://www.apache.org/licenses/LICENSE-2.0.txt这样真实的地址,不要随手填一个空链接。developers 里的 email 格式也必须合法,填写xxx这种字符串是无法通过校验的。
另外,如果你发布的是多模块项目,需要在每个子模块里也配置好这些插件,或者用 parent pom 统一管理。不然可能会出现主模块全过了,某个子模块缺少源码包,导致整个 staging 仓库校验失败。这时错误列表会定位到具体模块,你去看对应模块的 effective-pom,检查插件是否被继承到。
4.4 发布成功但中央仓库搜不到:索引延迟还是版本号写错
这是新手最容易自我怀疑的时刻:日志显示 BUILD SUCCESS,Central Portal 状态也是 Published,但 search.maven.org 上怎么搜都搜不到。其实大部分原因前面说过,就是搜索引擎索引的延迟。
补充一个排查技巧:如果dependency:get已经能拉到 jar,那就是制品确实在仓库里了,搜索不到只是索引还没更新。等几个小时再看就行,不用重复发布,重复发布同一个版本只会徒增烦恼。反过来,如果dependency:get报 404,那就是制品根本没进仓库,这时候要回到状态页面看是否已经正式 Released,还是停留在校验失败的边缘。
另外检查一下你是不是真的把版本号发布成了正式版,而不是 SNAPSHOT。有些人标签写1.0.0,但 pom 里 version 写的是1.0.0-SNAPSHOT,deploy 时会进入 snapshot 仓库,看着也像发布成功,但正式依赖永远拉不到。这种事发生了不止一次,检查起来却很简单:直接打开仓库文件列表页面,看目录名和文件名里的版本号。
以下是常见报错与排查的速查表:
| 报错信息 | 本质原因 | 建议处理方式 |
|---|---|---|
| 401 Unauthorized | 找不到匹配的认证信息或密码错 | 检查 server id 是否一致、重新生成 token |
| 403 Forbidden | 命名空间验证未完成 | 回到 Central Portal 查看 namespace 状态 |
| gpg: no secret key | 没有私钥或私钥不在当前用户 | 导入私钥,使用 gpg --list-secret-keys 确认 |
| Missing sources.jar/javadoc.jar | 构建阶段未生成附件 | 配置 source/javadoc 插件,javadoc 加 doclint |
| Missing license / scm | pom 元数据缺失 | 补全 licenses、developers、scm 内容 |
| FATAL: Not a valid release | 打包了 Spring Boot fat jar | 调整构建配置,保留普通 jar 再发布 |
5. 一次发布后的经验复盘:几个我后来一直在用的习惯
走过一遍完整流程后,我想分享几个从第二次发布开始就一直遵循的习惯。
第一个习惯,第一次正式发布前,先用一个 dummy 组件把全流程走通。这个 dummy 组件不需要任何真实业务代码,就是一个带 main 方法的空 jar,groupId 用你申请好的 namespace,版本号0.0.1。整个流程走下来,确认 GPG、staging、release、索引刷新都正常,再回过头发布真正的组件。这样能把签名、校验这些不可控因素提前暴露掉,不至于在业务组件的发布窗口期手忙脚乱。
第二个习惯,把本地手动发布做成 CI 自动发布。发布流程走通后,手动重复执行的收益很低,还很容易因为某一步忘了点 Release 导致组件永远卡在 staging 仓库。我现在的方式是:代码 push 并打上v*格式的 tag 后,CI 里自动触发mvn clean deploy,并且在 CI 环境里配置好 GPG 私钥和 Sonatype 凭据。这样每次发布都是可重复、可追溯的,发布状态也能在 CI 日志里看到。
第三个习惯是发布后立刻更新 README,把坐标示例放进去,并写好版本变更摘要。很多人以为发布的终点是 search.maven.org 能看到组件,其实组件被引用了,使用者的体验才刚开始。README 里给一个最简依赖示例,帮使用者跳过配置学习的成本,比任何宣传都有效。我也习惯在 CHANGELOG 里记录每个版本的变化,这样即使某次发布不小心引入了破坏性变更,使用者也能快速定位并在升级前做好评估。
组件发布的整个流程,本质上是一次面向公共生态的交付。你不仅是在上传一个 jar,更是在建立使用者对这个坐标的信任。把元数据写清楚,把签名做好,把发布流程自动化,后续维护就会变成发布新版本时一个很顺滑的动作。希望这篇文章能帮你少走一些我走过的弯路。