4. Gradle核心概念:Groovy/Kotlin DSL、Project与Task、依赖管理(implementation/api)、构建生命周期
好,咱们进入Gradle的核心地带。说实话,很多同学用Maven用得很顺手,一转到Gradle就懵了——脚本怎么写?依赖怎么配?生命周期怎么跟Maven不一样?
别急,这一章我把Gradle的四个核心概念掰开揉碎了讲。你只要把这四个东西吃透,Gradle在你手里就跟玩具一样。
4.1 Groovy/Kotlin DSL:构建脚本的两种写法
Gradle的构建脚本,说白了就是用代码来描述构建过程。它支持两种DSL:Groovy和Kotlin。
我个人习惯用Groovy,因为简洁、灵活,写起来快。但如果你团队里都是Kotlin爱好者,那用Kotlin DSL也没问题,类型安全更好。
核心区别:
- Groovy DSL:动态类型,语法灵活,用单引号或双引号都行,闭包用起来很爽。
- Kotlin DSL:静态类型,IDE提示更友好,但语法稍显啰嗦。
来看个对比。这是Groovy写法:
// build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.0'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'junit:junit:4.13.2'
}
这是Kotlin写法:
// build.gradle.kts
plugins {
java
id("org.springframework.boot") version "2.7.0"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
testImplementation("junit:junit:4.13.2")
}
嗯,这里要注意:Groovy里方法调用可以省略括号,Kotlin不行。Groovy里字符串可以用单引号,Kotlin必须双引号。这些细节你写多了自然就记住了。
我的建议:新项目用Kotlin DSL,老项目保持Groovy。别来回切换,团队统一就好。
4.2 Project与Task:构建的基本单元
Gradle里,Project代表一个构建单元(比如一个模块),Task代表一个可执行的操作(比如编译、打包)。
你可以把Project想象成一个容器,里面装着各种Task。每个Project都有自己的build.gradle文件,定义自己的Task和依赖。
Task的定义方式有很多种。最简单的:
// 直接定义一个任务
task hello {
doLast {
println 'Hello, Gradle!'
}
}
运行 gradle hello,就会输出那句话。是不是很简单?
但实际项目中,Task往往有依赖关系。比如编译任务必须在打包任务之前执行。Gradle会自动处理这些依赖。
task compile {
doLast {
println 'Compiling...'
}
}
task packageApp(dependsOn: compile) {
doLast {
println 'Packaging...'
}
}
这里 packageApp 依赖 compile,所以执行 gradle packageApp 时,Gradle会先跑compile,再跑packageApp。
我曾经踩过的坑:在doFirst和doLast里写错了逻辑,导致每次构建都执行了不该执行的操作。记住:doFirst在任务动作之前执行,doLast在之后执行。别搞反了。
4.3 依赖管理:implementation vs api
这是Gradle里最容易让人迷惑的地方。Maven里只有一种依赖范围(compile、test、provided等),但Gradle里多了个api和implementation。
为什么会这样?说白了,是为了控制依赖的传递性。
- implementation:依赖只在当前模块内部可见,不会泄露给依赖当前模块的其他模块。
- api:依赖会暴露给外部,其他模块也能看到这个依赖。
看个例子。假设你有三个模块:lib-a、lib-b、app。
// lib-a/build.gradle
dependencies {
implementation 'com.google.guava:guava:31.1-jre'
api 'org.apache.commons:commons-lang3:3.12.0'
}
// lib-b/build.gradle
dependencies {
implementation project(':lib-a')
}
// app/build.gradle
dependencies {
implementation project(':lib-b')
}
在这个例子里:
- lib-b能使用commons-lang3(因为lib-a用api暴露了它),但不能使用guava(因为lib-a用implementation隐藏了它)。
- app既不能使用guava,也不能使用commons-lang3(因为lib-b用implementation引用了lib-a,没有传递依赖)。
最佳实践:
- 默认用
implementation,减少依赖泄露。 - 只有当你确定这个依赖是模块对外接口的一部分时,才用
api。 - 比如一个工具类库,它暴露了某个第三方库的类型,那就用api。
我记得有一次,团队里有人把所有依赖都配成了api,结果整个项目依赖图乱成一锅粥,编译时间翻了一倍。后来改成implementation,世界清净了。
4.4 构建生命周期:三个阶段
Gradle的构建生命周期分为三个阶段:初始化、配置、执行。
| 阶段 | 做什么 | 典型操作 |
|---|---|---|
| 初始化 | 确定哪些Project参与构建 | 解析settings.gradle,创建Project实例 |
| 配置 | 执行build.gradle脚本,配置Task | 创建Task对象,设置依赖关系 |
| 执行 | 运行选中的Task | 执行doFirst、doLast中的代码 |
你想想看,为什么Gradle比Maven快?一个关键原因就是:Gradle只在执行阶段才真正干活,配置阶段只是搭架子。
举个例子:
task slowTask {
println 'Configuring...' // 配置阶段执行
doLast {
println 'Executing...' // 执行阶段执行
}
}
如果你运行 gradle help,只会输出"Configuring...",不会输出"Executing..."。因为help任务不依赖slowTask,所以slowTask的执行阶段不会被触发。
避坑指南:不要在配置阶段做耗时操作!我曾经见过有人在配置阶段调用网络接口下载文件,结果每次运行任何任务都要等10秒。正确的做法是把耗时操作放到doLast里。
嗯,到这里,Gradle的四个核心概念就讲完了。你可能会问:这些概念怎么串起来用?
其实很简单:你用DSL写脚本,脚本里定义Project和Task,Task之间通过依赖管理关联,整个构建过程由生命周期控制。就这么回事。
下一章,我会带你实战一个多模块项目的构建,把这些概念都用上。到时候你就知道,Gradle比Maven灵活太多了。