主题
第 3 章 核心概念:Project / Task / Plugin / 生命周期
学习目标:搞懂 Gradle 的"四块基石"—— Project(项目)、Task(任务)、Plugin(插件)、生命周期(Build Lifecycle);理解为什么
dependencies { ... }在配置阶段执行,为什么doLast { ... }在执行阶段才跑;能画出gradle build的 Task 依赖图。这是整本笔记最关键的一章。看懂这章,看任何 build.gradle.kts 都不会再发懵;看不懂这章,后面所有内容都会一头雾水。
3.1 用一张图概括 Gradle 的世界
┌─────────────────────────────────────────────────────────────────┐
│ Build (整个构建) │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Project (一个项目) │ │ Project (另一个) │ ... │
│ │ │ │ │ │
│ │ ┌─────┐ ┌─────┐ │ │ ┌─────┐ ┌─────┐ │ │
│ │ │Task │ │Task │ ... │ │ │Task │ │Task │ │ │
│ │ └─────┘ └─────┘ │ │ └─────┘ └─────┘ │ │
│ │ │ │ │ │
│ │ ★ Plugin 给 Project │ │ ★ Plugin 给 Project │ │
│ │ 注入一组 Task / 配置 │ │ 注入一组 Task / 配置│ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
★ 生命周期 (Build Lifecycle):Initialization → Configuration → Execution记住这张图:整个 Gradle 就在干一件事 —— 通过 Plugin 给 Project 装上一堆 Task,再按顺序执行用户指定的 Task。
3.2 Project:构建的中心
3.2.1 什么是 Project?
一个 Project = 一个可独立构建的产物。可以是:
- 一个 jar 包
- 一个 war 包
- 一个 APK
- 一个 zip 发行包
- 啥都不产出(只用作 root 协调子模块)
每个 build.gradle.kts 文件背后对应一个 Project 对象。所有你在脚本里写的 dependencies { }、tasks.register { },本质上都是调用 Project 对象的方法。
kotlin
// build.gradle.kts —— 看起来像配置文件
plugins { java }
group = "com.example"
version = "1.0.0"
dependencies { implementation("...") }
// 实际上 Gradle 把它解释为:
// (this 隐式指向 project 对象)
project.plugins.apply("java")
project.group = "com.example"
project.version = "1.0.0"
project.dependencies.implementation("...")📌 生活化类比:把 Project 想成"一家工厂"。
group/version是工厂名片;plugins是工厂里安装的"流水线"(如装个 Java 编译流水线);tasks是流水线上的"工序"。
3.2.2 多 Project 的层级
实际工程里很少只有 1 个 Project,更多是根 Project + 多个子 Project:
my-project/ ← 根 Project(root)
├── settings.gradle.kts ← 声明哪些子 Project
├── build.gradle.kts ← 根 Project 的脚本(可选)
│
├── app/ ← 子 Project ":app"
│ └── build.gradle.kts
│
├── lib-core/ ← 子 Project ":lib-core"
│ └── build.gradle.kts
│
└── feature-billing/ ← 子 Project ":feature-billing"
└── build.gradle.ktssettings.gradle.kts:
kotlin
rootProject.name = "my-project"
include("app", "lib-core", "feature-billing")每个子 Project 都是独立的 Project 对象,但共享同一个 Gradle 进程。这是多模块项目能极速并行构建的基础。
3.2.3 引用其他 Project
子 Project 之间想互相依赖:
kotlin
// app/build.gradle.kts
dependencies {
implementation(project(":lib-core")) // 用 :path 引用
implementation(project(":feature-billing"))
}根 Project 可以一次性给所有子 Project 配公共配置:
kotlin
// 根 build.gradle.kts
subprojects {
apply(plugin = "java")
java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }
repositories { mavenCentral() }
}⚠️ 现代最佳实践是用 Convention Plugin 替代
subprojects {}。第 8 章会讲。
3.3 Task:构建的最小单元
3.3.1 什么是 Task?
Task = 一个具体的"动作单元"。比如:
compileJava—— 编译 Javatest—— 跑测试jar—— 打 jar 包clean—— 清空 build 目录
每个 Task 有:
- 一个名字(在 Project 里唯一)
- 零个或多个依赖(必须先跑 X,才能跑我)
- 零个或多个 actions(doFirst / doLast 里的代码)
- input / output 声明(决定增量构建是否生效)
3.3.2 注册一个 Task
kotlin
tasks.register("hello") {
group = "intro" // 在 ./gradlew tasks 列表里的分组
description = "打印 hello" // 描述
doLast { // 执行阶段干的事
println("Hello from Gradle!")
}
}跑:
bash
$ ./gradlew hello
> Task :hello
Hello from Gradle!
BUILD SUCCESSFUL in 600ms3.3.3 doFirst vs doLast:动作链表
关键概念:Task 不是"一段代码",而是"一个动作链表"。
kotlin
tasks.register("multi") {
doFirst { println("1️⃣ doFirst-A") } // 加到链表头
doFirst { println("2️⃣ doFirst-B") } // 又加到链表头
doLast { println("3️⃣ doLast-X") } // 加到链表尾
doLast { println("4️⃣ doLast-Y") } // 又加到链表尾
}执行顺序:
执行队列:
[doFirst-B (后加,挤到前)]
[doFirst-A]
[doLast-X]
[doLast-Y (后加,挤到尾)]
输出:
2️⃣ doFirst-B
1️⃣ doFirst-A
3️⃣ doLast-X
4️⃣ doLast-Y📌 生活化类比:把 Task 想成"准备出门",doFirst 是"穿衣服前先做的事"(最近加的最先做:先擦防晒霜 → 再涂润肤露 → 再穿衣服),doLast 是"穿完衣服后做的事"(最早加的最先做:先关灯 → 再锁门)。
3.3.4 Task 之间的依赖
3 种声明方式:
kotlin
// 方式 1:dependsOn(强依赖:B 必须在 A 之后跑)
tasks.register("compile") { doLast { println("compile") } }
tasks.register("package") {
dependsOn("compile")
doLast { println("package") }
}
// 跑 ./gradlew package 会先跑 compile
// 方式 2:mustRunAfter(弱依赖:如果两个都要跑,B 必须在 A 之后;但单独跑 B 不会触发 A)
tasks.register("smokeTest") {
mustRunAfter("test")
doLast { println("smoke") }
}
// 方式 3:finalizedBy(A 跑完后,必须跑 B —— 即使 A 失败)
tasks.register("upload") {
doLast { println("upload") }
finalizedBy("cleanup")
}
tasks.register("cleanup") {
doLast { println("cleanup") }
}3.3.5 Task 的依赖图(DAG)
Gradle 在配置阶段把所有 Task 的依赖关系组合成一个 有向无环图(DAG)。跑某个 Task 时,从该 Task 反向追踪所有依赖,按拓扑序执行。
举例:跑 ./gradlew build,DAG 大致这样:
┌──────────────────┐
│ :build │
└────────┬─────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ :assemble │ │ :check │
└──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ :jar │ │ :test │
└──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ :classes │ │ :testClasses │
└──────┬───────┘ └──────┬───────┘
│ │
┌──────┴────────┐ │
▼ ▼ ▼
┌──────────┐ ┌──────────────┐ ┌─────────────────┐
│:compile │ │ :process │ │ :compileTest │
│ Java │ │ Resources │ │ Java │
└──────────┘ └──────────────┘ └─────────────────┘Gradle 会自动并行执行没有依赖关系的 Task(如 compileJava 和 processResources 可以同时跑)。
3.3.6 看 DAG 的命令
bash
# 列出所有 task
$ ./gradlew tasks
# 看某个 task 的依赖(dry run,不实际执行)
$ ./gradlew build --dry-run
:compileJava SKIPPED
:processResources SKIPPED
:classes SKIPPED
:jar SKIPPED
:assemble SKIPPED
:compileTestJava SKIPPED
:processTestResources SKIPPED
:testClasses SKIPPED
:test SKIPPED
:check SKIPPED
:build SKIPPED
# 用 task tree 插件(推荐)
$ ./gradlew build taskTree3.4 Plugin:把一组功能"打包"装进 Project
3.4.1 为什么要 Plugin?
如果没有 Plugin,每个 Java 项目都要自己写:
- compileJava task
- jar task
- test task
- 设置 src/main/java、src/test/java 这些目录约定
- ...
这些是 每个 Java 项目都要的公共逻辑。Gradle 把它们封装成 java 插件 —— 你只要 plugins { java },立刻拥有所有这些 Task 和约定。
3.4.2 应用 Plugin 的两种方式
kotlin
// ✅ 推荐:plugins DSL(类型安全、版本声明、自动加载到 classpath)
plugins {
java
id("org.springframework.boot") version "3.2.0"
kotlin("jvm") version "1.9.20"
}
// ❌ 老语法(apply)—— 看老项目可能见到,新项目不要用
apply(plugin = "java")
buildscript {
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:3.2.0")
}
}
apply(plugin = "org.springframework.boot")3.4.3 Plugin 的三大类型
| 类型 | 来源 | 应用方式 |
|---|---|---|
| Core Plugin(核心) | Gradle 自带 | plugins { java }、plugins { application }、plugins { jvm-test-suite } |
| Community Plugin(社区) | Plugin Portal | plugins { id("org.springframework.boot") version "3.2.0" } |
| Local / Custom Plugin(自定义) | 项目内 buildSrc / build-logic | plugins { id("my.convention") } |
3.4.4 一个 Plugin 给你装了什么
以 java 插件为例:
plugins { java } 实际上自动给你做了:
① 注册 6 个 Task:
- compileJava
- processResources
- classes (聚合)
- compileTestJava
- processTestResources
- testClasses (聚合)
- jar
- test
- check (聚合)
- assemble (聚合)
- build (聚合)
- clean
② 注册 4 个 Configuration(依赖配置):
- implementation
- api (需要 java-library 插件)
- compileOnly
- runtimeOnly
- testImplementation
- ...
③ 设置 SourceSet 约定:
- main → src/main/java、src/main/resources
- test → src/test/java、src/test/resources
④ 设置 build/ 输出目录约定📌 生活化类比:Plugin 像是"外卖软件里的一道套餐"。你点"java 套餐",立刻把"compile / test / jar / clean"这一组餐都端上来了,不用一道一道点。
3.4.5 常用插件速查
| 插件 ID | 用途 |
|---|---|
java | Java 编译 |
java-library | Java 库(含 api 配置) |
application | 可执行 Java 应用(带 run 任务) |
kotlin("jvm") | Kotlin JVM |
org.springframework.boot | Spring Boot |
com.android.application | Android 应用 |
com.android.library | Android 库 |
maven-publish | 发布到 Maven 仓库 |
signing | 给产物签名 |
jacoco | 代码覆盖率 |
idea / eclipse | 生成 IDE 元数据(很少用) |
3.5 生命周期(Build Lifecycle):Gradle 跑构建的 3 个阶段
这是整个第 3 章最关键的部分。理解 3 个阶段,是看懂任何 build.gradle.kts 的钥匙。
3.5.1 三个阶段总览
★ Gradle Build Lifecycle ★
┌─────────────────────────┐
│ ① Initialization 阶段 │ 读 settings.gradle.kts
│ "我要构建几个项目?" │ 决定有哪些 Project
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ ② Configuration 阶段 │ 执行所有 Project 的 build.gradle.kts
│ "把所有 Task 都创建好" │ 构造 Task 对象图(DAG)
└────────────┬────────────┘ ⚠️ 不执行 Task 的实际动作!
│
▼
┌─────────────────────────┐
│ ③ Execution 阶段 │ 按用户指定的 Task 反向追踪 DAG
│ "按图跑 Task" │ 依次执行需要的 Task 的动作
└─────────────────────────┘3.5.2 一段代码看尽 3 个阶段
kotlin
println("[1] 这行在哪个阶段执行?") // ← Configuration 阶段(脚本本身)
tasks.register("hello") {
println("[2] 这行在哪个阶段执行?") // ← Configuration 阶段(注册 Task 时)
doFirst {
println("[3] 这行在哪个阶段执行?") // ← Execution 阶段
}
doLast {
println("[4] 这行在哪个阶段执行?") // ← Execution 阶段
}
}
println("[5] 这行在哪个阶段执行?") // ← Configuration 阶段跑 ./gradlew hello:
[1] 这行在哪个阶段执行? ← Configuration(顺序执行脚本)
[2] 这行在哪个阶段执行? ← Configuration(register 闭包是配置代码)
[5] 这行在哪个阶段执行? ← Configuration
> Task :hello
[3] 这行在哪个阶段执行? ← Execution(doFirst 在执行阶段才跑)
[4] 这行在哪个阶段执行? ← Execution
BUILD SUCCESSFUL跑 ./gradlew tasks(没跑 hello):
[1] 这行在哪个阶段执行? ← Configuration 还是会跑!
[2] 这行在哪个阶段执行? ← Configuration 还是会跑!
[5] 这行在哪个阶段执行? ← Configuration 还是会跑!
# [3] [4] 不会跑,因为 hello task 没被执行3.5.3 关键陷阱:把"执行阶段代码"写到了"配置阶段"
新手最常见的 bug:
kotlin
// ❌ 错的写法
tasks.register("badTask") {
println("Building...") // ← 配置阶段就跑了,不对!
val result = computeSomething() // ← 配置阶段就计算了,不对!
file("output.txt").writeText("$result") // ← 配置阶段就写文件了,不对!
}
// ✅ 正确写法
tasks.register("goodTask") {
doLast {
println("Building...")
val result = computeSomething()
file("output.txt").writeText("$result")
}
}怎么验证?跑 ./gradlew tasks(没要求执行 badTask),如果你看到了 "Building..." 输出,说明你犯了这个错。
📌 核心原则:注册 Task 的闭包里只放"配置 Task 自己的元数据"(如
dependsOn、description、inputs.file(...)),真正的"动作"必须放进 doLast / doFirst。
3.5.4 为什么 dependencies {} 能在配置阶段就解析?
很多人疑惑:
kotlin
dependencies {
implementation("org.springframework:spring-core:6.1.1")
}这是配置阶段执行的,意味着"声明依赖"是配置阶段干的。但实际下载依赖、解析依赖图是首次需要时(lazy)。比如:
compileJava任务执行时需要 classpath → 这时才解析- 用户跑
./gradlew dependencies→ 这时才解析
📌 要点:声明 ≠ 解析。声明在配置阶段,解析在按需触发。
3.5.5 配置阶段不要做的事
| 反模式 | 为什么坏 | 正确做法 |
|---|---|---|
| 在脚本顶层读文件 / 网络 | 每次构建都跑,即使不需要 | 写到 Task 的 doLast 里 |
| 在 register 闭包里直接干活 | 同上 | 写到 doLast / doFirst |
滥用 afterEvaluate {} | 推迟配置不可控,调试困难 | 用 Provider/Property API |
用 tasks.getByName("xxx") { ... } 无脑修改 | 触发 task 立刻配置(破坏 lazy) | 用 tasks.named("xxx") { ... } |
3.6 Task 类型 vs 任意 Task
3.6.1 简单 Task vs Typed Task
kotlin
// 简单 Task(没有特定类型,只能用 doLast/doFirst)
tasks.register("simple") {
doLast { println("simple") }
}
// Typed Task(有类型,能用类型自带的属性)
tasks.register<Copy>("copyDocs") {
from("docs") // ← Copy 类型自带 from 属性
into(layout.buildDirectory.dir("docs"))
include("**/*.md")
}
// 不写 register,等价于 doLast 形式
tasks.register("docsZip", Zip::class) {
archiveFileName = "docs.zip"
destinationDirectory = layout.buildDirectory.dir("dist")
from("docs")
}3.6.2 内置常用 Task 类型
| 类型 | 用途 | 典型属性 |
|---|---|---|
Copy | 拷贝文件 | from / into / include / rename |
Delete | 删除文件 | delete |
Zip / Tar | 打包归档 | archiveFileName / destinationDirectory |
Exec | 运行外部命令 | commandLine / args |
JavaExec | 运行 Java main class | mainClass / classpath / args |
Sync | 同步目录(多余的会删) | from / into |
Test | JUnit 测试运行 | useJUnitPlatform / filter |
3.6.3 一个综合例子:自定义打包任务
kotlin
// 拷贝 web 静态资源 → 编译 java → 打 zip 发布包
tasks.register<Copy>("copyWeb") {
from("src/web")
into(layout.buildDirectory.dir("dist/web"))
}
tasks.register<Zip>("releaseZip") {
dependsOn("jar", "copyWeb")
archiveFileName = "release-${project.version}.zip"
destinationDirectory = layout.buildDirectory.dir("releases")
from(layout.buildDirectory.dir("libs")) // 拷 jar
from(layout.buildDirectory.dir("dist")) // 拷 web
from("README.md") // 拷 readme
}跑 ./gradlew releaseZip 就会得到一个完整的发布包。
3.7 命令行选 Task:: 路径写法
bash
# 单 Project 项目,直接 task 名
$ ./gradlew build
# 多 Project 项目,用 :path 指定子项目
$ ./gradlew :app:build # 只跑 app 模块的 build
$ ./gradlew :lib-core:test # 只跑 lib-core 的测试
# 跑多个
$ ./gradlew clean build
# 跳过某个 task
$ ./gradlew build -x test
# 强制执行(忽略 UP-TO-DATE)
$ ./gradlew build --rerun-tasks3.8 章末小结
★ 第 3 章核心知识图谱 ★
│
┌─────────────┬───────┴───────┬─────────────┐
│ │ │ │
┌────▼────┐ ┌───▼────┐ ┌────▼────┐ ┌───▼─────┐
│ Project │ │ Task │ │ Plugin │ │ Lifecycle│
├─────────┤ ├────────┤ ├─────────┤ ├──────────┤
│ 一个产物 │ │ 动作单元 │ │ 功能套餐 │ │ Init │
│ 多模块 │ │ doFirst│ │ 核心 │ │ Config │
│ project()│ │ doLast │ │ 社区 │ │ Execute │
│ │ │ DAG │ │ 自定义 │ │ │
└─────────┘ └────────┘ └─────────┘ └──────────┘
↑
★ 必须区分配置 vs 执行 ★🎤 3.9 章末面试题(10 道高频题)
Q1. Gradle 的构建生命周期分哪几个阶段?
答:3 个阶段:
- Initialization(初始化):读
settings.gradle.kts,确定有哪些 Project; - Configuration(配置):执行所有 Project 的
build.gradle.kts,构造 Task 对象图(DAG)。注意:所有的dependencies {}、plugins {}、tasks.register {}都在这个阶段执行; - Execution(执行):按用户指定的 Task 反向追踪 DAG,依次执行需要的 Task 的实际动作(doFirst、doLast 里的代码)。
⚠️ 关键点:配置阶段每次构建都会完整跑,所以不要在配置阶段做耗时操作(读大文件、调网络)。Configuration Cache(配置缓存)能缓存这一阶段,第 9 章会讲。
Q2. 下面这段代码会输出什么?
kotlin
println("A")
tasks.register("foo") {
println("B")
doFirst { println("C") }
doLast { println("D") }
}
println("E")跑 ./gradlew foo 时输出顺序是?跑 ./gradlew tasks 呢?
答:
跑 ./gradlew foo:
A ← 配置阶段
B ← 配置阶段(register 闭包也是配置)
E ← 配置阶段
> Task :foo
C ← 执行阶段(doFirst)
D ← 执行阶段(doLast)跑 ./gradlew tasks:
A ← 配置阶段还是会跑
B ← 配置阶段还是会跑
E ← 配置阶段还是会跑
(C 和 D 不会跑,因为没执行 foo)Q3. dependsOn、mustRunAfter、finalizedBy 三个有什么区别?
答:
dependsOn:强依赖。B dependsOn A表示"跑 B 之前必须跑 A"。如果 A 不在执行列表,会被自动加进来。mustRunAfter:弱顺序。B mustRunAfter A表示"如果 A 和 B 都要跑,B 必须在 A 之后"。但单独跑 B 不会触发 A。finalizedBy:终结依赖。A finalizedBy B表示"A 跑完后必须跑 B",即使 A 失败 B 也会跑。常用于资源清理(释放数据库连接、删临时文件)。
| 关系 | 单独跑 B 会跑 A 吗 | 顺序保证 |
|---|---|---|
| dependsOn | ✅ 会 | ✅ A → B |
| mustRunAfter | ❌ 不会 | 仅在两者都执行时保证 |
| finalizedBy | / | A 之后跑 B(A 失败也跑) |
Q4. doFirst 和 doLast 的区别是什么?
答:Task 维护一个动作链表:
doFirst { ... }把动作 加到链表头(多个 doFirst 倒序执行);doLast { ... }把动作 加到链表尾(多个 doLast 顺序执行)。
执行时整个链表按顺序跑。所以同一个 Task 的多个 doFirst / doLast 顺序是:
后写的 doFirst → ... → 第一个 doFirst → 第一个 doLast → ... → 后写的 doLast💡 实战建议:99% 时候用
doLast就够了。doFirst主要用来"在某个 Task 执行前插入打印 / 校验"。
Q5. tasks.register 和 tasks.create 有什么区别?
答:
tasks.create("foo") { ... }:立即创建并配置 Task —— 配置阶段一遇到就跑闭包。tasks.register("foo") { ... }:懒注册 —— 只把"配置闭包"存起来,等真正需要这个 Task 时才执行。
优先用 register,因为:
- 构建快:用不到的 Task 不配置;
- 配置缓存友好:register 是 Configuration Cache 推荐写法;
- 现代标准:Gradle 5+ 的官方推荐。
Q6. 为什么不要在 build.gradle.kts 顶层读文件 / 调用网络?
答:因为顶层代码在配置阶段执行,每次构建都会完整跑(即使你只是 ./gradlew tasks 列出任务)。结果:
- 拖慢每次构建(哪怕几秒,乘以一天 100 次构建就是几分钟);
- 破坏 Configuration Cache:网络 / 文件 IO 不是 deterministic,缓存机制无法处理;
- 难以测试:如果网络挂了,所有
./gradlew命令全挂。
正确做法:把这种逻辑放进自定义 Task 的 doLast,并用 inputs/outputs 声明输入输出 → 增量构建友好。
Q7. plugins { } 和 apply plugin: 'xxx' 有什么区别?
答:
plugins {}(DSL 块):现代推荐写法。优点:- 类型安全:IDE 立即识别 plugin 提供的扩展(DSL 智能提示);
- 自动管理 classpath:不需要手动 buildscript dependencies;
- Plugin Portal 自动解析:
id("xxx") version "yyy"一行搞定; - 可以静态分析:Gradle 能在 init 阶段就知道项目用了哪些插件。
apply(plugin = "xxx")(旧 API):老语法。需要先在buildscript {}里 classpath 上插件 jar,然后 apply。不要在新代码里用。
Q8. 多个子模块共享配置,subprojects {} 和 Convention Plugin 哪个好?
答:Convention Plugin 更好。
subprojects {} 的问题:
- 根 build.gradle 越写越长,难以维护;
- 所有子模块都被强制配置(即使只用一部分);
- 配置代码无法复用到其他项目;
- 影响并行配置 / 配置缓存。
Convention Plugin(写在 buildSrc/ 或 build-logic/ 里):
- 配置封装到 Kotlin 类,可单测;
- 子模块按需
plugins { id("my.convention") }; - 跨项目复用;
- 性能更好(懒加载)。
第 8 章会有完整示例。
Q9. 怎么看一个 Task 到底依赖了哪些 Task?
答:4 种方式:
--dry-run/-m:打印执行顺序但不跑bash$ ./gradlew build -mtaskTree插件(推荐):可视化依赖树kotlinplugins { id("com.dorongold.task-tree") version "2.1.1" }bash$ ./gradlew build taskTree--scan:上传到 Gradle 公共服务,浏览器里看完整依赖图bash$ ./gradlew build --scan手动看插件源码 —— 老办法。
Q10. project.afterEvaluate {} 是干啥的?什么时候用?
答:afterEvaluate {} 注册一个回调,在当前 Project 的配置阶段结束后执行。常见场景:
kotlin
afterEvaluate {
// 这时所有 plugins / extensions 都已经配好了
val springVersion = the<SpringBootExtension>().version
println("Spring Boot version: $springVersion")
}什么时候用:
- 需要读取别的插件配置完之后才有的属性
- 配置依赖关系时需要等所有 plugin 完成
什么时候 不要 用:
- 大多数情况下用
Provider/PropertyAPI 替代(lazy 而非延迟)。 - 滥用
afterEvaluate会让构建逻辑像意大利面 —— 别人看不懂啥时候配啥。
💡 现代最佳实践:优先 Provider API → 其次 register 的 lazy 配置 → 万不得已才 afterEvaluate。
下一章 → 第 4 章 · 构建脚本 DSL:Groovy vs Kotlin →
🎬 可视化演示
演示加载缓慢或样式异常?点此在新标签页打开 ↗
💻 示例代码
txt
/*
* 第 3 章 · 核心概念演示 — build.gradle.kts
*
* 这个脚本演示了 Project / Task / Plugin / 生命周期的所有核心点。
* 跑下面任意一条命令,对照注释观察输出:
*
* ./gradlew tasks → 看到所有自定义 task
* ./gradlew lifecycleDemo → 看 doFirst/doLast 顺序
* ./gradlew chained → 看 dependsOn 触发链
* ./gradlew pipeline → 看复杂 DAG
* ./gradlew clean lifecycleDemo → 看多个 task 的执行顺序
*/
plugins {
base // base 插件提供 clean / assemble / check / build 这些聚合任务
}
// ============================================================================
// 1. 配置阶段会执行的代码(脚本顶层)
// ============================================================================
println("[CONFIG] 我在配置阶段就执行了!只要你跑任何 ./gradlew 命令我都会跑")
println("[CONFIG] Project name: " + project.name)
println("[CONFIG] Project version: " + project.version)
// ============================================================================
// 2. 演示:doFirst / doLast 的顺序
// ============================================================================
tasks.register("lifecycleDemo") {
group = "ch03-demo"
description = "演示 doFirst / doLast 顺序"
println("[CONFIG] register 闭包里的 println 也是配置阶段")
// 注册多个 doFirst(后加的在前)
doFirst { println("[EXEC] doFirst-A (第一次写)") }
doFirst { println("[EXEC] doFirst-B (第二次写)") }
doFirst { println("[EXEC] doFirst-C (第三次写) ← 最后写的会最先执行") }
// 注册多个 doLast(按写入顺序)
doLast { println("[EXEC] doLast-X (第一次写) ← 第一次写的会最先执行") }
doLast { println("[EXEC] doLast-Y (第二次写)") }
doLast { println("[EXEC] doLast-Z (第三次写)") }
}
// ============================================================================
// 3. 演示:Task 之间的依赖(dependsOn)
// ============================================================================
tasks.register("step1") {
group = "ch03-demo"
doLast { println("⚙️ Step 1 done") }
}
tasks.register("step2") {
group = "ch03-demo"
dependsOn("step1")
doLast { println("⚙️ Step 2 done") }
}
tasks.register("step3") {
group = "ch03-demo"
dependsOn("step2")
doLast { println("⚙️ Step 3 done") }
}
tasks.register("chained") {
group = "ch03-demo"
description = "跑这个会触发 step1 → step2 → step3"
dependsOn("step3")
doLast { println("✅ All steps complete") }
}
// ============================================================================
// 4. 演示:mustRunAfter(弱顺序)
// ============================================================================
tasks.register("test1") {
group = "ch03-demo"
doLast { println("🧪 test1 running...") }
}
tasks.register("test2") {
group = "ch03-demo"
mustRunAfter("test1") // 仅在两者都跑时,test2 必须在 test1 之后
doLast { println("🧪 test2 running...") }
}
// ============================================================================
// 5. 演示:finalizedBy(终结依赖)
// ============================================================================
tasks.register("upload") {
group = "ch03-demo"
finalizedBy("cleanup")
doLast { println("📤 Uploading... (假装上传文件)") }
}
tasks.register("cleanup") {
group = "ch03-demo"
doLast { println("🧹 Cleanup running... (即使 upload 失败也会跑)") }
}
// ============================================================================
// 6. 演示:典型 Typed Task(Copy)
// ============================================================================
tasks.register<Copy>("copyDocs") {
group = "ch03-demo"
description = "演示 Copy 类型的 Task"
from("src")
into(layout.buildDirectory.dir("copied-src"))
include("**/*.kt", "**/*.java")
}
// ============================================================================
// 7. 综合演示:模拟一个 CI pipeline
// ============================================================================
tasks.register("compile") { group = "pipeline"; doLast { println("📦 compile") } }
tasks.register("unitTest") { group = "pipeline"; dependsOn("compile"); doLast { println("✅ unitTest") } }
tasks.register("lint") { group = "pipeline"; dependsOn("compile"); doLast { println("🔍 lint") } }
tasks.register("packageApp") {
group = "pipeline"
dependsOn("compile")
doLast { println("📦 packageApp") }
}
tasks.register("deploy") {
group = "pipeline"
dependsOn("unitTest", "lint", "packageApp") // 三个并行依赖
doLast { println("🚀 deploy DONE") }
}
tasks.register("pipeline") {
group = "ch03-demo"
description = "一个完整的 mock CI 流水线"
dependsOn("deploy")
}
// ============================================================================
// 8. 演示:用循环动态生成 Task
// ============================================================================
val features = listOf("auth", "billing", "notification", "payment", "search")
features.forEach { feature ->
tasks.register("test-" + feature) {
group = "feature-test"
doLast { println("🧪 Testing feature: " + feature) }
}
}
// 一个聚合任务,按顺序触发所有 feature 测试
tasks.register("testAllFeatures") {
group = "ch03-demo"
description = "测试所有 feature"
dependsOn(features.map { "test-" + it })
}
println("[CONFIG] 脚本结尾的 println,配置阶段执行")txt
rootProject.name = "ch03-core-concepts-demo"