构建脚本基础:build.gradle结构、Project与Task、Groovy/Kotlin DSL入门
说起build.gradle,我猜你第一眼看到它时,心里可能会想:“这不就是个配置文件吗?” 其实它的内核远比配置文件要深。我做了几年Gradle工程化后,才真正理解它背后那张“声明式构建”的网。
build.gradle 的骨架
一个典型的build.gradle文件,由三部分组成:插件声明、依赖管理、自定义配置。举个例子:
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.0'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.0'
}
tasks.named('test') {
useJUnitPlatform()
}
jar {
manifest {
attributes 'Main-Class': 'com.example.App'
}
}
这里的repositories、dependencies、tasks都是Gradle内置的配置块。每个配置块背后都对应一个Project对象的方法调用。Gradle在执行脚本时,会把这些块“委托”给一个叫Project的实例来解析。
我个人习惯把plugins块放在最上面,因为它决定了后面能用的所有扩展。项目依赖的仓库和外部插件顺序其实无所谓,但插件块必须优先——这是Gradle的硬性要求。
Project:构建的“总指挥”
每个build.gradle文件都对应一个Project对象。它负责管理整个构建的生命周期,包括:
- 加载并解析脚本
- 创建和配置Task
- 管理依赖关系
- 触发构建流程
你可以把Project想象成一个容器,里面装了插件、依赖、配置属性,以及最重要的——Task。Gradle的构建执行,本质上就是按照Task依赖图(DAG)的顺序,依次运行这些Task。
我曾经在一个多模块项目里,不小心在子项目的build.gradle里重复声明了全局插件,导致构建速度翻倍。记住:allprojects { }和subprojects { }区块会影响父Project的子Project集合,用之前一定要想清楚作用域。
Task:构建的“最小原子”
Task是Gradle里最基础的可执行单元。每个Task都有动作(action)、输入(inputs)和输出(outputs)。靠输入输出,Gradle能实现增量构建——只有发生变化的部分才重新执行。
定义一个Task很简单:
tasks.register('hello') {
doLast {
println 'Hello, Gradle!'
}
}
执行./gradlew hello就会打印这句问候。你可能会觉得这不就是一个脚本函数吗?但它背后隐藏着Gradle的配置阶段和执行阶段的分离逻辑:
- 配置阶段:Gradle解析所有Project和Task定义,构建依赖图
- 执行阶段:Gradle按照图顺序依次执行Task动作
嗯,这里要注意:如果你将代码写在doLast外面,它会在配置阶段就执行掉,而不是在执行阶段。我曾经因为这个bug排查了一下午——Task一直在“提前跑”。
Groovy DSL vs Kotlin DSL:你的选择
Gradle从5.0开始就支持两种DSL:Groovy DSL(.gradle)和Kotlin DSL(.gradle.kts)。说白了,一个用Groovy,一个用Kotlin。你想想看,如果你之前是Android开发者,大概早就用上Kotlin DSL了。我个人推荐新项目优先选用Kotlin DSL,理由很简单:类型安全、IDE提示友好、重构更安心。
但如果你接手的是老项目,Groovy DSL也不差,只是调试时容易因为动态类型踩坑。
下面是同一个配置在两种DSL中的写法对比:
| 功能 | Groovy DSL | Kotlin DSL |
|---|---|---|
| 插件声明 | |
|
| 依赖声明 | |
|
| Task注册 | |
|
| 属性赋值 | |
|
| 字符串拼接 | |
|
Kotlin DSL最大的好处是——你在IDE里Ctrl+点击一个扩展方法,能直接跳到定义处。这对于排查奇怪报错特别有用。我建议至少把build.gradle.kts的代码格式化加上,不然括号层级稍微一多就晕了。
核心知识结构
下面这张图总结了本章的核心概念及其关系。你看一眼就能明白build.gradle、Project、Task、Plugin和Dependency之间是怎么连起来的:
避坑指南:我踩过的几个坑
- Task注册用
register还是create? 我早期一直用create,后来才发现register是延迟创建(Lazy Configuration),在多配置场景下能避免不必要的配置耗时。建议无脑用register。 - Kotlin DSL里漏了构造参数 比如
tasks.register("myTask")后直接写action,IDE不会报错,但运行时可能找不到类型。解决方法:显式指定类型,如tasks.register<Copy>("copyTask")。 - Groovy DSL里的
def作用域 在build.gradle里用def定义的变量只在当前作用域可见,跨subprojects会丢失。我建议用ext扩展属性来传递公有变量,虽然麻烦,但不会出幺蛾子。 - 依赖版本冲突 没有声明
constraints时,Gradle会默认使用最高版本。你可以用dependencyLocking锁定版本,或者在buildSrc中统一管理版本号。
我习惯在项目根目录放一个gradle.properties,把公共版本号提出来,比如springBootVersion=2.7.0。这样所有子模块引用时就用springBootVersion变量,改一个地方全局生效。注意:Kotlin DSL里引用方式是property("springBootVersion")。
说实话,build.gradle的精髓不在于你写多少配置,而在于理解Project和Task的关系。你想想看,Gradle把构建过程拆成了一个个Task,再通过依赖将它们串成一张有向无环图。这个设计思想在很多构建工具里都能看到影子。学会了它,以后用Maven的execution或者Bazel的规则,你也不会觉得陌生。