第4章 Kotlin项目构建:Gradle配置、Kotlin编译与依赖管理
做Kotlin项目,绕不开Gradle。说句实话,我见过不少团队在Gradle配置上栽跟头——不是依赖冲突半天解不掉,就是编译缓存在CI上不生效。我自己刚到新公司接手第一个Kotlin微服务时,就被一个诡异的“编译通过但运行时类找不到”的问题折腾了一整天,后来发现是构建脚本里漏了runtimeOnly配置。嗯,这节课咱们就把Gradle配置、Kotlin编译和依赖管理这三大块彻底讲透。
为什么是Gradle?
现在Kotlin项目的官方构建工具就是Gradle,尤其是Kotlin DSL(.kts结尾)已经成为事实标准。你想想看,既然整个项目都用Kotlin写了,构建脚本还用Groovy?那多别扭。我个人习惯全程用build.gradle.kts,IDE提示更友好,类型检查也更严格。
| 对比项 | Groovy DSL | Kotlin DSL |
|---|---|---|
| 文件后缀 | .gradle |
.gradle.kts |
| 类型安全 | 弱类型,容易写错 | 强类型,IDE自动补全 |
| 学习成本 | 需要额外学Groovy | 熟悉Kotlin即可 |
| 性能 | 稍快(但差距不大) | 编译脚本本身需编译 |
建议所有新项目直接上Kotlin DSL。老项目迁移?也别怕,Gradle官方提供了迁移指南,我迁移过两个中型项目,大概花半天就能搞定。
Gradle核心配置:从零搭建一个Kotlin项目
一个标准Kotlin项目的build.gradle.kts长这样:
plugins {
kotlin("jvm") version "1.9.22"
application
}
repositories {
mavenCentral()
}
dependencies {
implementation(kotlin("stdlib"))
testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
}
application {
mainClass.set("com.example.MainKt")
}
tasks.test {
useJUnitPlatform()
}
注意几点:
- 插件声明:
kotlin("jvm")是你唯一需要的Kotlin插件,它会自动配置编译任务。 - 仓库:
mavenCentral()是主力仓库,如果你用公司私有仓库,加maven { url = uri("https://nexus.example.com/repository/maven-public/") }。 - 依赖声明:
implementation表示编译和运行时都需要,testImplementation只在测试阶段生效。 - 测试框架:JUnit 5 是目前主流,
useJUnitPlatform()这行容易忘,我踩过这个坑。
Kotlin编译配置:JVM版本与编译器选项
很多时候你需要在CI里锁定JVM目标版本,避免开发环境和生产环境不一致。比如服务器JDK是17,但本地是21——那编译出来的字节码版本必须是17才安全。
kotlin {
jvmToolchain(17)
compilerOptions {
languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9)
apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9)
freeCompilerArgs.add("-Xjvm-default=all")
}
}
我习惯用 jvmToolchain 来统一JDK版本,它自动下载匹配的JDK,CI镜像里甚至不用装JDK 17。至于 -Xjvm-default=all,这是为了让接口中的默认方法在JVM上正确生成。曾经有个同事没加这个参数,结果接口的默认实现在Java端调用时抛AbstractMethodError……你们猜修了多久?
jvmToolchain 需要先声明工具链仓库:toolchainManagement { jvm { javaRepositories { repository("mavenCentral") } } }
依赖管理:版本统一与冲突解决
依赖管理是构建配置里最头疼的部分。我经手过一个项目,dependencies 块里直接写死了十几个版本号,升级一次要改半天。后来用 version catalog 统一管理:
// gradle/libs.versions.toml
[versions]
kotlin = "1.9.22"
junit = "5.10.0"
coroutines = "1.7.3"
[libraries]
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
然后在build.gradle.kts里引用:
dependencies {
implementation(libs.kotlin.stdlib)
implementation(libs.coroutines.core)
testImplementation(libs.junit.jupiter)
}
这样版本号集中在一个地方,Dependabot自动更新也方便。如果你遇到依赖冲突(比如两个库依赖了不同版本的同个库),用 ./gradlew :app:dependencies 查看依赖树,然后可以用 force 或升级其中一个库来解决。我建议优先升级,别偷懒用force。
多模块项目配置要点
在CI流水线中,多模块项目需要额外注意子模块间的依赖顺序。Gradle会自动处理,但如果你用了api和implementation的区别混淆,会出现编译没问题但运行时类找不到的情况。记住一条原则:
implementation:依赖只对当前模块可见,对外不暴露。api:依赖同时暴露给外部消费者(相当于旧的compile)。
比如你的 core 模块依赖 kafka-clients,但只有 core 内部使用,那就用 implementation。如果需要暴露给其他模块,才用 api。这样做能减少编译时传递依赖的规模,加快构建速度。
SVG:Kotlin项目构建流程
CI集成时需要特别留意的几个点
刚才讲的是本地构建,放到CI流水线里,有几点不一样:
- Gradle Wrapper 必须上传到仓库。CI环境不需要预装Gradle,用
./gradlew就行。我见过有人把.gradle目录也提交了,那是错的,会引发缓存冲突。 - 配置缓存:Gradle 8支持配置缓存,显著加速CI。在
gradle.properties里开启org.gradle.configuration-cache=true。但是注意,如果你的构建脚本里用了System.getenv()之类的不兼容API,会报错。需要做一次适配。 - 依赖缓存:CI Runner最好挂载持久化卷保存
~/.gradle/caches,避免每次重新下载所有依赖。我自己管理的CI流水线,依赖缓存命中率在85%以上,每次构建节省2~3分钟。
cache 关键字来缓存.gradle目录。但注意别把构建产物也混进去——只缓存 ~/.gradle/caches 和 ~/.gradle/wrapper 就够了。
实战:一个典型的build.gradle.kts完整示例
我把上面讲的内容整合成一个相对完整的配置,你可以直接拿去用:
plugins {
kotlin("jvm") version "1.9.22"
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.22"
application
}
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral()
}
dependencies {
implementation(kotlin("stdlib"))
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("io.ktor:ktor-client-core:2.3.6")
testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
kotlin {
jvmToolchain(17)
compilerOptions {
freeCompilerArgs.add("-Xjvm-default=all")
}
}
application {
mainClass.set("com.example.AppKt")
}
tasks.test {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
}
}
说个避坑指南:testRuntimeOnly 有时候会被漏掉,导致JUnit Platform不工作。我去年有一次就是因为这个,测试任务一直在报“No tests found”,折腾了半小时才发现是少了这一行。
总结表格:本章知识速查
| 主题 | 关键点 | 常见错误 |
|---|---|---|
| Gradle DSL | 使用Kotlin DSL,类型安全 | 混用Groovy和Kotlin语法 |
| Kotlin编译器 | 设置jvmToolchain + compilerOptions | 忘记指定JVM版本导致编译产物不兼容 |
| 依赖管理 | version catalog统一版本 | 写死版本号,升级困难 |
| CI缓存 | 缓存~/.gradle/caches/wrapper | 提交.gradle目录到git |
| 测试配置 | useJUnitPlatform() + testLogging | 漏掉junit-platform-launcher |