Gradle构建基础:build.gradle、settings与生命周期钩子

说到 Gradle,我最先想到的就是那个让人又爱又恨的 build.gradle 文件。刚入行那会儿,我总觉得它就是个“配方文件”,把依赖堆上去就行了。直到有一次我在一个多模块项目里改了根项目的 build.gradle,结果子模块全崩了——嗯,从那以后我才认真研究起它的结构。

今天我就带你把这三块核心内容捋清楚:build.gradle 文件结构、project 与 settings 配置、构建生命周期钩子。搞懂了这些,你才算真正入门了 Gradle 的世界。


一、build.gradle 文件的结构

一个典型的 build.gradle 文件,其实就是一个 配置脚本。它用 Groovy 或 Kotlin DSL 写成,核心作用就是告诉 Gradle:「这个项目怎么编译、怎么打包、依赖什么」。我个人习惯用 Groovy,因为它早期生态更成熟,代码也更简洁。

文件大体分为三个区域:

  1. 插件声明区 —— 引入构建所需的能力,比如 java、application、android 等。
  2. 依赖与仓库配置区 —— 指定从哪里下载依赖、依赖项有哪些。
  3. 自定义任务与配置区 —— 写一些额外任务、覆盖默认行为。

下面是个最简单的 Java 项目示例:

plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'junit:junit:4.13.2'
}

结构很清晰对吧?但实际项目中,文件往往很长。我曾在某个遗留系统里见过一个超过 500 行的 build.gradle,里面塞满了各种自定义任务和条件配置——说实话,那维护起来真让人头疼。所以我的建议是:把公共配置抽到 allprojects 或 subprojects 块里,别在单个文件里堆太多。


二、project 对象与 settings 配置

你写 build.gradle 时,其实是在操作一个 Project 对象。Gradle 为每个模块都创建了一个 Project 实例,你通过脚本给它设置属性、添加任务、注册监听器。而 settings.gradle 文件,则负责管理整个构建的顶层结构——它决定了哪些模块参与构建。

文件 作用 典型内容
settings.gradle 定义多项目结构、全局插件管理 rootProject.name = 'myApp'
include 'sub1', 'sub2'
build.gradle 配置单个项目的构建逻辑 插件、依赖、任务、自定义配置

小技巧: 在 settings.gradle 里可以用 rootProject.children.each 遍历所有子项目,统一设置一些属性。我曾在微服务项目里用这招给所有子项目自动添加了公共仓库和代码检查插件,省了不少事。

另外,settings.gradle 里还有一个容易被忽略的配置 —— pluginManagement 块:

pluginManagement {
    repositories {
        gradlePluginPortal()
        mavenCentral()
    }
}

注意: 如果你的公司用了私有插件仓库,一定要在这里配好,否则子模块可能找不到插件。我曾经跨过这坑——因为忘了配仓库,CI 编译一直报“插件未找到”,排查了半天才发现问题出在 settings 里。


三、构建生命周期钩子

Gradle 构建并不是“拿起脚本就执行”那么简单。它分为三个阶段:初始化 → 配置 → 执行。每个阶段都有对应的钩子,你可以在这些时机插入自定义逻辑。

  1. 初始化阶段:解析 settings.gradle,确定哪些项目参与构建。钩子很少用,但 gradle.settingsEvaluated 偶尔能派上用场。
  2. 配置阶段:执行所有 build.gradle 中的配置代码(包括任务定义、依赖声明)。这是你写 gradle.taskGraph.whenReady 等钩子的地方。
  3. 执行阶段:真正运行选中的任务。钩子如 gradle.buildFinished,用于清理或通知。

常用的生命周期钩子有这些:

钩子名称 阶段 应用场景举例
gradle.settingsEvaluated 初始化 动态注入 Gradle 属性
gradle.projectsLoaded 初始化 项目加载完成后打印日志
gradle.projectsEvaluated 配置 所有项目配置完成后统一执行任务
gradle.taskGraph.whenReady 配置-执行过渡 检查即将执行的任务列表,做条件判断
gradle.buildFinished 执行 构建完成后的清理或通知(如发消息)

举个例子,我想在构建完成后发通知到企业微信:

gradle.buildFinished { result ->
    println "构建结束,结果: ${result.failure ? '失败' : '成功'}"
    // 可以在这里调用 HTTP 接口发送通知
}

核心要点: 配置阶段的代码一定会执行,无论最终运行哪个任务。所以如果你在配置阶段写了一些耗时操作(比如网络请求),要小心——我见过有人把下载文件写在配置阶段,导致每次执行 gradle tasks 都要先下载一遍,气得想骂人。

为了让你更直观地理解这三者的关系,我画了一张图:

Gradle 构建核心概念关系图 build.gradle 项目构建脚本 插件 / 依赖 / 任务 settings.gradle 多项目配置 模块包含 / pluginManagement 生命周期钩子 初始化 → 配置 → 执行 whenReady / buildFinished 初始化 配置 执行 关键关系 settings.gradle 决定项目结构 → 初始化阶段确定哪些项目参与 每个项目的 build.gradle 在配置阶段执行 → 定义任务和依赖 生命周期钩子让你可以在各阶段插入自定义逻辑,例如构建完成发送通知

记住这张图: build.gradle 负责“做什么”,settings.gradle 负责“谁参与”,生命周期钩子决定“什么时候做”。三者组合起来,你就能灵活控制整个构建过程了。

说到钩子,我再补充一个实战技巧:如果你需要在某任务执行前后干些事,可以用 task.doFirst 和 task.doLast,它们本质上是任务的生命周期回调,比全局钩子更轻量:

task myTask {
    doFirst {
        println '任务即将开始'
    }
    doLast {
        println '任务执行完毕'
    }
}

我平时写 CI 脚本时,经常在 doLast 里记录耗时、上传产物。这么做的好处是不会污染其他任务的逻辑,清晰又解耦。


小结一下

好了,这一块的核心内容就这些。我个人觉得,理解 build.gradle 和 settings.gradle 的关系是最重要的——很多同学刚开始只盯着一个模块的脚本写,忘了整个项目怎么组织的,结果一加子模块就乱套。至于生命周期钩子,不用背,遇到具体需求时回来查就行,多用几次自然就记住了。

下个话题我们来聊聊 Gradle 的网络请求时序图——也就是在实际构建中,依赖是怎么从仓库拉下来的,中间经历了哪些步骤。那才是真正贴近我们课程标题的内容。