依赖管理入门

说到 Gradle 的依赖管理,很多人第一个想到的就是往 build.gradle 里加一行 implementation,然后一切交给工具。嗯,表面上是这样,但如果你只想做「能用」,下面的内容可以跳过了。但如果想让项目依赖干净、稳定,不出幺蛾子——这一节值得你看完。

我个人习惯把依赖管理拆成三个环节:仓库配置(去哪里找)、依赖声明(怎么描述要什么)、依赖传递与冲突(拿到后怎么处理)。下图的框架可以帮你快速建立整体认知:

Gradle 依赖管理核心流程 仓库配置 Maven Central JCenter / 自定义 依赖声明 implementation api / compileOnly runtimeOnly / test 依赖传递与冲突 传递性 → 冲突解决 → 强制版本 💡 关键认知 仓库决定你能找到什么包,声明决定包的作用范围,传递与冲突决定最终的 classpath。

仓库配置:去正确的地方找依赖

Gradle 不生产 jar,它只是 jar 的搬运工。你得告诉它去哪里搬。最常见的公共仓库就三个:Maven Central、JCenter(已停更)、Google Maven。我刚开始用 Gradle 时,习惯把 JCenter 放在第一位,因为那时候它速度还行。后来发现 JCenter 宣布只读之后,很多包不更新了,现在我个人只保留 Maven Central 和 Google 的仓库。

推荐配置(2025 年之后的项目):

repositories {
    mavenCentral()
    google()          // Android 项目必备
    // jcenter()      // 不要再加,除非你还在维护古董项目
    maven { url "https://jitpack.io" }  // GitHub 上的开源库
}

技巧:如果你在墙内环境,可以配置阿里云镜像。我一般这样写:

maven { url "https://maven.aliyun.com/repository/public" }

记得放在第一个,能省下大量下载时间。

依赖声明方式:告诉你究竟要什么

在 Gradle 里声明依赖,不只是写个坐标。你还需要告诉 Gradle:这个 jar 在编译期要用,还是运行时要用,或者只在测试时用。我用一张表格总结最常见的配置:

配置名称作用范围举例场景
implementation编译期 + 运行时(不暴露给消费者)第三方库,如 Retrofit、Gson
api编译期 + 运行时(会暴露给消费者)你开发的 library 中需要传递出去的依赖
compileOnly仅编译期Lombok、Android 注解包
runtimeOnly仅运行时JDBC 驱动实现
testImplementation仅测试编译和运行时JUnit、Mockito

你可能觉得 implementation 和 api 选哪个都差不多。我有一次遇到过这样的坑:一个内部 library 用了 api 声明了 Gson,结果所有使用这个 library 的 app 都强制收到了 Gson 的传递依赖,版本冲突了三个 module。后来我改成 implementation,问题消失。记住:除非你需要把依赖暴露给下游,否则一律用 implementation。

避坑指南:我曾经花了一个下午排查一个诡异的 NoClassDefFoundError,最后发现是因为依赖的运行时包只在编译期通过 compileOnly 声明了,运行时找不到。如果你的库需要在运行时动态加载(比如 SPI),记得用 runtimeOnly 或者 implementation。

依赖传递与冲突:为什么你的 classpath 里有一堆旧包?

Gradle 默认会把依赖的依赖自动拉下来,这叫「传递性」。理论上很省事,但实际项目中经常出问题。比如:

  • A 依赖 B v1.0,B 依赖 C v2.0
  • 另一个模块 D 直接依赖 C v3.0
  • Gradle 会选 最高版本(默认策略)——结果可能就是 C v3.0

听起来合理对吧?但有例外:如果 v3.0 破坏了 API 兼容性,你的代码可能直接挂掉。我遇到过最惨的一次是日志库冲突,两个版本的 SLF4J 同时出现在 classpath,导致日志死活打印不出来。调试了一整天才发现是传递依赖惹的祸。

冲突解决三板斧

  1. 查看依赖树:./gradlew :app:dependencies --configuration runtimeClasspath。这个命令百试百灵,我每次改依赖都先跑一遍。
  2. 排除传递依赖:在声明里加上 exclude。比如:
implementation('com.example:some-lib:1.0') {
    exclude group: 'com.conflict', module: 'old-module'
}
  1. 强制指定版本:全局统一,适合公司内部库:
configurations.all {
    resolutionStrategy {
        force 'com.fasterxml.jackson:jackson-core:2.15.0'
    }
}

个人小经验:如果你希望某个传递依赖完全不进来,除了 exclude,还可以用 transitive = false 关掉整个模块的传递性。但慎用,这可能会导致你漏掉真正需要的子依赖。我一般只在排查冲突时临时用一下。

仓库优先级与镜像加速

仓库是有顺序的。Gradle 会从上到下依次查找,找到第一个版本就停止。所以如果你把速度慢的镜像放在前面,每次构建都要等他超时。我建议:本地缓存的私有仓库或阿里云镜像放第一位,Maven Central 放最后兜底。

另外,曾经有个同事把公司的私有仓库和 Maven Central 搞反了,结果每次发布后都要等几分钟才更新。嗯,顺序真的很重要。


到这里,依赖管理的三大块你已经清楚了。仓库配置是「货源」,声明方式是「合同条款」,传递与冲突是「售后处理」。把这三个环节理顺,你的构建配置基本不会出大问题。下一部分我们实际动手写一个模块的依赖配置,顺带把常见陷阱都踩一遍——当然,我会告诉你躲开的方法。