Gradle 7.x仓库收口:repositoriesMode三模式与依赖治理

发布时间:2026/11/3 13:13:18
Gradle 7.x仓库收口:repositoriesMode三模式与依赖治理

经常看到有团队把 Gradle 从 6.x 拉到 7.x,改完 wrapper、跑一次 sync,屏幕上直接糊出来一行红字:Build was configured to prefer settings repositories over project repositories but repository 'maven' was added by build file 'app/build.gradle'。第一反应都是去搜,然后就撞上了repositoriesMode这个以前从来没见过的属性。照着文档抄一行repositoriesMode.set(RepositoriesMode.PREFER_PROJECT),红字没了,人也就走了——过两周依赖解析开始抽风,某个内部构件死活拉不到,又回头怀疑是不是私服挂了。

这篇就把repositoriesMode掰开讲清楚:它在 Gradle 7.0 之后到底扮演什么角色、三个取值各自会把构建带向哪个方向、在多模块和插件满天飞的工程里该选哪个,以及升级过程中那些看着像它的错其实根本不是它的问题。刚上手 Gradle 的同学可以照着把仓库收口做一遍,已经在维护大型多模块工程的老手,建议重点看第三、六节,那里是真正会踩坑的地方。

1. 升级到 7.x 之后,为什么大家突然开始查 repositoriesMode

1.1 一条报错把问题顶到脸上

先看报错本身。Gradle 给出的提示信息其实信息量很足,不同小版本措辞略有差别,但骨架一致,基本都是三段式:

Build was configured to prefer settings repositories over project repositories but repository 'maven' was added by build file 'app/build.gradle'

翻译成人话就是:当前构建被配置成"settings 里的仓库优先",但你在app/build.gradle里又加了一个叫maven的仓库,两件事冲突了。三段分别对应:当前生效的模式、惹事的仓库名、谁加的。

第三段最好用,也最容易被忽略。它可能出现三种值:by build file 'xxx.gradle'——你自己写的;by plugin 'com.xxx.yyy'——某个插件在代码里加的;by settings file 'settings.gradle'——settings 自己加的(这种情况一般是配置写重了)。看到by plugin的时候,你去项目里全文搜索repositories是搜不到的,因为那行代码在插件的 jar 包里。这是排查这类问题的头号障碍,后面第六节会专门说怎么定位。

还有一种是Build was configured to fail if project repositories are declared but ...,这是FAIL_ON_PROJECT_REPOS模式下的措辞,语气更硬,意思是不管你加什么,只要项目级出现仓库声明就直接让构建失败。

1.2 Gradle 7.0 前后到底动了什么

得先把时间线捋清楚,不然容易把锅扣错。集中式仓库声明这块能力不是 7.0 才有的,它是 6.8 引入的孵化特性,dependencyResolutionManagement块连同repositoriesMode一起进来,7.0 正式转正、去掉孵化标记。所以"升级到 7.0 之后才出现这个属性"这个说法不太准确,准确的说法是:7.0 之后这套东西成了官方推荐的写法,大量脚手架和工程模板开始默认往里写,于是普通人第一次撞上它。

与此同时 7.0 还顺手清理了一批陈年旧账,比如compile、runtime这些老配置被彻底移除,仓库url的赋值一律要走uri()包装,url 'https://xxx'这种直接扔字符串的写法在升级时经常报类型转换错误。这些改动和repositoriesMode一点关系都没有,但它们会在同一次升级里集中爆发,混在同一屏报错里,很容易让人误以为是仓库模式在作妖。我见过的典型误判就是:明明报的是url类型错误,人却跑去改repositoriesMode,越改越乱。

还有一个关键事实值得记牢:repositoriesMode的默认值是PREFER_PROJECT,7.0 没有改这个默认值。也就是说,一个干净的老工程原样升到 7.x,不会因为仓库模式而失败。真正失败的那些,多半是工程模板、公司脚手架或者新建工程向导里已经提前写好了PREFER_SETTINGS或FAIL_ON_PROJECT_REPOS,老代码一进来就冲突。搞清楚这一点,排查方向立刻从"Gradle 坑我"变成"谁在我的 settings 里动了手脚"。

1.3 一句话说清它到底管什么

repositoriesMode解决的是一个非常具体的问题:当 settings 文件和项目构建脚本两处都声明了依赖仓库时,听谁的。

