2、Gradle构建生命周期:初始化、配置、执行阶段,理解断点续传的切入点
很多同学学了Gradle一段时间,build.gradle写得飞起,但问起构建到底分几个阶段,往往一脸懵。说实话,我最早带团队的时候也吃过这个亏——一次构建跑了十分钟,中途网断了,所有人干等着。后来我才意识到,不理解生命周期,你就根本不知道“断点续传”该从哪里下手。
Gradle的构建过程不是你写个脚本就一股脑执行完的。它严格分成三个阶段:初始化、配置、执行。这三个阶段各干各的,互不串场。理解它们,才能精准找到断点续传的切入点。
断点续传不可能在整个构建“跑完”之后回退,它只能插在执行阶段的某个任务之间。初始化、配置阶段的产物(Settings对象、Project对象、Task图)在续传时必须完整重建。
2.1 初始化阶段——Gradle的“开机自检”
你在终端敲下gradle build的那一刻,Gradle首先会去读settings.gradle或者settings.gradle.kts。这个文件决定了哪些子项目参与构建,以及项目内部的布局。
我个人习惯在这个阶段尽量少写业务逻辑,因为它跑得很快,一般也就几百毫秒。我在一个微服务项目里见过同事在settings里塞了一大堆条件判断和网络请求……每次构建光初始化就花了5秒。你想想看,这值吗?
初始化阶段的产物是Settings对象和一系列Project实例。这些东西没法被缓存——每次构建都得重新解析settings文件。所以断点续传不考虑这个阶段,它只是前戏。
includeBuild()配合file("...").exists(),但别做网络IO。我记得有次CI上初始化超时,就是因为settings里调了个远程API。
2.2 配置阶段——建立任务依赖图
配置阶段是Gradle最“重”的地方。所有build.gradle(.kts)文件被逐一执行,里面定义的task、dependencies、plugins全都会在这个阶段注册。
注意,这里注册只是“声明”,不是“执行”。很多初学者容易混淆:task hello { doLast { ... } },花括号里的配置代码在配置阶段就跑掉了,只有doLast里的代码才留在执行阶段。嗯,这里要注意——如果配置阶段写了耗时的逻辑,整个构建都会慢下来。
配置完成后,Gradle会生成一张有向无环图(DAG),标识所有Task的执行顺序和依赖关系。这张图很重要。为什么?因为断点续传需要知道你从哪个Task断了,它的上游哪些已经完成,哪些需要重新跑。
// 配置阶段典型代码(这段在配置阶段执行)
tasks.register("compileJava") {
// 这里的代码在配置阶段执行
println "compileJava 被注册了"
// doLast 里的代码在执行阶段才跑
doLast {
println "正在编译 Java 源码..."
}
}
看,上面的println "compileJava 被注册了"在你执行gradle tasks的时候就会打印出来,但doLast里的消息只有真正编译时才会出现。我当年第一次用Gradle时就被这个坑了——在配置阶段打印了太多日志,找真正的问题找了半天。
2.3 执行阶段——真正干活的地方
进入执行阶段,Gradle会按照DAG的顺序,一个一个跑task。每个task的输入(源码、配置、依赖jar)和输出(编译后的class、打包后的jar、测试报告)都会被Gradle记录下来。
这就是断点续传的真正战场。
Gradle内置的增量构建(Incremental Build)和构建缓存(Build Cache)都依赖这个机制:如果某个task的输入没有变化,输出还在,Gradle就会跳过它。我们做断点续传,无非是把这种“跳过”扩展到跨次构建的场景——比如网络断了,你手动修复后重新跑,已经成功的task不应该再执行。
| 阶段 | 关键产出 | 能否断点续传 | 原因 |
|---|---|---|---|
| 初始化 | Settings, Project实例 | ❌ 不能 | 纯元数据,每次必须解析文件 |
| 配置 | Task DAG | ❌ 不能 | 脚本可能有条件分支,必须每次重新计算 |
| 执行 | 每个Task的输入/输出状态 | ✅ 可以 | Task输出可缓存,Up-to-date检查可复用 |
file("temp/xxx.txt").write(...),结果续传时文件内容被覆盖了两次……血的教训。
2.4 断点续传的切入点到底在哪?
说白了,断点续传不是从零开始重跑整个构建,而是从失败的task那里接上。Gradle本身没有“断点续传”这个词,但它提供了好用的基础设施:
- Build Cache:远端或本地缓存task输出,别人构建过的你直接拿来用。
- Up-to-date检查:task的输入没变,输出还在,直接跳过。
- --rerun-tasks:强制重新执行某些task,通常配合自定义逻辑使用。
- 任务结果持久化:你可以在自定义插件里记录哪些task成功、哪些失败,下次构建时跳过成功的。
我个人比较推荐的方式是:在配置阶段结束后、执行阶段开始前插入一个自定义的lifecycle hook,分析上次构建的断点记录,然后有选择地忽略掉已经成功的task。后续章节我会手把手带你实现这个逻辑,别急。
① 初始化、配置阶段不可续传,但必须保证它们每次跑得足够快。
② 执行阶段是断点续传的唯一可行切入点。
③ 利用task的输入/输出声明 + Build Cache + 自定义状态记录,即可实现断点续传。
④ 记住:断点续传不是“暂停”构建,而是“跳过已完成的任务”。