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灵活太多了。