它不是一个"合并开关",而是"优先级与准入开关"。这里有个特别容易误解的点:很多人以为两处声明会叠加成一个大列表,实际不是。它是覆盖关系,要么全听 settings 的,要么全听项目的。用公司采购打个比方:PREFER_PROJECT是各部门自己下单,总部那份采购清单作废;PREFER_SETTINGS是只认总部清单,部门自己买的当没看见;FAIL_ON_PROJECT_REPOS是部门敢自己下单,流程直接驳回。

理解了这三句话,后面所有细节都是它们的推论。而之所以要把这个开关当回事,是因为仓库列表一旦失控,带来的不只是"能不能拉到包"的问题,还有构建速度、依赖来源可信度、以及团队之间配置漂移这三笔隐形成本。

2. 三种取值的行为差异、后果与选型

2.1 PREFER_PROJECT:默认值,兼容优先

行为很干脆:只要某个项目里声明了任意一个仓库,settings 里dependencyResolutionManagement.repositories声明的仓库就整份失效,不是逐个比较、也不是取并集。反过来说,如果这个项目一个仓库都没声明,那就老老实实用 settings 的。这个"任意一个就整份失效"的语义是最反直觉的地方,我第一次读文档时也反复确认了两遍。

// settings.gradle dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.PREFER_PROJECT) repositories { mavenCentral() } }

这套模式下,mavenCentral()只在"所有项目都没写仓库"时才生效。只要有一个模块写了repositories { maven { ... } },那个模块就走自己的,其他没写的模块继续用 settings 的——注意粒度是按项目计算的,不是整个工程一刀切。

选它的理由通常只有一个:过渡期。工程里插件太多、模块太杂,一时半会儿收不干净,先用默认值让构建跑起来。代价是仓库治理彻底失效,插件想加什么仓库就加什么仓库,镜像配置、私服策略、离线构建全部形同虚设。

2.2 PREFER_SETTINGS:静默覆盖,最容易被误用

行为是 settings 的仓库优先生效,项目里声明的(包括插件偷偷加的)被忽略掉。注意是"忽略",不是"报错"——构建照常跑,只在部分版本的告警日志里能看到一句不疼不痒的提示。

dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) repositories { mavenCentral() } }

这个模式最大的问题是静默失败。假设某个内部插件依赖一个只在公司私服上才有的构建期依赖,它自己在代码里加了私服地址。你把模式切成PREFER_SETTINGS,这个地址被忽略,构建跑到一半报Could not find com.example:xxx:1.0.0。你打开build.gradle一看,私服地址明明写在里面啊——但那是插件写的,而且已经被忽略了,你在项目里怎么找都找不到线索。

我的建议是把它当止血带用,不当长期方案:先用它把构建从红色拉回绿色,趁着能跑赶紧把缺的仓库补进 settings,然后在 CI 上切到FAIL_ON_PROJECT_REPOS做最终校验。直接拿PREFER_SETTINGS当终态,等于给自己埋一个只有换机器、换网络环境才复现的定时炸弹。

2.3 FAIL_ON_PROJECT_REPOS:强制收口,团队协作首选

行为最硬:只要检测到任何项目级仓库声明——不管是你写的还是插件加的——配置阶段立刻失败,构建根本进不到编译。

dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { maven { url = uri("https://repo.example.com/repository/maven-public/") } mavenCentral() } }

它的价值在于把"团队约定"变成了"构建期强制检查"。口头约定"仓库统一写在 settings 里"这种事,撑不过三个月,尤其是有新人加入、有新模块从别的工程拷进来的时候。而FAIL_ON_PROJECT_REPOS会在第一次 CI 跑起来的时候就告诉你:有人违规了,哪一行,哪个文件。

代价也很实在:改造初期会非常痛。历史工程里少说有十几个地方散落着仓库声明,插件注入的那些更是防不胜防。我的做法是分两步走——先在本地或一个独立分支上开这个模式,把报错一条条消灭;等本地全绿了再合入主干,避免影响其他人的日常开发节奏。

2.4 三种模式对照与选型建议

模式项目声明了仓库settings 声明了仓库构建是否中断典型使用场景
PREFER_PROJECT项目仓库生效,settings 整份失效仅当项目未声明时生效否老工程过渡期、插件生态复杂且短期无法收敛
PREFER_SETTINGS被静默忽略生效否紧急止血、临时统一镜像、排查依赖来源
FAIL_ON_PROJECT_REPOS直接失败生效是新工程、多模块主干、CI 强制校验

