1. 先弄明白:Maven 中央仓库到底解决了什么问题,值不值得折腾
1.1 一次依赖拉取背后,Maven 自己在干哪些事
好多人一上来就搜"maven是干嘛的",我觉得这个问题必须放在开头说清楚。Maven 不是一个单纯的下载器,它管三件事:依赖管理、构建流程、项目信息管理。平时我们改的maven配置文件、执行mvn clean install,核心都是让 Maven 帮你完成编译、测试、打包,并且把依赖从"某个地方"拉到本地。
这个"某个地方"分三级:本地仓库(默认在~/.m2/repository)、私服(公司内部的 Nexus / Artifactory)、中央仓库(repo1.maven.org)。本地仓库是缓存,私服是团队中间层,中央仓库是所有公开构件的最上层源头。比如你现在写:
<dependency> <groupId>org.example</groupId> <artifactId>some-lib</artifactId> <version>1.0.0</version> </dependency>Maven 的查找顺序是:本地仓库 → 配置的镜像/私服 → 中央仓库。这也是为什么很多人配置了阿里云仓库之后下载速度变快——它本质上是中央仓库的加速镜像,把你请求的构件从更近的位置发给你。
那"发布到 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 gnupg,Windows 直接装 Gpg4win,Linux 用apt install gnupg或dnf install gnupg2。确认可用后:
gpg --full-generate-key交互过程中选 RSA and RSA,位数选 4096,有效期我建议 3 年。选永久有效也行,但按安全习惯 3 年更稳妥。邮箱一定要用你能长期收到的,密钥的 UID 和你的账号、项目信息保持一致会更可信。
密钥生成后,先找到它的 Key ID:
gpg --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 | 建议 JDK |
|---|---|---|
| 3.6.x | 1.7 | 8 / 11 |
| 3.8.x | 1.8 | 8 / 11 / 17 |
| 3.9.x | 1.8 | 17(同时兼容 8/11) |
| 4.x | 17 | 17 / 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.xml
3.1 settings.xml 里的用户令牌,以及和阿里云镜像的相处之道
新 Portal 为每个账号生成了 User Token,本质是一对用户名/密码,专门给 Maven 命令上传用。你需要在~/.m2/settings.xml里新增一个 server:
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd"> <servers> <server> <id>central</id> <username>token-username</username> <password>token-password</password> </server> </servers> <mirrors> <mirror> <id>aliyun</id> <mirrorOf>central</mirrorOf> <name>阿里云公共仓库</name> <url>https://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 许可证为例:
<name>your-library</name> <description>A short description of your library.</description> <url>https://github.com/yourname/your-library</url> <licenses> <license> <name>Apache License, Version 2.0</name> <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url> <distribution>repo</distribution> </license> </licenses> <developers> <developer> <name>Your Name</name> <email>you@example.com</email> <url>https://github.com/yourname</url> </developer> </developers> <scm> <connection>scm:git:https://github.com/yourname/your-library.git</connection> <developerConnection>scm:git:git@github.com:yourname/your-library.git</developerConnection> <url>https://github.com/yourname/your-library</url> <tag>HEAD</tag> </scm>name和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> <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.5</version> <executions> <execution> <id>sign-artifacts</id> <phase>verify</phase> <goals> <goal>sign</goal> </goals> </execution> </executions> </plugin> </plugins>maven-gpg-plugin 默认会去找本机的 GPG 密钥。如果服务器上没有交互式终端,推荐在插件配置里指定 passphrase 来源,用环境变量而不是硬编码:
<configuration> <passphrase>${env.GPG_PASSPHRASE}</passphrase> <gpgArguments> <arg>--pinentry-mode</arg> <arg>loopback</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> <groupId>io.github.sonatype.maven</groupId> <artifactId>central-publishing-maven-plugin</artifactId> <version>0.4.0</version> <extensions>true</extensions> <configuration> <publishingServerId>central</publishingServerId> </configuration> </plugin>这里的版本号会继续更新,实际使用前看官方 README 确认最新版本即可。<extensions>true</extensions>是关键,它让这个插件在 Maven 生命周期里生效。配置好之后,直接执行:
mvn clean deploy插件会把构建产物上传到 Central Portal,默认行为是自动发布。如果你希望上传后先停在"待发布"状态,到 Portal 页面人工看一眼再发布,就加上:
<autoPublish>false</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> <id>ossrh</id> <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url> </repository> <snapshotRepository> <id>ossrh</id> <url>https://oss.sonatype.org/content/repositories/snapshots</url> </snapshotRepository> </distributionManagement>然后加上nexus-staging-maven-plugin:
<plugin> <groupId>org.sonatype.plugins</groupId> <artifactId>nexus-staging-maven-plugin</artifactId> <version>1.6.13</version> <extensions>true</extensions> <configuration> <serverId>ossrh</serverId> <nexusUrl>https://oss.sonatype.org/</nexusUrl> <autoReleaseAfterClose>false</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 或者上传前就冒出来。
最简单的做法是禁用 doclint:
<configuration> <doclint>none</doclint> </configuration>放在 maven-javadoc-plugin 的<configuration>里即可。如果你要保留严格校验,那就老老实实把所有 javadoc 注释修到不报警。对发布来说,我更建议先用doclint=none跑通流程,等发布稳定后再逐步开回严格模式。
5.3 服务器上 GPG 签名失败:没有图形界面的坑
如果你在 GitHub Actions 或一台没有图形环境的 Linux 服务器上执行mvn deploy,maven-gpg-plugin 调用 gpg 时,默认会尝试打开 pinentry 弹窗,结果自然是找不到输入终端。这个坑我在 3.3 里给了应对方案:--pinentry-mode loopback加环境变量传口令。
另外还有一个很隐蔽的坑:GPG 密钥的口令如果带有特殊字符,比如$、#、空格,在 CI 的 shell 里会被各种转义,建议环境变量赋值时用单引号包住,比如export GPG_PASSPHRASE='My#Pass',避免踩字符转义的雷。
5.4 上传成功但用户拉不到:别忽略传递依赖和 BOM
发布之后,自己测试通过,但用户拉到项目里却报"找不到类"或"依赖报错"。最常见的原因是你在 pom 里把运行期需要的依赖写成了<scope>provided</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 必须两两对应。新旧教程混用时最常见的问题是设置文件里写了ossrh,pom 新插件里却写的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 工程,走一遍发布流程,再把这个流程固化到自己的项目模板里。