简介这套Gradle依赖管理资料面向Android开发者与Gradle构建初学者聚焦依赖版本统一、多模块工程构建及常用命令行操作。资源以GradleDependencyManagement项目为主线整理了多份Gradle依赖配置脚本并梳理一组Gradle常用命令覆盖assemble、check、clean、build及Debug/Release打包等场景便于读者对照理解依赖管理与构建流程。包内另有bat批处理脚本、properties属性文件、xml配置、Java示例与jar文件配合截图与说明文档可辅助快速掌握目录结构和配置要点。从依赖版本声明到构建脚本组织从常用命令到批处理封装内容覆盖工程化实践中的关键环节适合在团队中推行统一依赖约定。资源共34个文件压缩包仅100KB轻量实用已有3656人学习下载适合需要规范依赖管理、优化Android构建流程的开发人员。1. GradleDependencyManagement先想清楚“统一管理”到底要统一什么我在一个多模块项目里做过一次“彻底统一”的依赖治理把所有版本号抽到一个地方删掉模块里的重复声明结果重新构建时项目反而更慢了——因为不同模块之间对同一个传递依赖的版本要求打架解析器来来回回做冲突消解。那一刻我才意识到GradleDependencyManagement 这件事真正要治理的不是“版本号写在哪”而是“依赖坐标系里谁说了算”。依赖的统一管理不是把一堆坐标拎出来晒在一起而是定义一套规则让版本来源、仓库来源、传递策略和升级方式都只有一个入口。这篇文章适合正在被多模块依赖散落、升级炸版本、仓库解析混乱折磨的开发者目标是在半天内搭出一套自己项目能落地的依赖管理方案并且避开那些只有踩过才记得住的坑。2. 为什么依赖需要统一管理失控的三个根源与选型底层逻辑2.1 依赖失控的三个根源版本漂移、传递冲突、仓库混乱先描述一个非常常见的场景一个项目有十几个子模块每个模块各自的 build 文件里都写着implementation xxx:yyy:1.2.3。表面上看只是重复实际上埋着三颗雷。第一颗雷叫版本漂移。A 模块升级了公共库到 2.0B 模块没更新还在用 1.x。编译时因为 Gradle 默认选择最高版本B 模块可能被迫用上 2.0 的类但它的代码还按 1.x 的 API 写于是运行期出现 AbstractMethodError 或 NoSuchMethodError。这类问题在单元测试里往往发现不了等打包出来到联调环境才炸排查成本极高。第二颗雷是传递冲突。你直接依赖的库 A 传递依赖库 C 的 1.0 版本另一个库 B 传递依赖 C 的 2.0 版本。Gradle 会选一个“看起来合理”的版本但合理不等于正确。更麻烦的是同一个 C 库在两个传递路径里被不同模块各自排重最后打出来的包里可能出现两个不同版本的 C 的类。第三颗雷是仓库混乱。有的模块走公司内网镜像有的模块直接访问中央仓库还有的模块为了某个特殊构件临时加了一个第三方仓库。仓库一多解析顺序就变成玄学同样的坐标不同机器上解析出来的构件可能都不一样。仓库混乱造成的后果比版本漂移更隐蔽因为它不会报错只会让你莫名其妙地等上一个小时然后构建失败。这三类问题有一个共同特征单看某一个模块都是正确的只有站在全项目视角才会发现问题。而“全项目视角”恰恰是 Gradle 官方自己不给你提供的——默认的 build 脚本是模块自治的依赖声明默认就是分散的。所以统一管理这件事本质上不是 Gradle 的默认能力而是使用者的治理手段。2.2 三种统一方案的对比外置脚本、buildSrc 模块、独立插件模组聊实现之前先看当前从业者常用且被验证过的三条路线。第一类方案是把依赖信息统一写到根项目的ext块或一个单独的.gradle文件里其他模块通过引用这个文件获取坐标。这类方案的优点是上手快不用额外建模块几分钟就能做完。缺点是它只是一个“变量集合”并没有类型安全也不参与 Gradle 的依赖解析模型。你写错一个版本号编译器不会拦你要等运行时才暴露。第二类方案是用buildSrc模块在项目根目录下建一个名为buildSrc的普通 Gradle 模块所有依赖坐标以 Kotlin 或 Groovy 代码的形式定义成常量对象其他模块在 build 脚本里能直接访问这些常量。这类方案是目前我见过最多的落地形态原因有两个它是 Gradle 自身构建的一部分任何模块都能直接引用不需要发布到任何地方编译期就有类型检查方法名和参数类型写错了当场报错。第三类方案是把依赖管理逻辑打包成一个独立的 Gradle 插件模组通过插件的形式给多个项目复用。这个方案适合公司里有多个互不相关的工程、想统一维护一套依赖基线的情况代价是复杂度和前期投入明显高至少要处理插件发布的完整链路。三者的取舍本质是想要多低的进入成本和多高的可控性。个人项目和两三个模块的小工程用第一类没什么问题一旦模块超过五六个我一般会直接上第二类需要跨工程复用、有统一版本基线诉求的才考虑第三类。本文后面的实现步骤以第二类为主因为它最贴近“Gradle 依赖统一管理”这个标题的核心诉求也最容易在一天内跑通。2.3 统一管理的基本盘依赖分组与版本区隔方案选定之后下一个要做的是设计“容器结构”。我见过很多失败的统一管理项目不是技术不会而是把几十个依赖平铺在一个文件里Long 到让人不想看。依赖统一管理的可维护性主要取决于分组方式是否合理。实践中常用做法是先把依赖按使用目的分组基础工具类、网络请求类、序列化类、测试类、可选的功能类。每组对应一个独立对象对象内部再按库名定义属性。这种分组方式解决的是“查找成本”的问题——你改一个依赖时能快速定位到它也能看到同组里有哪些相关依赖。另一个重要概念是版本区隔。同一个库可能同时出现在 compile 类路径和 test 类路径上而且版本要求不同因此统一管理时不能只存一个版本号。要区分“编译期最低版本”“测试期专用版本”。“统一”的正确含义是每一类用途都有唯一入口而不是所有场景强行压成一个版本。基础工具类里我还会单独列一个“平台基准”组用来放置某框架提供的基础依赖约束清单也就是 BOM 类坐标避免各模块手动声明每个组件的版本这部分后面会具体展开。3. 用 buildSrc 搭建一套最小可用依赖管理模块3.1 为什么要选 buildSrc类型安全与项目内可见性说一个我踩过的反直觉经历。早期我做统一管理用的是根项目里一个deps.gradle文件所有模块在脚本开头写一行apply from: rootProject.file(deps.gradle)。它跑得很顺直到某次我把junit版本升级不小心把一个变量名拼错了。构建全程没有报错直到测试报告显示用例全部没有执行——因为这个变量名解析出来是另一个库我写进了testImplementation配置里。buildSrc 方案能消除这类问题因为它把依赖坐标变成真实代码。你在 buildSrc 里写一个 Kotlin 对象属性名和类型都由编译器检查拼错变量名在编译阶段就会报错。另一个好处是buildSrc 构建结果会自动被你项目里的每个子模块识别子模块的 build 脚本里不需要任何额外的 include 或者 apply 操作天然可见。这套机制还有一个容易被忽略的价值Gradle 会像处理普通构建一样对这个模块做增量编译。依赖统一管理逻辑如果写得比较细buildSrc 的改动频率不会太高它的增量缓存能让配置阶段的性能影响基本可忽略。3.2 初始化 buildSrc 目录结构与构建配置文件先做最小化第一步在工程根目录下创建buildSrc目录里面放一个独立的构建脚本。这一步不需要修改你的根settings.gradle也不用额外 include。// buildSrc/build.gradle.kts plugins { kotlin-dsl } repositories { gradlePluginPortal() }这段配置里有两个关键点。第一kotlin-dsl插件让这个模块能以 Kotlin 写辅助代码并自动生成对项目其他模块的依赖访问点。第二repositories 里声明gradlePluginPortal()是因为 buildSrc 自身也需要解析依赖它解析的是构建逻辑的依赖不是项目运行时的依赖所以跟你的项目仓库是两套。逻辑说明这个文件本质上定义了一个“用于构建项目本身的模块”它生产出来的是一个插件不是普通 Jar。参数说明如果构建环境访问插件门户不通畅可以将仓库替换成你自己内网已同步的镜像源但要注意这不是项目组件的解析路径。接下来在buildSrc/src/main/kotlin/目录下创建包结构比如用com.example.deps。注意包名只是代码组织方式不会影响项目模块访问这些常量。3.3 定义依赖分组常量对象从字符串到类型安全核心文件是用来声明依赖坐标的 Kotlin 对象。日常维护时我们只需要改这个文件里的字符串常量即可。// buildSrc/src/main/kotlin/com/example/deps/Libs.kt package com.example.deps object Versions { const val JUNIT 5.9.2 const val OKHTTP 4.11.0 const val JACKSON 2.15.2 } object Libs { object Test { const val JUNIT org.junit.jupiter:junit-jupiter:${Versions.JUNIT} } object Network { const val OKHTTP com.squareup.okhttp3:okhttp:${Versions.OKHTTP} const val OKHTTP_MOCK com.squareup.okhttp3:mockwebserver:${Versions.OKHTTP} } object Json { const val JACKSON_DATABIND com.fasterxml.jackson.core:jackson-databind:${Versions.JACKSON} } object BOMs { const val JACKSON_BOM com.fasterxml.jackson:jackson-bom:${Versions.JACKSON} } }逻辑说明这里做了一个关键设计——Versions单独成对象每个库的版本号只出现一次Libs下面的每个子对象按用途分组。同组内若有多个库共享同一个版本统一引用Versions里的同一个常量版本升级时只需改一处。参数说明坐标字符串的格式必须是标准的 group:artifact:version不要在这里拼接路径或添加多余字符。const val是编译期常量优点是访问时无对象创建开销但要注意它必须是编译期可确定的字符串不能引用非 const 变量。这套代码写完buildSrc 还没有展示它的真正价值。它的意义在子模块里体现。3.4 在子模块中引用统一依赖并去掉硬编码坐标子模块的 build 脚本需要做的事情是从原来的“写完整坐标”改变成“引用常量对象”。以 Java 模块为例// app/build.gradle.kts import com.example.deps.Libs plugins { java } dependencies { testImplementation(Libs.Test.JUNIT) implementation(Libs.Network.OKHTTP) implementation(Libs.Json.JACKSON_DATABIND) testImplementation(Libs.Network.OKHTTP_MOCK) }逻辑说明所有依赖现在只有一个来源——Libs对象。你不可能再写错版本号因为版本号不存在于本模块里。参数说明implementation和testImplementation的配置名称保持 Gradle 原有语义不变需要api暴露给下游模块时直接写成api(Libs.Json.JACKSON_DATABIND)即可。子模块不再需要任何额外的 import 之外的配置。需要提醒的是buildSrc 中的常量代表的是“直接声明的依赖坐标”它不会自动约束传递依赖的版本传递依赖冲突的治理要依靠后面提到的策略和检查手段。这一步完成你已经拥有一套最基础的 GradleDependencyManagement 了。但别急着庆祝接下来要处理的是从“单模块依赖替换”到“全项目统一生效”之间的那几个容易翻车的细节。4. 从单模块到全项目多模块进阶与自定义约定插件4.1 多模块统一依赖时暴露的新问题同步、插件版本与配置时机当项目有十几个模块都改为引用Libs后会出现几个初看不明显、实际很影响体验的问题。第一个问题是配置同步新增一个依赖时必须先改 buildSrc再改目标模块IDE 要重新同步一次否则光标处标红。这不是 bug而是 buildSrc 的固有行为——它本身也是一个构建模块。习惯上我会建议把“先改 Libs 再改消费方”固化成流程并且在代码评审时检查有没有人绕过 Libs 直接写坐标如果有合并请求直接打回。第二个问题是插件版本。统一管理如果只统一了库依赖而忽略 Gradle 插件版本等于留了一个不稳定的口子。插件版本散落在各模块时同一个插件在不同模块用不同版本Gradle 会取最高值但插件行为往往跟版本强相关甚至不同小版本之间的配置 DSL 会变。所以我一般建议把插件版本也放进统一管理对象中。第三个问题是配置时机。依赖统一管理最好在配置阶段早期就完成解析不要在任务执行阶段动态拼接依赖坐标。Gradle 的配置阶段和执行阶段分开如果你在task的 doLast 里才去解析一个依赖坐标所有依赖相关的配置比如 resolutionStrategy可能已经来不及生效。常见做法是依赖统一管理的常量全部在配置阶段被各模块引用避免跨阶段传值。4.2 把共用的依赖配置收敛成自定义约定插件多模块里经常遇到“每个模块都要加日志库”“每个模块的测试要实现同一个测试框架”这种重复声明。此时依赖统一管理只能让版本一致还不能消除重复。要消除重复需要自定义约定插件convention plugin。约定插件本质是一个只在项目内存在的小插件各个模块通过它来统一获得依赖和应用插件。在 buildSrc 里加一个 Kotlin 文件定义一个插件扩展// buildSrc/src/main/kotlin/com/example/conventions/BaseConventionPlugin.kt package com.example.conventions import com.example.deps.Libs import com.example.deps.PluginVersions import org.gradle.api.Plugin import org.gradle.api.Project class BaseConventionPlugin : PluginProject { override fun apply(target: Project) { with(target) { // 给所有模块统一应用 Java 插件 plugins.apply(java) // 统一代码仓库入口 repositories { mavenCentral() } dependencies { // 所有模块自动获得日志实现子模块无需重复声明 implementation(Libs.Logging.LOGBACK) } } } }逻辑说明这个插件做的事情有两个层面。第一层是应用 Java 插件和设置统一仓库避免每个模块重复写 repositories第二层是通过dependencies容器给所有模块注入一组“公共依赖”。参数说明注意这里implementation(Libs.Logging.LOGBACK)的引号语法在 Kotlin DSL 里配置容器时需要将配置名作为字符串传给容器的invoke操作符。公共依赖里的库如果将来不需要了从这个文件的 dependencies 块里删掉即可所有模块同时生效。然后在各模块的构建脚本里应用这个约定插件// service-impl/build.gradle.kts plugins { id(com.example.conventions.base) }这样 service-impl 模块不再需要自己写 java 插件、仓库和日志依赖它只需要写自己独有的依赖。公共逻辑收敛到一个文件后新增模块的成本显著下降一致性却提高了。4.3 依赖分组命名规范从“能用”到“好维护”统一管理的下一步是给依赖的命名定规矩否则用不了两个月Libs对象就会膨胀成一个没人敢动的“遗产类”。我采用的分组规则是按功能域划分不按模块划分。具体来说同一个功能域里的所有库放一个子对象哪怕它被多个模块使用。这样做的原因是依赖的功能域相对稳定而模块边界会随着项目重构不断变化。比如网络层库不管被哪个业务模块用都放在Libs.Network里以后想统一替换 HTTP 库时只改这一个位置即可。命名的第二层约定是版本常量对象的粒度。版本对象采用“库名全称拼音规则”不用缩写避免OK到底是 okhttp 还是 okio 的歧义。第三层约定是为了保护代码评审的“可读性”禁止在子模块 build 脚本里出现魔法字符串坐标凡是依赖必须从Libs引用。这三点做齐之后“统一管理”才具备了长线维护的基础——规则比工具本身更能决定治理成效。5. 依赖统一管理避坑指南五个翻车现场与处理办法5.1 问题一buildSrc 改完所有模块配置阶段整体失效现象只在Libs里加了一个常量重新构建时所有模块的配置阶段全部重跑耗时从十几秒暴增到一分多钟并且 IDE 频繁提示需要重新同步。原因buildSrc 本身就是构建的一部分它发生变化时Gradle 会认为整个构建链路的输入都变了于是强制所有模块重新走配置阶段。这不算错误但高频更新 buildSrc 会显著拖慢开发节奏。解决把依赖声明代码和构建逻辑代码分开。Libs.kt这种纯常量文件放到一个稳定目录日常只改这个文件自定义约定插件中复杂的构建逻辑不要频繁调整。如果改动确实很频繁考虑把版本目录方案version catalog作为运行期配置替代 buildSrc 的一部分它可以做到不重新配置所有模块。但注意版本目录是针对“配置数据”的一部分优化复杂约定逻辑仍然要放插件。5.2 问题二统一管理后依赖版本冲突反而更隐蔽了现象以前每个模块写完整坐标冲突时错误信息会指明是哪两个模块引入的替换成Libs统一引用后错误信息里只显示传递依赖路径看不出来是谁直接声明的排查反而更难。原因统一管理让直接声明变得一致但传递依赖的冲突依然存在而且因为直接声明高度统一出问题的往往是更深层的传递路径。解决不要把统一管理和依赖冲突治理混为一谈。单独维护依赖分析手段遇到冲突时用gradle dependencyInsight查指定库的完整来源路径。# 在模块目录下执行查询 okhttp 的版本决策路径 gradle dependencyInsight --dependency okhttp --configuration runtimeClasspath逻辑说明这条命令输出该配置里 okhttp 的所有出现路径以及 Gradle 最终选择了哪个版本以及选择原因。参数说明--configuration必须指定否则命令会报错通常优先查runtimeClasspath因为它包含运行时真正会用到的传递集合。结合resolutionStrategy里的force或strictly统一管控出口时优先在依赖统一管理模块里做不要散落到各子模块。5.3 问题三仓库声明顺序导致的“解析到旧包”现象统一管理落地后发现某些库从公司内网镜像解析到的构件总是比中央仓库旧几个月。更诡异的是换台机器构建结果可能就变了。原因Gradle 的仓库解析顺序默认是按照仓库声明顺序来的。如果统一管理模块里先声明了内网镜像再声明中央仓库那么所有依赖都会优先尝试镜像只有镜像缺失时才去中央仓库补齐。而镜像同步可能滞后或忽略某些坐标。解决凡是支持按版本策略过滤的场景优先把中央仓库或者权威仓库放在前面内网镜像作为加速缓存放在后面也可以在统一仓库配置里对镜像开启“只用于已存在构件”的策略避免它抢先返回错误版本。我一般会在统一仓库配置处打印解析仓库列表用gradle buildEnvironment验证顺序是否符合预期。5.4 问题四使用动态版本号导致统一管理形同虚设现象某同事为了“自动获取最新补丁”把版本声明写成1.或latest.release。统一管理后版本号被收进了Libs但动态版本仍然存在导致每次构建依赖解析都会发起网络请求版本随时可能漂移最终锁定的版本无法复现。原因动态版本和固定版本在 Gradle 的版本选择机制中优先级不同动态声明会让缓存和解析行为变得不确定也让“统一管理”失去了它最重要的可复现性。解决统一管理模块里禁用动态版本全部使用精确版本号如果需要定期升级主动改Versions中的常量而不要用通配符。在 repositories 的配置里加上内容缓存策略对动态版本设置较短的缓存超时这样即使历史遗留了动态版本也能尽快暴露而不是默默缓存。5.5 问题五配置缓存开启后统一管理常量“消失了”现象项目开启 Gradle 配置缓存configuration cache后构造阶段正常但执行阶段访问Libs的某个常量时抛异常提示无法从序列化后的状态中找到对应类。原因配置缓存会序列化构建配置但依赖统一管理模块里的 Kotlin 对象如果携带了不可序列化的引用或者在某段逻辑里被闭包捕获缓存恢复时就拿不到原始对象。常见于自定义任务里直接引用了Libs。解决约定一个边界Libs常量只能在依赖配置块及其直接相关逻辑中使用不要在任务执行代码里捕获。把任务执行阶段需要的依赖坐标作为参数传入任务属性而不引用外部常量。这样既满足配置缓存的隔离要求又能保住统一管理的入口地位。6. 让“统一管理”真正可靠自检命令与一个版本约束验证技巧依赖统一管理方案搭好之后系统不会自己告诉你治理是否生效。我养成的习惯是每次重构依赖相关配置后跑一遍自检清单而不是只追求“构建通过”。下面这个是优先级最高的验证动作。# 1. 确认所有模块对同一依赖的版本选择完全一致 gradle dependencies --configuration runtimeClasspath | grep 项目名:依赖名 -A 5 # 2. 检查是否还有模块直接声明了依赖坐标 grep -r implementation.*: --include*.gradle.kts --include*.gradle . | grep -v Libs第一段逻辑说明dependencies报告能暴露一个模块里所有直接依赖和传递依赖的最终版本。检查同一依赖在不同模块的输出是否一致比看代码更直观。第二段逻辑说明直接搜索构建脚本里的冒号坐标找出那些绕过统一管理的硬编码声明它们是治理失效的早期信号。还有一个我更常用的技巧在统一管理模块里写一个自定义校验任务把禁止出现的坐标和允许出现的版本范围写死每次构建时自动检查。// buildSrc 中的自定义校验任务注册于约定插件内 tasks.register(verifyDependencyRules) { val forbidden listOf(com.google.code.gson:gson:2.8.9) doLast { // 读取各子模块 runtimeClasspath 的解析结果 configurations.forEach { cfg - cfg.resolvedConfiguration.resolvedArtifacts.forEach { artifact - val module artifact.moduleVersion.id.toString() check(module !in forbidden) { 发现被禁止的依赖坐标: $module } } } } }逻辑说明这个任务的关键点是放在约定插件里注册所有模块自动拥有且通过根项目执行一次即可全量校验。参数说明配置解析发生在任务执行阶段会有少量耗时所以我会把它放在 CI 流程上执行本地开发构建不挂载它。运行方式是在根目录执行./gradlew verifyDependencyRules一旦有模块解析结果命中黑名单构建直接失败并指出是哪个坐标。通过这种方式把“依赖统一管理”从人工自觉转成机器强制。最后说一点真实经验我做某跨平台系统的依赖治理时前两周只做了版本提取仓库和传递策略没动结果线上环境出了两起依赖不兼容。后来把仓库顺序、版本约束和自检任务都补齐同类问题再没出现过。依赖统一管理不是一次性工程它会跟随项目持续演进——保持规则简单坚持机器校验比追求一次性的“完美目录”更值得投入。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?