Kotlin 与 Gradle
用 Gradle Kotlin DSL 构建 Kotlin 项目:依赖管理、版本目录、多模块、K2 编译器配置与构建提速。
概述
Gradle 是 Kotlin 项目最常用的构建工具,而 Kotlin DSL 是 Gradle 官方推荐的构建脚本编写方式(新建项目默认生成)。与传统的 Groovy DSL 相比,Kotlin DSL 提供了编译期类型检查、IDE 自动补全和更好的重构支持。理解 Gradle Kotlin DSL 是搭建和管理 Kotlin 项目的基础。
本文涵盖:单模块项目骨架、依赖管理、版本目录(Version Catalog)、多模块项目、Kotlin 2.x 编译器选项的新 DSL 与常见构建提速手段。
基础概念
- build.gradle.kts:Kotlin DSL 构建脚本,用 Kotlin 代码描述构建逻辑
- settings.gradle.kts:项目设置文件,定义项目名称、子模块与依赖仓库
- gradle.properties:Gradle 属性文件,配置 JVM 参数、缓存开关等
- Plugin(插件):扩展 Gradle 功能,如
kotlin("jvm")、application等 - Task(任务):构建的基本执行单元,如编译、测试、打包
- Configuration(配置):依赖的分组,如
implementation、testImplementation等 - 版本目录(Version Catalog):
gradle/libs.versions.toml集中管理依赖坐标与版本,Gradle 7.4 起稳定
快速上手
创建一个最简单的 Kotlin 项目,需要两个文件:
// settings.gradle.kts
rootProject.name = "my-app"// build.gradle.kts
plugins {
kotlin("jvm") version "2.2.0" // K2 时代版本线,2.0+ 默认 K2 编译器
application
}
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral() // 从 Maven 中央仓库下载依赖
}
dependencies {
implementation(kotlin("stdlib"))
testImplementation(kotlin("test"))
}
application {
mainClass.set("com.example.MainKt")
}
// 统一 JDK 版本(Gradle 会自动探测/下载对应 JDK)
kotlin {
jvmToolchain(21)
}项目结构:
my-app/
build.gradle.kts
settings.gradle.kts
src/
main/kotlin/com/example/Main.kt
test/kotlin/com/example/MainTest.kt常用命令:
./gradlew build # 编译并测试
./gradlew run # 运行应用
./gradlew test # 运行测试
./gradlew clean # 清理构建产物详细用法
依赖管理
// build.gradle.kts
dependencies {
// implementation:编译和运行时都需要,但不会传递给依赖此模块的模块
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
// api:编译和运行时都需要,且会传递给依赖此模块的模块(需应用 kotlin("jvm") 的 api 配置)
api("com.example:shared-library:1.0.0")
// compileOnly:只在编译时需要,运行时不需要
compileOnly("org.projectlombok:lombok:1.18.36")
// runtimeOnly:只在运行时需要
runtimeOnly("com.h2database:h2:2.3.232")
// testImplementation:只在测试编译和运行时需要
testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
testImplementation("io.mockk:mockk:1.13.16")
// 协程测试:runTest 虚拟时间的入口(用法见协程测试篇)
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
}测试依赖的完整选型与框架配置(JUnit 5/Kotest/MockK/Kotlin 协程测试)分别在Kotlin 测试框架集成与Kotlin 协程测试。
使用版本目录管理依赖
版本目录是当前官方推荐的依赖管理方式(替代散落在脚本里的字符串与 buildSrc 常量对象):
# gradle/libs.versions.toml
[versions]
kotlin = "2.2.0"
coroutines = "1.10.2"
ktor = "3.2.0"
[libraries]
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", 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.coroutines.core)
implementation(libs.ktor.server.core)
}多模块项目
// settings.gradle.kts
rootProject.name = "multi-module-app"
include("shared")
include("server")// build.gradle.kts(根项目)
plugins {
kotlin("jvm") version "2.2.0" apply false // 版本只声明一次,子模块不写版本
}// shared/build.gradle.kts
plugins {
kotlin("jvm")
}
dependencies {
implementation(kotlin("stdlib"))
}// server/build.gradle.kts
plugins {
kotlin("jvm")
application
}
dependencies {
implementation(kotlin("stdlib"))
implementation(project(":shared")) // 依赖 shared 模块
implementation("io.ktor:ktor-server-netty-jvm:3.2.0")
}
application {
mainClass.set("com.example.server.MainKt")
}Kotlin 2.x 编译器选项:compilerOptions DSL
Kotlin 2.x 推荐 compilerOptions {} 扩展(类型安全、可复用);旧的 kotlinOptions {} 已进入维护状态并在向新 DSL 迁移:
// build.gradle.kts
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21) // 生成的字节码目标
freeCompilerArgs.addAll(
"-Xjsr305=strict", // 严格模式处理 JSR-305 空安全注解
"-opt-in=kotlin.RequiresOptIn", // 批量 opt-in 实验性 API
)
}
}
// 多模块统一配置:在根项目的 build.gradle.kts 中
subprojects {
plugins.withType<org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin> {
// 所有 Kotlin 编译任务共享的选项
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions {
freeCompilerArgs.add("-Xjsr305=strict")
}
}
}
}自定义 Task
// build.gradle.kts
// 注册自定义任务
tasks.register("copyConfig") {
group = "custom"
description = "复制配置文件"
doLast {
val source = file("config/template.yml")
val target = file("build/config.yml")
target.parentFile.mkdirs()
target.writeText(source.readText())
println("配置文件已复制到 ${target.absolutePath}")
}
}
// 配置已有任务
tasks.withType<Test> {
useJUnitPlatform() // 使用 JUnit 5
testLogging {
events("passed", "failed", "skipped")
}
}
// 任务依赖
tasks.register("buildAndCopy") {
dependsOn("build")
dependsOn("copyConfig")
doLast {
println("构建和复制完成")
}
}常见场景
Spring Boot 项目配置
plugins {
kotlin("jvm") version "2.2.0"
kotlin("plugin.spring") version "2.2.0" // open 化 Spring Bean 类,绕过 final-by-default
kotlin("plugin.jpa") version "2.2.0" // JPA 实体无参构造与延迟加载适配
id("org.springframework.boot") version "3.4.0"
id("io.spring.dependency-management") version "1.1.7"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation(kotlin("reflect"))
runtimeOnly("org.postgresql:postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.withType<Test> {
useJUnitPlatform()
}Ktor 项目配置
Ktor 3.x 的插件与依赖坐标(-jvm 后缀):
plugins {
kotlin("jvm") version "2.2.0"
kotlin("plugin.serialization") version "2.2.0"
id("io.ktor.plugin") version "3.2.0"
application
}
dependencies {
implementation("io.ktor:ktor-server-core-jvm")
implementation("io.ktor:ktor-server-netty-jvm")
implementation("io.ktor:ktor-server-content-negotiation-jvm")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
implementation("ch.qos.logback:logback-classic:1.5.18")
testImplementation("io.ktor:ktor-server-tests-jvm")
}
application {
mainClass.set("com.example.ApplicationKt")
}Android 项目配置(Kotlin 2.0+ Compose)
Kotlin 2.0 起 Compose 编译器随 Kotlin 版本一起发布,改用官方 Compose 编译器插件,不再需要 composeOptions.kotlinCompilerExtensionVersion:
plugins {
id("com.android.application") version "8.7.0"
kotlin("android") version "2.2.0"
alias(libs.plugins.kotlin.compose) // 即 org.jetbrains.kotlin.plugin.compose,版本随 Kotlin
}
android {
namespace = "com.example.myapp"
compileSdk = 35
defaultConfig {
applicationId = "com.example.myapp"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"
}
buildFeatures {
compose = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
dependencies {
implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.compose.material3:material3:1.3.1")
}Gradle 配置优化
# gradle.properties
# 启用并行构建
org.gradle.parallel=true
# 启用构建缓存
org.gradle.caching=true
# 增加 JVM 内存
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
# 配置缓存(Gradle 8.1 起稳定,可大幅缩短重复构建的配置阶段)
org.gradle.configuration-cache=true文档与覆盖率工具链:Dokka 与 Kover
两者都是 Gradle 插件,各三步接入:
// build.gradle.kts
plugins {
id("org.jetbrains.dokka") version "1.9.20"
id("org.jetbrains.kotlinx.kover") version "0.8.3"
}./gradlew dokkaHtml # 生成 KDoc 文档站点(build/dokka/html)
./gradlew koverHtmlReport # 生成覆盖率报告(build/reports/kover)
./gradlew koverVerify # 校验覆盖率阈值(需先配置规则)Dokka 的两个实用点:
- KDoc 语法兼容 Markdown,
[ClassName]双方括号是可点击的跨文件链接,写库/SDK 的团队把它接进 CI,文档随版本发布(dokkaHtml的多模块聚合版叫dokkaHtmlMultiModule,挂在根项目)。 - KDoc 不是 Javadoc:
@param/@return标签类似但属性文档写在属性声明上而非 getter;从 Javadoc 迁移时@author/@since这类标签 Dokka 不渲染——文档为读者服务,别照搬企业模板。
Kover 覆盖率阈值示例(防覆盖率跳水进主干):
kover {
verify {
rule("行覆盖率不低于 60%") {
bound { minValue = 60 } // 百分比
}
}
}易错点:Kover 统计的是 JaCoCo 同源的行/分支覆盖,它测不出「断言写弱了」——覆盖率 100% 只说明每行执行过,不代表每行被验证。变异测试补这个盲区(原理与工具见 CI/CD 测试的变异测试节)。另一个易错点:Android 模块的覆盖率统计需要 kover 与 AGP 的构建变体配合,纯 JVM 模块开箱即用;给整个仓库上阈值前先单模块试跑。
配置缓存与构建缓存出问题时的排查实操
概念在前文「底层原理」已讲(配置缓存序列化任务图,构建缓存复用任务输出)。这里给排障顺序:
# 1. 配置缓存报错:看它生成的报告
./gradlew --configuration-cache
# 报错会给出 HTML 报告路径(build/reports/configuration-cache/),
# 常见两种:构建脚本里用了「构建时未知」的值(读环境变量、File(...) 直接参与任务配置),
# 或第三方插件不兼容。
# 2. 判断是配置慢还是执行慢
./gradlew --profile clean build
# 打开 build/reports/profile/:Summary 里 Configuration 占比高 → 配置缓存有救;
# 任务执行占比高 → 看哪个任务最慢、是否 up-to-date 失效。
# 3. 任务明明没变却重跑:查 up-to-date 失效原因
./gradlew build --info | grep "not up-to-date"
# 典型原因:输入文件时间戳变化(代码生成任务不稳定输出)、
# 任务用了 System.currentTimeMillis() 这类隐式输入、构建缓存未命中(路径大小写/绝对路径混进任务输入)。
# 4. 构建缓存命中率验证
./gradlew clean build # 第一次
./gradlew clean build # 第二次应大量 FROM-CACHE- 报告定位法优先于猜:配置缓存的 HTML 报告直接列出「哪个脚本第几行」用了不兼容 API;先读报告再改代码,避免「注释掉试试」式盲修。
- 易错点:
gradle.properties里的org.gradle.configuration-cache=true对所有任务生效,包括 IDE 同步——插件不兼容时 IDE 同步也失败,回退开关是命令行--no-configuration-cache(临时)与org.gradle.configuration-cache=false(项目级关闭)。 - CI 与本地的缓存行为差异:CI 干净环境只有远程构建缓存可用;本地命中不了常因为
gradle.properties没开org.gradle.caching或任务输入里有本机绝对路径。
注意事项与常见陷阱
- 依赖坐标字符串拼错是最高频错误:优先使用版本目录,坐标集中在一处;
./gradlew dependencies查看依赖树排查版本冲突。 - 脚本内定义的变量作用域只在当前块:在
dependencies {}里定义的val不能在其他块引用(这也是旧教程常出现的复制粘贴 bug),跨脚本共享请用版本目录。 kotlinOptions逐步退役:新项目一律用compilerOptions {};网上旧教程(含jvmTarget = "17"字符串赋值写法)迁移时注意 API 差异。- Kotlin DSL 需要学习成本:如果你之前用 Groovy,切换到 Kotlin DSL 需要适应”类型检查更严、脚本在配置阶段编译”的差异——好处是写错立刻在 IDE 与构建时报错。
- 不要在构建脚本中写复杂逻辑:构建脚本应该简洁,复杂逻辑放在 buildSrc 或 Convention Plugin 中;但 buildSrc 的任何改动会使所有模块重新编译,纯版本常量优先迁去版本目录。
- Gradle Wrapper:始终使用 Gradle Wrapper(
./gradlew),确保团队成员使用相同的 Gradle 版本;Kotlin 2.2 需要较新的 Gradle(8.10+ 为宜),升级 Kotlin 与升级 Gradle 常需要成对进行。
底层原理:构建脚本的一次「编译加执行」
理解 Gradle 行为的关键是把构建分成三个阶段:
- 初始化:settings.gradle.kts 决定哪些模块参与构建;
- 配置:每个 build.gradle.kts 被当作 Kotlin 程序编译并执行——你在脚本里
写的所有语句(包括
dependencies {}块)此刻按顺序运行一遍,产出一张 「任务及其依赖关系」的图; - 执行:Gradle 从任务图中挑出本次命令需要的任务(含 up-to-date 判断)执行。
三个推论能解释大部分「为什么」:
- 为什么 Kotlin DSL 写错坐标会编译报错而 Groovy 只在运行时才炸——配置阶段 本来就是一次编译;
- 为什么「脚本顶层随手写的 println」每次构建都会打印(哪怕只跑
gradlew help), 而doLast {}里的只在任务执行时打印——前者在配置阶段运行,后者在执行阶段运行; - 为什么 buildSrc 一改全量重编——buildSrc 是配置阶段的输入,它变了所有脚本的 编译产物失效;
- 配置缓存为什么能提速——它把「配置阶段的任务图」序列化复用,跳过重复的脚本 编译执行(这也是它对插件兼容性挑剔的原因)。
implementation 与 api 的区别同样能从编译模型推出:api 里的类型会出现在本模块
对外暴露的 ABI(公开函数签名、公开父类)中,所以消费方编译时必须看见它——
传递;implementation 只在本模块内部使用,消费方编译不需要,改版本时也不会
触发下游重编——不传递。判断口诀:这个依赖的类型会出现在我的公开签名里吗?
会就 api,不会就 implementation。
面试题思路:三个高频考法
- 「implementation 和 api 怎么选?」——按上面的 ABI 口诀答,并补一条工程后果:
库项目里把本该 api 的写成 implementation 会让消费方编译失败;反过来全用 api
则依赖树膨胀、版本升级重编面扩大。追问「如何检查」:对库模块跑
./gradlew build的 ABI 校验(Kotlin 显式 api 模式)或依赖分析报告 (./gradlew buildHealth,需 dependency analysis 插件)是加分项。 - 「Gradle 构建慢,你会从哪里入手?」——期望的框架:先测量
(
./gradlew --profile或 build scan 定位慢在配置还是执行);配置慢上配置缓存, 执行慢上并行 + 构建缓存;再查 up-to-date 失效原因(--info里的 「Task :x is not up-to-date because…」)。能说出「不要盲目调 Xmx、先看瓶颈 在哪个阶段」体现测量优先的纪律。 - 「版本目录解决了什么问题?」——三个:坐标与版本集中一处(升级只改 toml); 类型安全的访问器(写错编译报错,IDE 可补全);替代 buildSrc 常量对象带来的 全量重编。能补充「toml 支持 bundle(依赖捆绑)与 plugin 声明」是加分项。
动手实验
- 三个阶段实验:在 build.gradle.kts 顶层加一行
println("配置阶段"),在tasks.register("hello") { doLast { println("执行阶段") } },分别跑./gradlew help、./gradlew hello、./gradlew hello(第二次),观察三者的 输出差异与 up-to-date 行为。 - 依赖传递实验:建 shared(含 api 一个 data class)与 server(implementation 依赖 shared)两模块,在 server 里写一个「公开函数签名引用 shared 类型」的函数, 新建第三个模块消费它——亲眼看到编译失败;把 implementation 改成 api 修复。
- 版本目录迁移实验:把一个散落字符串坐标的项目(或本文快速上手的骨架)迁移到
libs.versions.toml,故意把libs.coroutines.core写错一个字母,观察 IDE 与 构建期报错形态。 - 构建缓存验证:跑两次
./gradlew clean build(第二次开org.gradle.caching=true),对比 BUILD SUCCESSFUL 旁的 up-to-date/ FROM-CACHE 标记;再改一个源文件重跑,观察只有受影响的任务重新执行。
小练习(先自己做,再展开参考实现)
练习 1:给骨架项目装上「发布可用的」构建配置。要求:(a) 全部依赖走版本目录;
(b) 生成可双击运行的 fat jar(提示:application 插件 + jar 任务加
duplicatesStrategy 与 Manifest 主类,或用 shadow 插件);(c) ./gradlew test
输出每个用例的通过/失败状态。先列改动清单,再动手。
参考实现(fat jar 用原生 jar 任务版,不引第三方插件):
# gradle/libs.versions.toml
[versions]
kotlin = "2.2.0"
[libraries]
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }// build.gradle.kts
plugins {
alias(libs.plugins.kotlin.jvm)
application
}
repositories { mavenCentral() }
dependencies {
implementation(libs.kotlin.stdlib)
testImplementation(libs.kotlin.test)
}
application { mainClass.set("com.example.MainKt") }
kotlin { jvmToolchain(21) }
tasks.withType<Test> {
useJUnitPlatform()
testLogging { events("passed", "failed", "skipped") }
}
// fat jar:把运行时依赖解包进同一个 jar 并指定主类
tasks.jar {
manifest { attributes["Main-Class"] = "com.example.MainKt" }
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
}自检问题:为什么 fat jar 要设 DuplicatesStrategy?(答:多个依赖 jar 里都有
META-INF/LICENSE 这类同名文件,解包合并时必须声明遇到重复条目的策略,否则
构建报错。)
练习 2:修一个「构建脚本坏味道」项目。给定如下脚本,指出三处问题并重构:
// build.gradle.kts(问题版)
val coroutinesVersion = "1.10.2"
val ktorVersion = "3.2.0"
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
testImplementation("io.ktor:ktor-server-tests-jvm:$ktorVersion") // ?
api("ch.qos.logback:logback-classic:1.5.18") // ?
}参考答案:三处问题——(a) 版本常量写在脚本内,作用域只在当前文件(陷阱 2),
应迁往版本目录;(b) 测试专用依赖应放 testImplementation 没错,但 Ktor 的
server-tests 坐标用在了依赖 ktor 的项目里却缺少主依赖声明(此处缺
ktor-server-core),依赖来源不完整;(c) logback 是日志实现,只在运行时需要,
api 会把它传递给所有消费方并放大重编面——正确配置是 runtimeOnly。
挑战题(不给参考实现):把一个 Groovy 构建脚本(网上随便找一个教程项目)
翻译成 Kotlin DSL + 版本目录,并把遇到的每一处「翻译不了」记下来(通常集中在
动态字符串与 ext 变量),写五句迁移心得。写完自查:迁移后
./gradlew build --warning-mode all 还有没有警告?
小结
- Kotlin DSL + 版本目录 + Wrapper 是当前 Kotlin 工程的三件套:类型安全、集中管理、版本一致。
- 依赖配置按传递性选择:
implementation默认首选,api用于库的公开类型,runtimeOnly/compileOnly处理特例。 - Kotlin 2.x 用
compilerOptions {}配置编译器,kotlinOptions不再用于新项目;Compose 编译器改用org.jetbrains.kotlin.plugin.compose插件。 - 提速三板斧:并行构建、构建缓存、配置缓存(Gradle 8.1+ 稳定)。
- 编译器插件的原理与更多选项见 Kotlin 编译器插件;Android 工程完整实践见 Kotlin 与 Android。