第69章:Gradle构建——Kotlin DSL for Gradle、依赖管理、自定义任务、多模块构建
说实话,很多Kotlin开发者对Gradle又爱又恨。爱的是它强大的构建能力,恨的是那套Groovy DSL的语法——写起来总觉得别扭。我个人习惯用Kotlin DSL来写Gradle脚本,原因很简单:既然项目代码是Kotlin,构建脚本为什么不能用Kotlin?
这一章,我就带你彻底搞懂Gradle构建的四个核心话题:Kotlin DSL、依赖管理、自定义任务、多模块构建。嗯,都是实战中绕不开的东西。
69.1 Kotlin DSL for Gradle:告别Groovy
Gradle从5.0开始正式支持Kotlin DSL。说白了,就是把.gradle文件换成.gradle.kts文件。你想想看,这样你在写构建脚本时,IDE的自动补全、类型检查、重构功能全都能用上——这体验比Groovy好太多了。
我曾经把一个中型项目的Groovy脚本全部迁移到Kotlin DSL,花了大概两天时间。踩了不少坑,但迁移完后的维护体验确实提升了一个档次。
69.1.1 基本配置
先看一个最简单的build.gradle.kts:
plugins {
kotlin("jvm") version "1.9.0"
application
}
repositories {
mavenCentral()
}
dependencies {
implementation(kotlin("stdlib"))
testImplementation(kotlin("test"))
}
application {
mainClass.set("com.example.MainKt")
}
注意看,plugins块里用的是kotlin("jvm")而不是id "org.jetbrains.kotlin.jvm"。这是Kotlin DSL提供的语法糖,我个人觉得更简洁。
69.1.2 类型安全的访问器
Kotlin DSL最大的优势就是类型安全。比如配置sourceSets:
sourceSets {
main {
java.srcDirs("src/main/java", "src/main/kotlin")
resources.srcDirs("src/main/resources")
}
test {
java.srcDirs("src/test/java", "src/test/kotlin")
}
}
在Groovy里,srcDirs是个字符串列表,写错了也不会报错。但在Kotlin DSL里,它是Set<File>类型,IDE会帮你检查类型是否匹配。这感觉,就像从动态语言切到了静态语言——安全多了。
69.2 依赖管理:版本统一与冲突解决
依赖管理是Gradle构建的核心。我见过太多项目因为依赖版本混乱导致编译失败。这里分享几个实战经验。
69.2.1 版本目录(Version Catalog)
从Gradle 7.0开始,官方推荐用版本目录来统一管理依赖版本。在gradle/libs.versions.toml中定义:
[versions]
kotlin = "1.9.0"
coroutines = "1.7.3"
ktor = "2.3.5"
[libraries]
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
然后在build.gradle.kts中引用:
plugins {
alias(libs.plugins.kotlin.jvm)
}
dependencies {
implementation(libs.kotlin.stdlib)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.ktor.server.core)
}
这样做的好处是:所有模块的依赖版本都集中在一个文件里管理。我曾经接手过一个20多个模块的项目,每个模块都自己写版本号,升级依赖时简直是一场噩梦。后来统一用版本目录,一次修改,全局生效。
69.2.2 依赖冲突解决
依赖冲突是家常便饭。比如A库依赖了1.0版本的Guava,B库依赖了2.0版本,Gradle默认会选择最高版本。但有时候这会导致运行时错误。
我的做法是:
configurations.all {
resolutionStrategy {
// 强制使用某个版本
force("com.google.guava:guava:31.1-jre")
// 排除传递依赖
exclude(group = "commons-logging", module = "commons-logging")
// 如果发现冲突,直接报错
failOnVersionConflict()
}
}
failOnVersionConflict()在开发阶段很有用,能帮你尽早发现冲突。但在生产构建中,我建议去掉这个配置,或者只在CI环境中启用。
69.3 自定义任务:让构建更智能
Gradle的自定义任务非常灵活。我经常用它来做一些自动化工作,比如代码生成、文件处理、部署脚本等。
69.3.1 注册一个简单的任务
tasks.register("hello") {
doLast {
println("Hello from Kotlin DSL!")
}
}
运行./gradlew hello就能看到输出。但实际项目中,我们通常需要更复杂的任务。
69.3.2 实战:自动生成版本文件
我曾经需要在构建时自动生成一个version.properties文件,包含当前版本号和构建时间。我是这样做的:
tasks.register<DefaultTask>("generateVersionFile") {
val outputDir = project.layout.buildDirectory.dir("generated/version")
val outputFile = outputDir.map { it.file("version.properties") }
// 配置输入输出
inputs.property("version", project.version.toString())
outputs.file(outputFile)
doLast {
val props = Properties()
props["version"] = project.version.toString()
props["buildTime"] = Instant.now().toString()
props["gitCommit"] = getGitCommitHash()
outputFile.get().asFile.outputStream().use { stream ->
props.store(stream, "Auto-generated version file")
}
}
}
fun getGitCommitHash(): String {
return try {
ProcessBuilder("git", "rev-parse", "--short", "HEAD")
.redirectErrorStream(true)
.start()
.inputStream.bufferedReader().readText().trim()
} catch (e: Exception) {
"unknown"
}
}
这个任务会读取Git的commit hash,结合版本号和构建时间,生成一个properties文件。然后我把它加到processResources的依赖链中:
tasks.named("processResources") {
dependsOn("generateVersionFile")
}
这样每次构建时,版本文件都会自动更新。嗯,这个小功能在排查线上问题时帮了我大忙——能快速知道线上跑的是哪个版本。
69.4 多模块构建:大型项目的基石
当项目变大后,单模块结构就撑不住了。多模块构建能帮你实现代码复用、独立编译、并行构建等好处。
69.4.1 项目结构
一个典型的多模块项目结构如下:
my-project/
├── settings.gradle.kts
├── build.gradle.kts # 根项目配置
├── common/
│ ├── build.gradle.kts
│ └── src/
├── api/
│ ├── build.gradle.kts
│ └── src/
├── service/
│ ├── build.gradle.kts
│ └── src/
└── app/
├── build.gradle.kts
└── src/
在settings.gradle.kts中声明子模块:
rootProject.name = "my-project"
include("common", "api", "service", "app")
69.4.2 依赖关系配置
子模块之间的依赖关系在各自的build.gradle.kts中配置。比如service模块依赖common和api:
// service/build.gradle.kts
dependencies {
implementation(project(":common"))
implementation(project(":api"))
implementation("io.ktor:ktor-server-core:2.3.5")
}
app模块依赖service:
// app/build.gradle.kts
dependencies {
implementation(project(":service"))
}
69.4.3 统一配置管理
多模块项目最头疼的是配置重复。每个子模块都要写一遍kotlin插件、repositories等。我的做法是在根项目的build.gradle.kts中用allprojects或subprojects统一配置:
// 根项目 build.gradle.kts
subprojects {
apply(plugin = "kotlin")
apply(plugin = "java-library")
repositories {
mavenCentral()
}
dependencies {
implementation(kotlin("stdlib"))
}
tasks.withType<Test> {
useJUnitPlatform()
}
}
但要注意,不要把所有配置都放在根项目里。每个子模块特有的配置(比如特定的依赖、插件)应该放在自己的build.gradle.kts中。这样既减少了重复,又保持了模块的独立性。
69.5 知识体系总览
下面这张图总结了本章的核心知识点:
69.6 避坑指南
最后,分享几个我踩过的坑:
- Kotlin DSL 缓存问题:有时候修改了
.gradle.kts文件,但Gradle没有重新加载。运行./gradlew --refresh-dependencies可以强制刷新。 - 版本目录中的版本号:我曾经把版本号写成了字符串"1.0.0",但实际库的版本是"1.0.0.0"。这种问题很难排查,建议用
strictly约束来强制版本匹配。 - 多模块中的循环依赖:如果A模块依赖B,B又依赖A,Gradle会报错。解决办法是提取公共代码到新的模块,或者用接口隔离。
- 自定义任务的增量构建:如果不配置
inputs和outputs,每次构建都会重新执行任务。配置后,只有输入发生变化时才会执行,能显著提升构建速度。
嗯,Gradle构建这块内容确实不少,但掌握了这些核心技能,你就能驾驭从单模块到多模块的各种项目。记住:构建脚本也是代码,值得你认真对待。
公众号:蓝海资料掘金营,微信deep3321