2、Gradle构建生命周期:初始化、配置、执行阶段,理解断点续传的切入点

很多同学学了Gradle一段时间,build.gradle写得飞起,但问起构建到底分几个阶段,往往一脸懵。说实话,我最早带团队的时候也吃过这个亏——一次构建跑了十分钟,中途网断了,所有人干等着。后来我才意识到,不理解生命周期,你就根本不知道“断点续传”该从哪里下手。

Gradle的构建过程不是你写个脚本就一股脑执行完的。它严格分成三个阶段:初始化、配置、执行。这三个阶段各干各的,互不串场。理解它们,才能精准找到断点续传的切入点。

一句话结论:
断点续传不可能在整个构建“跑完”之后回退,它只能插在执行阶段的某个任务之间。初始化、配置阶段的产物(Settings对象、Project对象、Task图)在续传时必须完整重建。
Gradle构建生命周期三个阶段与断点续传切入点 阶段一:初始化 • 读取settings.gradle(.kts) • 确定参与构建的Project • 创建Gradle对象实例 阶段二:配置 • 执行build.gradle(.kts) • 注册所有Task并配置依赖 • 构建有向无环图(DAG) 阶段三:执行 • 按依赖顺序执行Task • Task输入/输出被缓存 • 产生构建产物 🎯 断点续传核心切入点 在配置完成后、执行Task之前保存状态 阶段产物缓存/可续传性 初始化阶段 → 纯元数据(不可被缓存续传) 配置阶段 → Task DAG(每次构建必须重新配置) 执行阶段 → 每个Task的输出可被缓存/续传 结论:只有执行阶段的任务输出适合断点续传 续传典型策略 1. 记录已成功执行的Task列表 2. 从失败Task处重新配置并执行 3. 利用Gradle Build Cache复用输出 4. 结合--rerun-tasks指定具体任务

2.1 初始化阶段——Gradle的“开机自检”

你在终端敲下gradle build的那一刻,Gradle首先会去读settings.gradle或者settings.gradle.kts。这个文件决定了哪些子项目参与构建,以及项目内部的布局。

我个人习惯在这个阶段尽量少写业务逻辑,因为它跑得很快,一般也就几百毫秒。我在一个微服务项目里见过同事在settings里塞了一大堆条件判断和网络请求……每次构建光初始化就花了5秒。你想想看,这值吗?

初始化阶段的产物是Settings对象和一系列Project实例。这些东西没法被缓存——每次构建都得重新解析settings文件。所以断点续传不考虑这个阶段,它只是前戏。

一个小技巧: 如果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检查可复用
常见坑: 如果你在配置阶段做了副作用操作(如写文件、生成代码),那你就破坏了Gradle的生命周期规矩。续传时配置阶段会重新跑,那些副作用可能重复执行,导致不可预知的结果。我曾经接手一个项目,构建脚本里直接在配置阶段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 + 自定义状态记录,即可实现断点续传。
④ 记住:断点续传不是“暂停”构建,而是“跳过已完成的任务”。