第1章 Gradle依赖管理基础

说实话,我刚开始用Gradle那会儿,光搞懂依赖是怎么来的就折腾了一下午。好多同学觉得build.gradle里写几行坐标就完事了,结果一跑就撞上版本冲突、找不到jar包之类的问题。先打好地基,后面解决冲突才不慌。

1.1 依赖声明 – 把需要的库写清楚

Gradle的依赖声明都在dependencies {}块里,说白了就是把每个依赖的坐标告诉构建系统。格式很简单,但作用域不一样,行为差异很大。我个人习惯把依赖分成两类:项目自身的和项目之间传递的。

💡 避坑提醒: 同一个依赖如果出现在多个作用域里,Gradle会选版本最高的那个。我遇到过有人把implementation和api混着写,结果模块间暴露了不该暴露的东西。

先看一个最常见的声明例子:

dependencies {
    implementation 'com.google.guava:guava:31.1-jre'
    testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2'
}

这里implementation和testImplementation就是作用域。我建议把依赖写在build.gradle里,别写在gradle.properties那种奇怪的地方——当年我真见过有人这么干,结果Gradle根本不认。

1.2 仓库配置 – 从哪里拉jar包

依赖声明了,Gradle得知道去哪里下载。这就是repositories {}块干的事。默认可能只有mavenCentral(),但实际项目经常要配私有仓库或本地maven。我自己的经验是:仓库顺序影响构建速度,把访问速度快的放前面。

repositories {
    mavenCentral()
    mavenLocal()
    maven { url 'https://nexus.yourcompany.com/repository/maven-public/' }
    google()  // Android项目常见
}
仓库类型用法注意点
mavenCentral()公共Maven仓库默认有,但国内访问慢
mavenLocal()本机 ~/.m2 仓库有时会缓存坏版本
jcenter()已弃用,不建议再用2022年停止服务
自定义Maven仓库公司内部Nexus等记得配认证
⚠ 曾经踩过的坑: 我配JitPack仓库时忘了写allowInsecureProtocol,结果一直报HTTPS错误。如果你用http://协议,必须显式设置allowInsecureProtocol = true。好在那只是个内部测试仓库,不然后果很严重。

1.3 常见依赖作用域 – 搞清楚谁能用

作用域控制着依赖的传播范围和编译、运行时可见性。Gradle官方文档列了一大堆,但实际项目里常用的就这几个。我整理了一个表格方便对照:

作用域什么时候用是否透传给消费者例子
implementation模块内部使用的依赖❌ 不透传Logback、内部工具类
api消费者也需要该依赖✅ 透传公共SDK、fragment接口
compileOnly仅编译需要,运行时由环境提供❌ 不透传Lombok、javax.annotation
runtimeOnly运行时需要,编译时不用❌ 不透传MySQL驱动、某些ClassLoader
testImplementation测试代码专用❌ 不透传JUnit、Mockito
annotationProcessor注解处理器的声明❌ 不透传Dagger、Glide

有个经典场景:你用implementation引入了Guava,模块B依赖你,B的代码里直接引用Guava会编译报错。这就是“不透传”的好处——各模块自扫门前雪,减少冲突。我记得有一个项目就是因为到处用api,结果所有子模块都暴露了各种版本的Jackson,冲突叠罗汉。

核心原则: 尽量用implementation,只在需要暴露接口给消费者时用api。compileOnly能帮你省掉冗余jar,但别滥用,否则运行时ClassNotFoundException会教你做人。

1.4 传递性依赖与版本锁定

你声明了A,A又依赖B和C,B和C可能还依赖D。这个传递链是Gradle自动解析的。但如果B和C都依赖同一个D但版本不同呢?冲突就这么来了。Gradle默认选择最高版本,但有时候这个“最高”并不是你想要的那个。

我个人习惯在dependencies块里加一句constraints或者用force = true来锁定关键版本。比如:

dependencies {
    implementation('com.google.guava:guava:31.1-jre') {
        version {
            strictly '31.1-jre'
        }
    }
}

这里strictly强制Gradle必须用这个版本,如果传递依赖里出现更高版本,它会报错。嗯,有点激进,但适合安全敏感场景。我一般在多模块项目的根项目里做好统一管理,后面章节会详细讲。

1.5 本章知识体系图谱

下面这张图把这一章的核心概念串起来了。从仓库拿到依赖、声明时指定作用域、再通过传递性影响下游。你想想看,如果任何一环没搞对,构建就可能出岔子。

Gradle依赖管理基础结构 仓库配置 依赖声明 依赖作用域 传递依赖 mavenCentral() mavenLocal() implementation api compileOnly runtimeOnly 版本冲突 (最高版本策略) 核心逻辑 1. Gradle按顺序扫描仓库,找到第一个匹配的依赖坐标。 2. 根据依赖作用域决定是否透传递依赖给下游。 3. 当同一坐标出现多个版本,Gradle选最高版本并给出警告。 4. 可以通过 strictly、force 等策略锁定版本。

这张图里,仓库配置是源头,依赖声明是中间人,作用域决定了传播规则,传递依赖则是引发冲突的直接原因。后面几章我们会围绕这张图里的每一个分支深入展开。

1.6 小试牛刀 – 快速检查你的理解

  1. 现在你的项目需要引入 OkHttp 3.12.0,其他模块也可能用到它,你会用implementation还是api?为什么?
  2. 如果公司 Maven 仓库地址写错了,Gradle 会从哪里下载依赖?
  3. 你在运行时抛了 ClassNotFoundException,但编译时一切正常。猜猜这可能是哪个作用域造成的?

这些问题答案不写在正文里,但我建议你动手改几行依赖声明试试,跑一下./gradlew dependencies看看树状图。我当年就是这么学会的——读一百遍文档不如亲手写错一次。

📌 实用命令: ./gradlew :app:dependencies --configuration compileClasspath 可以列出指定配置的依赖树,方便查传递关系。

好了,这一章先到这儿。依赖管理的基础打牢了,后面讲版本冲突解决时你就能更快理解为什么Gradle会那样选版本、为什么有些冲突看似无解其实只是作用域设错了。