第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提供的语法糖,我个人觉得更简洁。

小技巧:如果你不确定某个插件的Kotlin DSL写法,可以先在Groovy里写一遍,然后让IntelliJ IDEA帮你自动转换——右键点击.gradle文件,选择"Convert to 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模块依赖commonapi

// 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中用allprojectssubprojects统一配置:

// 根项目 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 知识体系总览

下面这张图总结了本章的核心知识点:

Gradle构建核心 Kotlin DSL .gradle.kts 文件 类型安全访问器 IDE自动补全 依赖管理 版本目录(Version Catalog) 依赖冲突解决 传递依赖管理 自定义任务 任务注册与配置 输入输出增量构建 任务依赖链 多模块构建 settings.gradle.kts 声明子模块 project() 引用子模块依赖 subprojects 统一公共配置 独立编译与并行构建

69.6 避坑指南

最后,分享几个我踩过的坑:

  • Kotlin DSL 缓存问题:有时候修改了.gradle.kts文件,但Gradle没有重新加载。运行./gradlew --refresh-dependencies可以强制刷新。
  • 版本目录中的版本号:我曾经把版本号写成了字符串"1.0.0",但实际库的版本是"1.0.0.0"。这种问题很难排查,建议用strictly约束来强制版本匹配。
  • 多模块中的循环依赖:如果A模块依赖B,B又依赖A,Gradle会报错。解决办法是提取公共代码到新的模块,或者用接口隔离。
  • 自定义任务的增量构建:如果不配置inputsoutputs,每次构建都会重新执行任务。配置后,只有输入发生变化时才会执行,能显著提升构建速度。

嗯,Gradle构建这块内容确实不少,但掌握了这些核心技能,你就能驾驭从单模块到多模块的各种项目。记住:构建脚本也是代码,值得你认真对待


公众号:蓝海资料掘金营,微信deep3321