JCenter 远程仓库停止维护这件事,说起来已经不算新闻,但直到今天,我打开任何一个老项目或者接手别人的历史代码时,依然能频繁撞见一大串Could not resolve的报错,最后追根溯源,发现是某个传递依赖还死死捆在jcenter()上。如果你现在项目里还留着jcenter()这个声明,或者你经常要维护那些从 Android Studio 早期模板一路传下来的工程,那这篇文章就是给你写的。我会把 JCenter 停维护的来龙去脉、Gradle 依赖解析的原理、以及从配置迁移到问题排查的完整实操都过一遍,争取让你看完之后能直接上手处理。
1. 先搞清楚 JCenter 停服到底停掉了什么
1.1 JCenter 在项目里扮演什么角色
JCenter 本质上是一个面向 Java/JVM 生态的 Maven 仓库,和 Maven Central 一样,托管了大量开源库的 jar、aar 以及对应的 POM 元数据。它在 Android 开发早期非常流行,很多第三方库只在 JCenter 上发布,正因为这个生态惯性,Android Studio 早期的工程模板也曾经把jcenter()写成默认远程仓库。
那个年代的项目打开根目录的build.gradle,几乎必然能看到这么一段:
allprojects { repositories { google() jcenter() mavenCentral() } }这种写法在当年没什么问题,因为构建时 Gradle 会按顺序去这些仓库找依赖,找不到就换下一个。问题在于,JCenter 停止维护之后,这个仓库的定位从“还能用”变成了“只读”甚至“随时可能消失”,而大量老项目依然把jcenter()当成默认依赖来源来使用。你表面上只是在构建,实际上等于把一个已经宣布退役的服务器当作生产依赖源,这就像把公司文件放在一个已经歇业的库房里,平时没人查的时候一切正常,某天突然要拿一份关键合同,才发现库房已经封了。
1.2 停止维护的时间线与真实影响范围
时间线大概是这样的:官方在 2021 年发布公告,明确计划停止 JCenter 的维护,并且很快就不再接受新版本和新包的提交,仓库进入只读状态;之后又经历了多次时间点调整,外界一度以为仓库完全关闭,但最后它以只读的形式继续保留了一段时间。整个过程最大的迷惑点就在这里:它不是某一天零点准时消失,而是“慢慢变得不可靠”。
对使用者的影响就非常具象了:
- 构建时提示某个依赖在
jcenter()中找不到,但明确知道这个依赖以前存在; - 下载依赖时连接超时、返回 403、证书异常,阻塞全部构建流程;
- CI 机器上因为缓存是干净的,第一次拉取就直接失败,而本地开发机因为 Gradle 缓存里已经存了旧依赖,编译反而一直正常;
- 项目里既有
google()又有mavenCentral(),但因为jcenter()还写在仓库列表中间,Gradle 在解析某些库时优先去 JCenter 碰运气,哪怕 Maven Central 里明明有同款坐标,也会被这个“半死不活”的仓库卡住。
这最后一点是最坑的。很多开发者以为只要我没主动用 JCenter 的库就行,但实际上,只要仓库声明还挂在配置里,Gradle 的解析过程中就可能花时间访问它,甚至在部分异常场景下导致解析中断。所以这不仅仅是“把库搬走”,还得把所有指向 JCenter 的配置清理干净。
2. 依赖解析机制和“为什么断”的原理
2.1 Gradle 从远程仓库拉依赖的完整链路
要理解为什么 JCenter 停止维护会造成连锁反应,得先把 Gradle 拉依赖的过程捋一遍。当我们写下implementation 'com.example:example-lib:1.2.3'时,Gradle 会把这个坐标拆成 group、artifact、version 三部分,然后在配置阶段按顺序检查每一个远程仓库。
具体链路是这样的:首先检查本地缓存目录,比如~/.gradle/caches/modules-2/files-2.1下有没有对应坐标的缓存;如果没有,就向仓库发起 HTTP 请求,获取该组件的 POM 文件;POM 文件里写明了这个库自己需要的传递依赖,Gradle 还要继续逐个去解析那些传递依赖;所有依赖都找到之后,再下载真正的 jar 或 aar 到本地。任何一个环节失败,构建就会报错,并且经常把所有失败信息叠加在一起,看起来非常吓人。
所以当 JCenter 已经不可写、部分内容不可读、或者干脆连服务器都不稳定时,它影响的不只是“某个库拿不到”,还包括“依赖解析过程中需要花大量等待时间”和“传递依赖链断裂”。老项目的依赖树往往又深又乱,一个库背后挂着十几个传递依赖,只要其中有任何一个只在 JCenter 上发布,整个构建就会失败。
2.2 仓库优先级和只读模式下的行为差异
仓库顺序在 Gradle 里非常重要。各远程仓库会按照配置的先后顺序被逐个尝试,一旦某个仓库成功返回了 POM,Gradle 通常就不会再询问后面的仓库了。这就解释了为什么google()一般要放在最前面,因为 AndroidX、Google Play services 这类官方库必须从 Google 仓库拿,如果不放最前面,Gradle 可能会从 Maven Central 里解析到一个陈旧或不完整的版本。
而 JCenter 进入只读模式之后,行为变得非常微妙。对于 JCenter 上原本就有的库,它还能正常返回元数据;对于那些根本不在 JCenter 上的库,它会返回 404。问题在于很多配置里没有把仓库区分清楚,Gradle 会先去 JCenter 碰壁,再跑去 Maven Central 找。一次两次没问题,但在网络状况不佳、或者 JCenter 进入不稳定阶段后,这种多余的访问会显著拖慢构建,甚至直接导致解析失败。
更隐蔽的是,如果某个库的 POM 文件里声明了它只存在于 JCenter,那么即使这个库已经发布了新版本到 Maven Central,Gradle 也可能因为 POM 里的依赖声明没有更新,继续去旧的仓库解析老版本。这类问题不是简单换仓库就能解决的,需要配合依赖版本管理来处理。
3. 先做体检:判断项目是否真的依赖 JCenter
3.1 从配置里找到 jcenter() 声明
迁移之前,第一步就是给项目做一次全面体检,把所有jcenter()的引用的找出来。位置通常不只在根目录的build.gradle,还包括:
- 根项目的
build.gradle settings.gradle或settings.gradle.kts- 多模块项目的每个子模块
build.gradle - 通过
buildscript块声明的插件仓库 - 一些公共的
gradle脚本文件
最直接的办法是在项目根目录执行搜索:
grep -rn "jcenter()" --include="*.gradle" --include="*.gradle.kts" .然后逐个文件确认。新版 Gradle 推荐把仓库配置统一收敛到settings.gradle的dependencyResolutionManagement块中,所以搜索到的位置可能更集中。老项目则经常分散在各模块的repositories里,搜出来的结果可能很多,不要怕麻烦,一个一个处理。
找到声明只是第一步,需要留意jcenter()出现的位置。如果是buildscript里的插件仓库,意味着某些 Gradle 插件本身是从 JCenter 下载的;如果出现在allprojects或dependencyResolutionManagement里,影响的是项目所有依赖;如果只出现在某个模块里,就只影响这个模块。处理优先级上,建议先处理allprojects和settings,因为影响面最大。
3.2 用依赖树和构建日志定位“漏网之鱼”
光靠搜索还不够,因为有些传递依赖是间接引入的。你根本不知道哪个库在 POM 里写了jcenter()的仓库地址,或者哪个依赖只在 JCenter 上才有。这时候可以用 Gradle 自带的依赖树分析命令:
./gradlew :app:dependencies --configuration debugRuntimeClasspath这个命令会输出一份非常完整的依赖树,能看见每个直接依赖底下挂着哪些传递依赖。如果某个节点后面标注了仓库来源,或者解析过程提示来自 JCenter,那就很值得关注了。不过依赖树很长,更高效的做法是先尝试构建,然后看报错信息里最先出现的那几个依赖坐标。
构建失败时,Gradle 通常会输出类似:
Could not resolve com.example:example-lib:1.2.3. Could not get resource 'https://jcenter.bintray.com/com/example/example-lib/1.2.3/example-lib-1.2.3.pom'.这时候就能直接看到是哪个依赖、从哪个仓库地址解析失败。拿到这个坐标后,再去查它是否在 Maven Central 上存在替代版本。我习惯的做法是把所有解析失败的坐标统一收集起来,列成一个清单,然后逐个判断是单纯迁移、升级,还是需要重新发布到私有仓库。
4. 迁移到 Maven Central 的完整操作
4.1 替换仓库声明并修正仓库顺序
确认项目依赖范围后,就可以开始动手改配置了。对于绝大多数依赖,只要把jcenter()换成mavenCentral(),问题就能解决大半。新版项目建议统一在settings.gradle里管理仓库:
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() } }如果是老项目还在用allprojects,也要同步修改:
allprojects { repositories { google() mavenCentral() } }这里有一个容易被忽略的点:repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)会禁止项目模块里单独声明仓库,强制统一走中央配置。这个限制对大型项目是件好事,能防止某个模块私自加一个奇怪的仓库地址,但它也会让一些老项目在迁移时直接报错。如果你不想一步到位,可以先设置成PREFER_PROJECT或干脆不加这一行,等所有仓库都收敛到中央配置后再开启严格模式。
另一个重点是把google()放在mavenCentral()前面。Google 官方库在 Android 项目里优先从 Google 仓库解析,能够避免部分库被中央仓库的老版本覆盖。顺序错了,构建通常也能过,但遇到某些版本兼容问题时会特别难排查。
注意:
jcenter()不能想当然地直接删除,一定要先确认所有依赖都能从其他仓库解析到。如果某个依赖只有 JCenter 有,删掉之后会让原本能构建的项目立刻失败,所以迁移之前最好先把依赖清单拉出来核对一遍。
4.2 只在 JCenter 存在的老库怎么办
迁移过程中,最麻烦的其实是那批“只有 JCenter 上有、Maven Central 没有”的库。这类库通常是因为作者早年只往 JCenter 发布,后来项目不维护了,也没同步到中央仓库。遇到这种依赖,有几种处理思路。
第一种是升级替代库。去官方仓库或者开源平台看看这个库是不是已经换新坐标,或者有没有功能相同的后继库。比如某些老库已经停止维护,官方推荐用另一个全新的库替代,这时候不要犹豫,花点时间改代码,比在构建配置里硬扛强得多。
第二种是把老库手动迁移到私有仓库。对于实在找不到替代的库,最稳妥的做法是下载到对应版本的 jar/aar 和 POM 文件,然后上传到企业内部自建的 Maven 私有仓库中。上传时一定要把 POM 一并传上去,因为 POM 里记录了传递依赖,只传 jar 会导致下游项目解析依赖时出错。自建仓库的工具选择很多,诸如 Nexus 一类的开源仓库管理工具就可以胜任,我这里不具体展开安装步骤,反正思路是让私有仓库成为企业内部统一依赖入口。
第三种是短期应急方案,在项目的libs目录下放本地 jar/aar,然后通过flatDir仓库引用。比如:
repositories { flatDir { dirs 'libs' } }这个方案能解决最眼前的构建问题,但副作用很大。flatDir不支持 POM 和传递依赖解析,也拿不到模块元数据,版本管理基本靠自觉,后续维护成本很高。我建议只把它当成临时补丁,不要长期依赖。
4.3 用私有仓库或镜像仓库做平滑过渡
对很多团队来说,一次性把所有依赖从 JCenter 迁到 Maven Central 是有风险的,尤其是项目历史长、依赖数量大、网络环境又不稳定的情况。这时可以分两步走:先让私有仓库接管 JCenter 的“库存”,再把项目仓库配置改成“私有仓库优先 + 中央仓库兜底”。
操作思路是这样的:在私有仓库管理端先配置一个代理仓库,代理源指向 Maven Central,同时把之前从 JCenter 上下载过的所有老依赖手动导入。然后项目的repositories配置改成:
repositories { google() mavenCentral() maven { url "http://你的私服地址/repository/maven-public/" allowInsecureProtocol = true } }如果私服支持 HTTPS,就不需要allowInsecureProtocol = true。这个过渡方式的优势是,所有依赖都从内网拉取,速度更快,也能绕过 JCenter 关闭后带来的各种连接异常。等确认业务代码不再依赖任何 JCenter 的老库之后,再逐步把中央仓库的地址也收敛到私服代理,这样既统一了入口,又方便审计。
5. 实操中常见问题与排查口诀
5.1 解析失败的典型报错与定位方法
迁移过程里会遇到各种各样的报错,我整理一个高频问题对照表,方便你快速定位:
| 报错特征 | 可能原因 | 处理建议 |
|---|---|---|
Could not resolve com.example:lib:1.0.0 | 该坐标在当前仓库中不存在,或只存在于 JCenter | 去 Maven Central 搜索替代版本;无法替代则导入私有仓库 |
Could not GET https://jcenter.bintray.com/... | 仓库声明残留,构建仍在访问 JCenter | 全局搜索并移除jcenter(),替换为mavenCentral() |
Received status code 403 from server | JCenter 或某些镜像拒绝访问,或私服权限校验失败 | 检查仓库地址和凭据;无权限则改用其他可用仓库 |
SSLHandshakeException/ 证书问题 | 私服或镜像的 HTTPS 证书不受信任 | 配置allowInsecureProtocol前先确认网络安全性,必要时升级证书 |
Could not find a matching dependency | 传递依赖只存在于 JCenter | 查看依赖树,锁定传递源,升级或手动指定新版本 |
RepositoriesMode.FAIL_ON_PROJECT_REPOS相关报错 | 子模块里私自声明了仓库 | 把子模块仓库统一迁移到settings.gradle中央配置 |
定位问题是关键。不要被 Gradle 输出的“无法解析所有配置”这种大而全的总结句吓到,往上翻,找到What went wrong后面的核心原因,尤其是Caused by部分。那里才会写出真正失败的依赖坐标和仓库地址。
5.2 缓存、认证、传递依赖的几个坑
我发现很多开发者对 Gradle 缓存有误解,觉得只要报错就是环境问题,跑一遍--refresh-dependencies就好了。实际上,这个参数只会强制刷新动态版本(比如1.+、SNAPSHOT)和 POM 元数据,对于已经缓存的固定版本,它并不会重新下载。如果你的本地缓存里已经有那个依赖,那构建还是会通过,并不能模拟干净环境下的问题。想真正复现 CI 场景,要么删掉~/.gradle/caches对应模块,要么在有隔离环境的工作机器上跑一次。
传递依赖是另一个重灾区。假设 A 库已经迁移到 Maven Central,但 A 的 POM 里还声明依赖 B,而 B 只在 JCenter 上有,那你的项目即使配置了mavenCentral()也会挂。处理方式有几种:
- 如果 B 有新版并已经上传 Maven Central,可以在项目里显式声明
implementation 'com.example:b:2.0.0',Gradle 会优先用较新的显式版本覆盖传递版本; - 如果 B 没有新版,可以用
resolutionStrategy强制指定版本:
configurations.all { resolutionStrategy { force 'com.example:b:1.5.0' } }但前提是你得确保这个版本能从当前仓库解析到,否则越强制越糟。
认证问题也不得不提。如果你迁移之后用私有仓库,而又需要在构建时提交用户名和密码,千万不要直接在build.gradle里写死。正确做法是通过环境变量或gradle.properties读取,把凭据文件加入版本控制忽略列表。这一点我在很多团队审查代码时都看到过,包括把私服密码明文提交到仓库的案例,一旦仓库源码泄漏,内部系统就跟着暴露了。
5.3 迁移后的构建验证清单
配置改完之后,我通常不会直接上生产,而是先执行一套验证步骤:
./gradlew clean ./gradlew :app:assembleDebug如果报错,优先看仓库地址是否已经完全去掉 JCenter;再看构建日志里是否还出现jcenter.bintray.com的字样。确认没问题后,把所有模块的任务都跑一遍:
./gradlew build有条件的再多跑一次:
./gradlew --refresh-dependencies build这样可以确认依赖从新仓库重新解析时也能成功,而不是依赖本地残留缓存。整个过程有点枯燥,但值得做,毕竟构建系统是开发流程的地基,地基不稳,后续什么自动化都白搭。
6. 长期维护经验:从这次迁移里养成的几个习惯
6.1 依赖锁定与统一版本管理
这次迁移给我最大的教训,就是项目依赖不能一家独大,更不能长期挂在一个已经停止维护的仓库上。所有项目都应该尽快把依赖版本管理做起来。Gradle 7.0 以后推荐使用 Version Catalog(版本目录),在gradle/libs.versions.toml里统一管理版本号,让依赖坐标的变更集中在一个文件里,而不是散落各模块。
更进一步,可以启用依赖锁定。Gradle 的lockfile机制能记录已解析的依赖版本和来源,防止未来某个传递依赖悄悄变更导致构建结果不可复现。在项目根目录执行:
./gradlew dependencies --write-locks生成 lockfile 后,每次构建都会校验锁定状态,任何不在锁定列表里的依赖变更都会直接报错。对于需要长期维护的项目,这个功能相当值得开启,等于给依赖上了一份保险。
6.2 定期审计仓库源与依赖健康度
依赖管理不是一次性的事,仓库地址可能因为某个同事的顺手修改又多了奇怪的源,或者某个依赖的官方版本已经迁移到了新的坐标但项目还在用老坐标。我的习惯是写一个小脚本,定期扫一遍所有*.gradle和*.gradle.kts文件里的repositories块,把所有仓库地址输出出来,人工确认一遍,确保里面没有任何已停止维护的旧仓库。
grep -rn "maven {\\|mavenCentral\\|google\\|jcenter" --include="*.gradle" --include="*.gradle.kts" . | grep -v "^Binary"另外,我还会检查依赖树里是否出现一些明显没人维护的老库,用 version catalog 定期更新插件和核心依赖版本。JCenter 停止维护这种事,本质上是一次对工程健康度的大考。那些平时依赖管理规范的项目,迁移时只需要改两行配置;那些长期依赖缓存、凭感觉加依赖的项目,就会被一堆老库拖住很久。
我个人在这轮迁移中最后悔的事情,就是早期没把仓库源当作代码资产来管理。JCenter 还在时,它掩盖了很多依赖相关的坏味道,比如不规范的版本号、缺失的 POM、混乱的传递依赖。当它真正不可靠之后,这些问题才集中暴露出来。所以如果你现在就在处理类似迁移,不用焦虑,把每个依赖都当成一个需要交代的接口来对待,一个坐标一个坐标查清楚来源,会比盲目改配置靠谱得多。走过这轮之后,你的项目反而会变得更干净、更可控,这也是坏事变成好事的地方。