ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Maven 中央仓库发布实战:从 PGP 签名到 mvn deploy 避坑指南

2026/10/7 11:11:27 拓冰建站 浏览量
Maven 中央仓库发布实战:从 PGP 签名到 mvn deploy 避坑指南 1. 先弄明白Maven 中央仓库到底解决了什么问题值不值得折腾1.1 一次依赖拉取背后Maven 自己在干哪些事好多人一上来就搜maven是干嘛的我觉得这个问题必须放在开头说清楚。Maven 不是一个单纯的下载器它管三件事依赖管理、构建流程、项目信息管理。平时我们改的maven配置文件、执行mvn clean install核心都是让 Maven 帮你完成编译、测试、打包并且把依赖从某个地方拉到本地。这个某个地方分三级本地仓库默认在~/.m2/repository、私服公司内部的 Nexus / Artifactory、中央仓库repo1.maven.org。本地仓库是缓存私服是团队中间层中央仓库是所有公开构件的最上层源头。比如你现在写dependency groupIdorg.example/groupId artifactIdsome-lib/artifactId version1.0.0/version /dependencyMaven 的查找顺序是本地仓库 → 配置的镜像/私服 → 中央仓库。这也是为什么很多人配置了阿里云仓库之后下载速度变快——它本质上是中央仓库的加速镜像把你请求的构件从更近的位置发给你。那发布到 Maven 中央仓库是什么概念就是把你打的 JAR以及源码包、Javadoc 包、POM上传到repo1.maven.org背后的正式发布平台让全世界任何配好 Maven 的人只需要写一段 dependency 坐标就能拉到你的代码。这是 Java 生态里最主流的组件分发方式没有之一。1.2 收益和代价劝你先想清楚这一层好处很直观你的开源项目能被别人用三行配置引入团队协作时也不用让每个人都去公司私服拉包。很多人会优先去search.maven.org搜坐标再复制到自己 pom.xml 里这本身就是项目影响力的体现。代价也不能忽略。发布到中央仓库的版本不可删除、不可覆盖一旦传错坐标或者包体有问题只能发一个新版本盖过去。整个流程涉及 Sonatype 账号、域名所有权验证、PGP 密钥签发对第一次操作的人来说稍有不慎就能卡上一整天。如果你只是做公司内部组件或者只想给三五个人用那完全没必要上中央仓库私有 Nexus 就解决了。如果你要做开源库、想积累技术影响力、要让用户无脑使用坐标依赖那中央仓库就是绕不开的一步。这篇指南按照我的实际操作顺序来写尽量让你少走弯路。我默认你已经装好了 Maven 和 JDK如果连环境都没有直接跳去 2.3 节先把工具链对齐。2. 发布前必须核对的清单账号、命名空间、PGP 密钥2.1 在 Central Portal 注册并认证你的 groupId现在的发布入口是central.sonatype.com也就是 Sonatype 官方的 Central Portal。早年间新用户还需要去 JIRA 工单系统填一张 OSSRH 工单人工审批 groupId这套流程如今已经被平台化取代了。网上很多先去 issues.sonatype.org 申请的旧教程现在主要给存量用户用了新用户直接走 Portal。用邮箱或 GitHub 注册登录后找到 Namespace命名空间管理页面添加你需要发布的 groupId。这里会遇到两种常见情况。第一种你有自己的域名比如example.com。那可以申请com.example这个命名空间。Portal 会给你一个唯一的 TXT 校验值你到域名服务商那边加一条形如主机记录 记录类型TXT 记录值xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx加完等 DNS 生效回到 Portal 点验证一般几分钟到几十分钟就能通过。这里的逻辑是你能控制域名的 DNS就说明你是这个组织名下的合法发布者这也是中央仓库防止别人抢注坐标的核心机制。第二种你没有域名但你有 GitHub 账号最常用的是io.github.你的用户名这种命名空间。GitHub 用户名本身是唯一的平台会要求你用对应的 GitHub 账号关联验证认证通过后这个命名空间就归你了。这是个人开发者最常见的路线我自己的几个小工具用的就是io.github.xxx开头。这里有个很关键的提醒命名空间一旦验证通过它就是你这个 groupId 的版图。你发布的所有 artifactId 都必须在这个 groupId 之下。比如验证了io.github.demo那只能发io.github.demo:xxx不能发com.demo:xxx。后期想换 groupId 很麻烦所以第一次申请时想清楚用域名还是 GitHub 命名空间别拍脑袋。2.2 用 GnuPG 生成签名密钥并让公钥能被查得到中央仓库要求所有构件都有 PGP 签名这是硬性校验。签名的作用很朴素防止有人把你发布的 JAR 替换成恶意版本用户下载后可以用公钥验证这个包确实是原作者发布的。这一步最容易卡人我尽量把命令写完整。先确认本机有 GnuPG。macOS 可用brew install gnupgWindows 直接装 Gpg4winLinux 用apt install gnupg或dnf install gnupg2。确认可用后gpg --full-generate-key交互过程中选 RSA and RSA位数选 4096有效期我建议 3 年。选永久有效也行但按安全习惯 3 年更稳妥。邮箱一定要用你能长期收到的密钥的 UID 和你的账号、项目信息保持一致会更可信。密钥生成后先找到它的 Key IDgpg --list-secret-keys --keyid-format long输出里形如sec rsa4096/3F5D9C8E2A1B4C6D的那一段3F5D9C8E2A1B4C6D就是你的 Key ID。后面配置maven-gpg-plugin时会用到最好记下来。公钥分发同样重要。Maven 在验证签名时要去公钥服务器或者你的账号关联信息里找发布者的公钥。现在的新 Portal 通常会在账号的 PGP Public Keys 区域让你直接粘贴 ASCII 格式的公钥同时我也建议把公钥推到公开的 keyserver双保险gpg --armor --export 3F5D9C8E2A1B4C6D gpg --keyserver hkps://keys.openpgp.org --send-keys 3F5D9C8E2A1B4C6D第一条命令会打印大段BEGIN PGP PUBLIC KEY BLOCK把这段内容复制到 Portal 的公钥配置区。第二条是把公钥广播出去。私钥文件在~/.gnupg/private-keys-v1.d目录下平时记得整个.gnupg目录做备份也可以单独导出加密后的私钥gpg --export-secret-keys my-private-key.asc这个备份文件一定要保存到安全的地方丢了私钥意味着你以后没法发布新版本了。2.3 JDK 和 Maven 先对齐这是一条常被忽略的硬规则很多新手第一次发布就踩依赖报错或者deploy 莫名其妙失败回头一看是他本地的 Maven 和 JDK 版本跨度太大。这不是玄学Maven 对 JDK 版本有明确支持范围太老的 Maven 跑在新 JDK 上或者太新的 Maven 配旧 JDK都会出现不可名状的问题。下面这张表是我常用的对齐依据Maven 版本最低 JDK建议 JDK3.6.x1.78 / 113.8.x1.88 / 11 / 173.9.x1.817同时兼容 8/114.x1717 / 21如果你用的是 IDEA正确配置位置是Settings → Build, Execution, Deployment → Build Tools → Maven在这里指定 Maven home、settings 文件、本地仓库路径。IDEA 自带的内嵌 Maven 也能发布但为了环境一致我建议用自己安装的 Maven并把MAVEN_HOME、JAVA_HOME都配上。macOS 上安装 Maven 的常见问题是环境变量不生效记得在~/.zshrc里配置export MAVEN_HOME/opt/homebrew/opt/maven之类的位置然后执行mvn -v验证。maven配置文件的另一个重点是分清全局配置和用户配置全局配置在 Maven 安装目录下的conf/settings.xml用户配置在~/.m2/settings.xml后者的优先级更高。发布组件时应该在用户配置里写服务器凭据而不是动全局文件这样不会影响同一台机器上的其他项目也方便以后改成 CI 场景。3. Maven 侧配置credentials 放 settings.xml发布插件放 pom.xml3.1 settings.xml 里的用户令牌以及和阿里云镜像的相处之道新 Portal 为每个账号生成了 User Token本质是一对用户名/密码专门给 Maven 命令上传用。你需要在~/.m2/settings.xml里新增一个 serversettings xmlnshttp://maven.apache.org/SETTINGS/1.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd servers server idcentral/id usernametoken-username/username passwordtoken-password/password /server /servers mirrors mirror idaliyun/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror /mirrors /settings注意 server 的id要和后面 pom 里发布插件配置的publishingServerId一致否则它连 server 都找不到直接报 401。我用的是central很多人沿用旧教程里写ossrh那也没问题只要两边是同一个 id 就行。更容易被坑的是镜像。mirrorOf配置为central时它只拦截对中央仓库的下载请求不影响 deploy。但如果你在别的电脑上习惯性地写了mirrorOf*/mirrorOf那么mvn deploy时 Maven 会傻乎乎地把构件也部署到阿里云镜像地址给你一个 403 或 400。解决办法有两种把 mirrorOf 改为精确匹配比如mirrorOf*,!central/mirrorOf意思是除了 id 为 central 的仓库其他都走镜像。!排除这个写法是 Maven 镜像语法的保留功能大多数旧文档不提但实际排错时很有用。3.2 pom.xml 必须补齐的元信息这五项是硬门槛中央仓库在进入 Staging 校验时会非常严格地检查 pom.xml 里的元信息。哪怕插件全齐了缺一项也过不了。以最常见的 Apache 2.0 许可证为例nameyour-library/name descriptionA short description of your library./description urlhttps://github.com/yourname/your-library/url licenses license nameApache License, Version 2.0/name urlhttps://www.apache.org/licenses/LICENSE-2.0.txt/url distributionrepo/distribution /license /licenses developers developer nameYour Name/name emailyouexample.com/email urlhttps://github.com/yourname/url /developer /developers scm connectionscm:git:https://github.com/yourname/your-library.git/connection developerConnectionscm:git:gitgithub.com:yourname/your-library.git/developerConnection urlhttps://github.com/yourname/your-library/url tagHEAD/tag /scmname和description是给搜索页面看的url指向项目主页licenses、developers、scm是中央仓库强制校验的字段。有人说随便填一个 license 名就行实际校验会看name和url是否合法最好直接用 Apache 2.0、MIT 这种成熟模板。还有一点parent尽量不要引用了中央仓库里没有的父 POM否则会给用户引入多余的依赖解析链。如果你的项目本身有父工程发布时也不要让子模块依赖的父 POM 变成一个不存在于中央仓库的坐标否则用户解析时就报找不到父 POM。3.3 把 sources、javadoc、gpg 三个插件一次性挂上中央仓库要求每个构件都附带源码包sources.jar、文档包javadoc.jar和 GPG 签名文件.asc。签名是对三个 jar 以及主 jar 都要做的所以下面这段配置最好理解成三件套流水线plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-source-plugin/artifactId version3.3.1/version executions execution idattach-sources/id goals goaljar-no-fork/goal /goals /execution /executions /plugin plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-javadoc-plugin/artifactId version3.6.3/version executions execution idattach-javadocs/id goals goaljar/goal /goals /execution /executions /plugin plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-gpg-plugin/artifactId version3.2.5/version executions execution idsign-artifacts/id phaseverify/phase goals goalsign/goal /goals /execution /executions /plugin /pluginsmaven-gpg-plugin 默认会去找本机的 GPG 密钥。如果服务器上没有交互式终端推荐在插件配置里指定 passphrase 来源用环境变量而不是硬编码configuration passphrase${env.GPG_PASSPHRASE}/passphrase gpgArguments arg--pinentry-mode/arg argloopback/arg /gpgArguments /configuration这样你需要提前执行export GPG_PASSPHRASE你的密钥口令。loopback参数是关键它让 GPG 从 stdin 读口令而不是弹出图形窗口CI 环境里必须有这个参数。Windows 命令行如果遇到找不到 pinentry之类的错也是靠这两个参数绕过。4. 执行 mvn clean deploy两条路线新手我建议走新门户4.1 新门户路线central-publishing-maven-plugin 一把梭2024 年之后Sonatype 主推的 Maven 发布方式是central-publishing-maven-plugin它和上面的 source/javadoc/gpg 插件配合把上传、发布这套流程简化了不少。pom 里加上plugin groupIdio.github.sonatype.maven/groupId artifactIdcentral-publishing-maven-plugin/artifactId version0.4.0/version extensionstrue/extensions configuration publishingServerIdcentral/publishingServerId /configuration /plugin这里的版本号会继续更新实际使用前看官方 README 确认最新版本即可。extensionstrue/extensions是关键它让这个插件在 Maven 生命周期里生效。配置好之后直接执行mvn clean deploy插件会把构建产物上传到 Central Portal默认行为是自动发布。如果你希望上传后先停在待发布状态到 Portal 页面人工看一眼再发布就加上autoPublishfalse/autoPublish然后上传完去 Portal 的 Publishing 页面确认即可。这里我多说一句不要在deploy的时候把 IDEA 的内嵌 Maven 和命令行 Maven 换来换去同一个项目建议固定一个 Maven 实例否则本地缓存的.lastUpdated、_remote.repositories混在一起会出现本地明明有包却一直报依赖找不到的幽灵问题。上传成功后Portal 页面能看到刚上传的构件列表以及签名、元数据等校验状态。等它流转到 Published 状态中央仓库就正式收录了。通常几分钟到一小时后你会发现search.maven.org能搜到到这一步新用户复制坐标就能用了。4.2 经典路线nexus-staging 插件 oss.sonatype.org 手工释放如果你的项目模板是两三年以前写的或者你参考的教程还在用 JIRA 工单流程你八成会遇到oss.sonatype.org这个词。这套流程本质上还是通过 Sonatype 的 Staging 仓库发布只不过入口是老平台。老路线需要在distributionManagement里指定上传地址distributionManagement repository idossrh/id urlhttps://oss.sonatype.org/service/local/staging/deploy/maven2//url /repository snapshotRepository idossrh/id urlhttps://oss.sonatype.org/content/repositories/snapshots/url /snapshotRepository /distributionManagement然后加上nexus-staging-maven-pluginplugin groupIdorg.sonatype.plugins/groupId artifactIdnexus-staging-maven-plugin/artifactId version1.6.13/version extensionstrue/extensions configuration serverIdossrh/serverId nexusUrlhttps://oss.sonatype.org//nexusUrl autoReleaseAfterClosefalse/autoReleaseAfterClose /configuration /plugin流程是mvn clean deploy先上传到 Staging 仓库然后登录oss.sonatype.org在 Staging Repositories 列表里找到刚创建的仓库点击 Close。这一步会触发一系列校验GPG 签名、pom 元数据、sources/javadoc 是否存在、坐标是否与命名空间匹配等。校验通过后点 Release构件才会真正进入中央仓库。这条老路线和 4.1 的新路线并不冲突。手头有维护中的老项目继续走老路线完全没问题但新项目建议直接用新门户省下 JIRA 审核和手工巡检的环节。4.3 发布成功的判定标准不要只看终端没报错BUILD SUCCESS不等于发布成功很多人在这里产生误解。判断成功的硬指标有这几个repo1.maven.org或search.maven.org能搜索到groupId:artifactId:version。直接访问https://repo1.maven.org/maven2/你的groupId路径/你的artifactId/版本号/能看到主 jar、sources.jar、javadoc.jar 以及每个文件对应的.asc签名文件。在你本机的另一个空项目里引用该坐标执行mvn dependency:resolve能成功拉到。尤其第三条是最真实的验收。因为你的本地.m2/repository里可能已经缓存了刚才自己打的包直接在本项目验证没有意义。我一般会开一个新的临时工程或者用-Dmaven.repo.local指向一个临时目录来测确保是真正从中央仓库拉的。5. 发布过程中踩过的坑按出现频率排序5.1 重复版本和 SNAPSHOT两个最容易的一票否决中央仓库的规则是同一个版本只能发布一次不能覆盖、不能删除也不能发布 SNAPSHOT 版本。第一次尝试时很多人会用1.0.0-SNAPSHOT来做试发布结果平台直接拒绝。原因是 SNAPSHOT 代表快照它的上传地址和正式版不是一个通道中央仓库最终只收录固定版本号。如果发布后发现包有严重 bug正确做法是立刻发1.0.1把问题版本撇在身后让所有人改用新版本。我发现不少团队会把不可变版本这回事忘了导致用户拷贝旧坐标装不上这是开源维护里很基础却重要的一课。5.2 Javadoc 严格模式doclint 会让 javadoc.jar 直接构建失败Java 8 之后 javadoc 默认开启 doclint遇到注释里的、、{link}写错、HTML 标签不闭合直接报错整个 build 失败。这个错误在本地通常不显眼一到 CI 或者上传前就冒出来。最简单的做法是禁用 doclintconfiguration doclintnone/doclint /configuration放在 maven-javadoc-plugin 的configuration里即可。如果你要保留严格校验那就老老实实把所有 javadoc 注释修到不报警。对发布来说我更建议先用doclintnone跑通流程等发布稳定后再逐步开回严格模式。5.3 服务器上 GPG 签名失败没有图形界面的坑如果你在 GitHub Actions 或一台没有图形环境的 Linux 服务器上执行mvn deploymaven-gpg-plugin 调用 gpg 时默认会尝试打开 pinentry 弹窗结果自然是找不到输入终端。这个坑我在 3.3 里给了应对方案--pinentry-mode loopback加环境变量传口令。另外还有一个很隐蔽的坑GPG 密钥的口令如果带有特殊字符比如$、#、空格在 CI 的 shell 里会被各种转义建议环境变量赋值时用单引号包住比如export GPG_PASSPHRASEMy#Pass避免踩字符转义的雷。5.4 上传成功但用户拉不到别忽略传递依赖和 BOM发布之后自己测试通过但用户拉到项目里却报找不到类或依赖报错。最常见的原因是你在 pom 里把运行期需要的依赖写成了scopeprovided/scope或者把依赖声明成了开发期才有的 scope导致中央仓库拉下来的 pom 没有把该传递的依赖带过去。另一个典型是只发了一个空壳 POM忘了把 main jar 挂到构件列表。检查标准方法还是那条用空项目跑mvn dependency:resolve然后dependency:tree看传递依赖是否齐全。再不行就把下载下来的 jar 解压看看target/classes里的类是否都在。5.5 一个容易忽略的检查ID 不匹配导致 401设置好settings.xml里的 server id、pom 里的distributionManagement、插件里的publishingServerId三处 id 必须两两对应。新旧教程混用时最常见的问题是设置文件里写了ossrhpom 新插件里却写的central结果上传 401。我自己的习惯是全局统一用central只在维护老项目时改用ossrh并在 pom 注释里写明原因方便几个月后的自己回看。还有一种 401 是 Token 过期。Central Portal 的 User Token 你可以重新生成token 生成后不会自动同步到 settings.xml需要手动粘贴。遇到奇怪鉴权错误时优先去 Portal 生成新 token别跟旧 token 死磕。6. 发布完成后的维护版本策略、本地仓库合并和那些只能接受的事6.1 语义化版本和快照策略开源库的生命线发布成功不是终点。我维护的几个组件基本遵循major.minor.patch新增向后兼容的功能升 minor破坏性变化升 major修 bug 升 patch。中央仓库不可删除的机制逼着你必须认真规划每次 version。如果项目还在剧烈变化期可以只在发布时打正式版不要在中央仓库放 SNAPSHOT因为 SNAPSHOT 本身就不属于中央仓库的正式收录范围。另外每个正式版本发布前我习惯先在本地把整个生命周期跑一遍mvn clean verify确保测试通过mvn install后用一个空项目引用验证最后才mvn clean deploy。别把步骤省成反正能 compile 就发布你会为一次失误付出一个版本号的代价。6.2 版本不可删除这件事要把它当作设计约束来看待很多刚接触中央仓库的人会问传错了能不能撤回答案基本是不能。中央仓库的设计原则是可追溯、不可变所有构件一旦上线就永久存在。这听起来很残酷但也保证了供应链的稳定。你不需要和这个问题对抗只需要接受它然后用版本号迭代来纠正错误。如果你发现某个版本里有敏感信息比如把数据库密码打包进去了那不止是尴尬是实打实的安全问题。这种时候要立即发新版本并标注废弃同时在项目 README 里明确提醒用户升级。中央仓库没有删干净选项我们能做的是让旧版本尽量不被使用。6.3 本地仓库合并、多镜像这类周边问题顺带说两句热词里总有人问我有两个本地仓库 repository怎么合并。我的答案是尽量不合并。本地仓库只是缓存缺什么 Maven 会重新下载。如果你的两台机器或两个目录里存了不同历史构件最省事的办法是选一个作为主仓库路径在 IDEA 的 Maven 配置里把 Local repository 指向它缺失的依赖让 Maven 从镜像重新拉一遍。硬要手动合并就是把一个仓库里的目录复制到另一个然后清理_remote.repositories和*.lastUpdated这类状态文件否则 Maven 可能判定来源不合法而重新下载。阿里云镜像和中央仓库的关系同理镜像解决的是下载快不快发布中央仓库解决的是别人能不能公开下载。下载走镜像上传直连官方两条路径分开对待就不会出现deploy 被打到镜像的怪问题。6.4 最后分享一点维护心得我自己的项目现在保持一个很轻的发布脚本一次mvn clean deploy然后打开 Portal 确认状态。如果不是自动发布模式就手动点一下 publish。这个流程我跑了几十个版本几乎不会翻车。真正让我记住的教训反而是那些一次都没有错的版本旁边藏着的重复版本号、Javadoc 报错、Token 过期这些细节。把这些内容整理成上面这份清单之后我发布新库的信心大了很多。你要是第一次上手建议把这篇里的配置直接复制进一个 Demo 工程走一遍发布流程再把这个流程固化到自己的项目模板里。