选型的判断标准其实就一条:这个工程里还有没有人有正当理由在模块内声明仓库。如果答案是"没有",直接上FAIL_ON_PROJECT_REPOS,别犹豫;如果答案是"有,但我不知道是哪个插件",那先用PREFER_SETTINGS撑住,同时把定位插件这件事排进迭代。PREFER_PROJECT只应该出现在"我刚升完级,先让它跑起来,明天再说"这个时间窗口里,超过一周就是技术债。

3. 别把三块仓库搞混:作用域全景

3.1 dependencyResolutionManagement 只管"项目依赖"

这一块的职责边界必须刻在脑子里:它管的是项目里configurations(比如implementation、api、testImplementation、releaseRuntimeClasspath)解析依赖时用哪份仓库列表。也就是build.gradle里那个裸的repositories { }块,被它接管了。

// build.gradle(项目级) dependencies { implementation 'com.example.core:core-utils:2.3.1' }

上面这个依赖会走dependencyResolutionManagement.repositories里的仓库去解析,repositoriesMode影响的就是它。范围就这么大,不多不少。

3.2 pluginManagement 管"插件",且必须写在最前面

插件解析走的是另一套:pluginManagement.repositories。它和repositoriesMode完全无关,你就算把模式设成FAIL_ON_PROJECT_REPOS,插件仓库该从哪来还是从哪来。而且pluginManagement块必须是 settings 文件的第一个块,前面只能有注释,写在别的地方 Gradle 会直接报错。

// settings.gradle.kts pluginManagement { repositories { maven { url = uri("https://repo.example.com/repository/gradle-plugins/") } gradlePluginPortal() mavenCentral() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { maven { url = uri("https://repo.example.com/repository/maven-public/") } mavenCentral() } } rootProject.name = "demo" include(":app", ":core")

这段是完整可用的骨架,Groovy DSL 版本把include(":app")换成include ':app'、url = uri(...)保持不变即可。注意两个块的仓库列表通常是不同的:插件仓库要放插件门户,依赖仓库不需要。

3.3 buildscript 里的 repositories 完全不受约束

这是踩坑率最高的一条。buildscript { repositories { ... } }解析的是构建脚本自身的 classpath(比如各种 Gradle 插件 jar),它走的是buildscript.repositories,跟dependencyResolutionManagement是两套独立的东西。

// build.gradle buildscript { repositories { maven { url = uri("https://repo.example.com/repository/gradle-plugins/") } } dependencies { classpath 'com.example.tools:build-tools:1.4.0' } }

很多人开了FAIL_ON_PROJECT_REPOS,发现build.gradle里这段居然还能正常跑,于是怀疑模式没生效。其实它本来就管不到这里。真要收口 buildscript 的仓库,只能靠代码评审和自定义检查脚本,repositoriesMode帮不上忙。

3.4 included build 与 buildSrc 各自为政

includeBuild引入的复合构建、以及buildSrc目录,本质上都是独立的构建,各自有自己的 settings。父工程设了FAIL_ON_PROJECT_REPOS,不会自动传递下去,子构建该怎样还怎样。所以在大型工程里,你可能会遇到"主工程明明收口了,buildSrc里还躺着三个仓库"的情况。

处理办法很简单:把buildSrc/settings.gradle(或.kts)也按同样的方式配一遍。别嫌麻烦,buildSrc的依赖解析出问题的时候,报错信息比主工程更难读,因为它连模块名都不会给你。

3.5 作用域对照速查

配置位置影响什么受 repositoriesMode 约束必须写在
pluginManagement.repositories插件解析否settings 文件第一个块
dependencyResolutionManagement.repositories项目依赖解析是(它就是开关本体)settings 文件中任意位置
项目repositories { }项目依赖解析是build.gradle
buildscript.repositories构建脚本 classpath否build.gradle
buildSrc/settings.gradlebuildSrc 自身否(独立构建)buildSrc 目录下

这张表建议直接存进团队文档。仓库解析类的问题,九成都能靠它快速定位到"到底是哪一层出了问题"。

4. 实操:把散落各处的仓库收敛到 settings

4.1 第一步:摸底,把所有声明扫出来

别急着改配置,先搞清楚现状。Linux 或 macOS 下用:

grep -rn --include="*.gradle" --include="*.gradle.kts" -E "repositories\s*\{" .

Windows 的 cmd 环境:

findstr /s /n /c:"repositories" *.gradle *.gradle.kts

第一遍只找repositories {,把文件路径和行号列出来。然后针对每个命中位置再单独确认里面有没有mavenLocal()、flatDir、ivy {、maven { url ... }这些特殊写法。flatDir尤其要注意,它绕开了正常的坐标体系,收敛到 settings 里会出问题,得单独处理或直接干掉。

另外顺手跑一下:

./gradlew buildEnvironment

它会把构建脚本 classpath 的依赖树和解析来源打出来,能帮你发现一些静态搜索漏掉的、由插件动态注入的构建期依赖。

4.2 第二步:改写 settings(Groovy 与 KTS 双版本)

Groovy DSL 版本:

// settings.gradle pluginManagement { repositories { maven { url = uri("https://repo.example.com/repository/gradle-plugins/") } gradlePluginPortal() mavenCentral() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { maven { name = "internal" url = uri("https://repo.example.com/repository/maven-public/") } mavenCentral() } } rootProject.name = "demo" include ':app', ':core'

Kotlin DSL 版本:

// settings.gradle.kts pluginManagement { repositories { maven { url = uri("https://repo.example.com/repository/gradle-plugins/") } gradlePluginPortal() mavenCentral() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { maven { name = "internal" url = uri("https://repo.example.com/repository/maven-public/") } mavenCentral() } } rootProject.name = "demo" include(":app", ":core")

两个细节值得说。一是repositoriesMode.set(...)这种写法在任何版本都稳,Groovy 下直接repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS也能跑,但如果你在多个 Gradle 版本之间来回切,用.set()更省心。二是给私服仓库加个name,构建日志和--info输出里会显示这个名字,比默认的maven、maven2好认一百倍,排查时能省不少时间。

4.3 第三步:清理 build.gradle 里的残留

根工程的build.gradle里通常长这样:

// 改造前 allprojects { repositories { maven { url = uri("https://repo.example.com/repository/maven-public/") } mavenCentral() } }
// 改造后:整段删掉,别留空壳 // allprojects 里只留真正需要的配置,比如统一的编译参数 allprojects { tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' } }

注意别只是把repositories { }里面清空、留下一个空块。空块在FAIL_ON_PROJECT_REPOS下不报错(因为没声明具体仓库),但会给后来人错误暗示:"这里本来是有仓库的,可能以后还要加回来"。直接删干净,配合 git blame 一眼能看到历史。

各子模块的build.gradle同理。这一步用第一步扫出来的清单逐个处理,改完再跑一次 grep 确认没有漏网的。

4.4 第四步:对付"插件偷偷加仓库"

这一步最费时间,但也有章法。流程是这样:

第一,跑一次构建,从报错里把插件 id 抄下来,形如by plugin 'com.example.foo'。

第二,用--stacktrace拿完整堆栈:

./gradlew help --stacktrace

堆栈里能找到插件内部调用repositories.maven { }的具体位置,虽然看不到源码,但类名和方法名足够你确认是哪个插件了。

第三,按优先级做取舍。优先升级插件,新版本大多遵守了集中式仓库的约定,尤其是主流的构建插件,这两年基本都改过了。次选是把它需要的仓库补进 settings,注意补的时候带上content过滤(见第五节),别让一个插件的私服地址污染所有依赖的解析路径。最后才是临时降级到PREFER_SETTINGS,而且要在代码里加注释写清楚原因和移除条件。

有一种做法是在 settings 里用gradle.beforeProject { }钩子把项目仓库清空,我不太推荐。它确实能让构建跑起来,但等于把所有仓库来源都变成了"黑盒",以后谁也不知道某个依赖是从哪拉下来的,出问题时排查成本翻倍。

4.5 第五步:验证收口结果

改完之后别只看 sync 成不成功,跑这几条:

# 强制刷新依赖,确认所有构件都能从 settings 声明的仓库拿到 ./gradlew --refresh-dependencies :app:dependencies --configuration releaseRuntimeClasspath # 检查构建脚本 classpath 有没有意外来源 ./gradlew buildEnvironment # 打开全部告警,看有没有被忽略的弃用提示 ./gradlew help --warning-mode all

--refresh-dependencies这一步很关键。Gradle 有本地依赖缓存(默认在用户目录的.gradle/caches/modules-2下),如果只跑普通构建,很可能全部命中缓存,你以为收口成功了,换台机器直接崩。强制刷新能把真实解析路径完整走一遍。

5. 顺手把仓库治理做扎实:内容过滤与版本目录

5.1 content 过滤:让每个仓库只回答自己该回答的问题

仓库收口之后,下一个问题是:Gradle 会按顺序挨个仓库尝试,没命中就返回 404 再试下一个。仓库越多、顺序越靠后,无谓的网络往返就越多。content块能告诉 Gradle"这个仓库只负责这几组坐标",其他的别来问。

dependencyResolutionManagement { repositories { maven { name = "internal" url = uri("https://repo.example.com/repository/internal/") content { includeGroup("com.example.internal") includeGroupByRegex("com\\.example\\.internal\\..*") } } maven { name = "public" url = uri("https://repo.example.com/repository/maven-public/") content { excludeGroup("com.example.internal") } } mavenCentral() } }

includeGroupByRegex里的反斜杠要写双份,这是 Kotlin 字符串转义的锅,第一次写基本都会错一遍。excludeGroup的价值在于防止同名构件从公共仓库误拉——内部构件如果恰好和公共坐标撞名,那就不只是慢的问题了,可能直接拉到错误的版本。

5.2 exclusiveContent:锁死某个坐标只能从指定仓库拿

content是"建议",exclusiveContent是"规定"。被它圈定的 group,Gradle 只会去指定的那个仓库找,别处一律不看。

dependencyResolutionManagement { repositories { exclusiveContent { forRepository { maven { name = "internal" url = uri("https://repo.example.com/repository/internal/") } } filter { includeGroup("com.example.internal") } } mavenCentral() } }

这段配置的意义在于供应链层面的确定性。内部构件必须在内部仓库解析,这条规则如果只写在文档里,早晚会有人为了图快加个mavenLocal()绕过去。写成exclusiveContent,绕不过去。

5.3 mavenContent 与 releasesOnly:别让快照版本污染发布流程

还有一个很实用的小配置,用来限制仓库只提供正式版或只提供快照版:

maven { name = "internal-releases" url = uri("https://repo.example.com/repository/releases/") mavenContent { releasesOnly() } } maven { name = "internal-snapshots" url = uri("https://repo.example.com/repository/snapshots/") mavenContent { snapshotsOnly() } }

不这么分的时候,Gradle 会在 release 仓库里也去查-SNAPSHOT版本,白白多一次 404。多模块工程里快照依赖又多,累积起来相当可观。这属于典型的"配置五分钟,长期受益"。

5.4 把版本也一起收口:versionCatalogs

仓库收口解决的是"从哪拿",版本目录解决的是"拿哪个版本"。Gradle 7.0 同期引入的版本目录,正好可以和仓库治理配套使用。

# gradle/libs.versions.toml [versions] core = "2.3.1" [libraries] core-utils = { module = "com.example.core:core-utils", version.ref = "core" }
// settings.gradle.kts dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { maven { url = uri("https://repo.example.com/repository/maven-public/") } mavenCentral() } versionCatalogs { create("libs") { from(files("gradle/libs.versions.toml")) } } }

放在dependencyResolutionManagement里的好处是:仓库策略、版本策略、仓库准入模式三件事集中在同一个小文件里,评审时一眼能看完。分散在十几个build.gradle里的时代该过去了。

5.5 收口带来的收益,给个量级估算

不用太精确,算个大概就有体感。假设一个 20 模块的工程,去重后有 120 个外部依赖,改造前每个模块各自声明 3 个仓库,改造后 settings 里配 2 个仓库。

阶段仓库声明位置首次解析请求量级说明
改造前20 个模块 × 3 仓库约 360 次元数据请求按依赖数 × 仓库数粗略估算,未命中也要走一次往返
改造后settings 统一 2 仓库约 120 至 240 次命中率提升后多数依赖一次命中
加 content 过滤同上约 120 至 150 次私服不再被问公共依赖,少掉大量无效探测

这组数字是量级估算,实际值受缓存命中、网络 RTT、仓库响应速度影响很大,别拿去当性能报告。但方向是明确的:仓库数量减半,请求量就接近减半,CI 上每次构建省下的几十秒,一年下来很客观。

6. 常见问题与排查实录

6.1 报错指向 build.gradle,但那行根本不是 repositories

这是最典型的一种。报错说was added by build file 'app/build.gradle',你打开对应行号一看,是一行apply plugin: 'com.example.foo',压根没有仓库声明。

原因很直接:插件在apply的时候执行了自己的配置代码,里面调用了project.repositories.maven { }。Gradle 把责任归到了"触发点"所在的构建文件上,而不是实际执行加仓库动作的插件代码。

定位方式就是前面说的--stacktrace,堆栈会指向插件内部的类。另外--info级别日志也会打出仓库被添加的时机,配合时间线能判断是哪个插件在配置阶段动的手。找到之后按 4.4 节的取舍流程处理。

6.2 切到 FAIL_ON_PROJECT_REPOS 之后开始大量 Could not find

这种"改完模式就跑不动"的情况,八成是 settings 的仓库列表不全。改造前的仓库分散在十几个文件里,你未必全抄过来了。

先用dependencyInsight精确看某个依赖的解析路径:

./gradlew :app:dependencyInsight \ --dependency core-utils \ --configuration releaseRuntimeClasspath \ --info

输出里会列出 Gradle 尝试过的仓库以及每个仓库的响应。如果某个仓库压根没出现在尝试列表里,说明它没进 settings;如果出现了但报 404,说明坐标或路径不对,可能是私服的 group 路径拼接规则和公共仓库不一样。

还有一种隐蔽情况:仓库确实在 settings 里,但被content过滤挡住了。这时候日志里会显示"仓库被跳过"之类的信息,--info下能看到。加了过滤之后一定要回头验证一遍所有依赖,别只验证主模块。

6.3 mavenLocal 带来的"本地能跑、CI 挂"

mavenLocal()会把本机 Maven 本地仓库加进解析范围。它的特点是:解析出来的构件完全不做校验,而且优先级取决于声明位置。开发机上因为之前手动install过某个内部包,构建顺畅;CI 上没有这个本地仓库,直接找不到。

我的处理原则是:mavenLocal()不写进 settings 的公共仓库列表,只在确实需要本地联调时,通过开发者自己的初始化脚本临时加,用完就撤。写进 settings 里当常驻配置,早晚会变成团队级的偶发故障源。这条经验是用两次深夜排查换来的,不太好受。

6.4 混合工程里"改了又被覆盖"

有些跨端或移动端工程,构建脚本是由工具链自动生成的。你手动删掉repositories { },下次同步一跑,它又回来了,FAIL_ON_PROJECT_REPOS继续报错,形成死循环。

应对方式分两层:短期用PREFER_SETTINGS兜住,让构建先能跑;同时去找生成源头,把工具链版本升上去。这类工具链在新版本里基本都改成了往 settings 写仓库,升级路径通常是通的。实在升不上去,就在 CI 上单独跑一个检查脚本,比对生成文件是否被改动,把问题显式暴露出来而不是让它静默存在。

6.5 改了仓库配置但不生效

检查缓存。Gradle 的元数据缓存放在用户目录的.gradle/caches/modules-2下面,仓库地址变了但缓存没失效的话,可能继续用旧结果。先用--refresh-dependencies跑一次,多数情况能解决。还是不行的话,再考虑清掉对应的缓存目录,但别养成"一有问题就删缓存"的习惯——那会掩盖真正的问题。

6.6 常见问题速查表

现象大概率原因处理方向
报错指向 build.gradle 但该行无仓库声明插件在 apply 时注入仓库--stacktrace定位插件,升级或补仓库
切模式后出现 Could not findsettings 仓库列表不全或被 content 挡了dependencyInsight --info看尝试路径
buildscript 里的仓库依然生效repositoriesMode不管 buildscript靠代码评审和自定义检查收口
buildSrc 里还有仓库声明独立构建,配置不继承单独给 buildSrc 配 settings
本地正常 CI 失败依赖了mavenLocal()的产物移除常驻的 mavenLocal 声明
改了配置无变化元数据缓存未失效--refresh-dependencies,再考虑清缓存
手改的配置反复被还原工具链自动生成构建脚本升级工具链或在 CI 加检查

7. 几点实践体会

新起一个工程,我从第一天就把repositoriesMode设成FAIL_ON_PROJECT_REPOS。这个阶段成本最低,因为还没人往里扔东西,边立规矩边开发,比半年后再来治理轻松十倍不止。等工程铺开了、十几个模块、一堆插件,再想收口就是一场硬仗。

老工程改造,我一般分两个迭代。第一个迭代切PREFER_SETTINGS,同时把 CI 上的--warning-mode all打开,让所有被忽略的仓库声明都暴露在日志里,一条条抄进 settings。第二个迭代再切FAIL_ON_PROJECT_REPOS,这时候基本只剩插件注入的那几个钉子户要处理。中间这两三周别省,省了就是给自己挖坑。

最后一个小技巧:把"仓库声明只允许出现在 settings"这条规则写成 CI 的一个 grep 检查,跑在构建之前。几十行脚本的事,比任何文档和口头约定都管用。人都会忘,脚本不会。