Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
---
|
||||
title: Gradle и Kotlin DSL
|
||||
description: build.gradle.kts, конфигурации зависимостей, version catalog, таски и настройка компилятора
|
||||
tags:
|
||||
- kotlin
|
||||
- tooling
|
||||
- gradle
|
||||
---
|
||||
|
||||
Gradle - стандарт сборки Kotlin-проектов. Kotlin DSL (`build.gradle.kts`) - это обычный Kotlin-код, поэтому IDE даёт автодополнение, переход к определению и проверку типов, чего не было в Groovy DSL.
|
||||
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0"
|
||||
application
|
||||
}
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
|
||||
testImplementation(kotlin("test"))
|
||||
}
|
||||
|
||||
kotlin { jvmToolchain(21) }
|
||||
|
||||
application { mainClass.set("com.example.MainKt") }
|
||||
|
||||
tasks.test { useJUnitPlatform() }
|
||||
```
|
||||
Это полный рабочий файл. `kotlin("jvm")` - сокращение для `id("org.jetbrains.kotlin.jvm")`, `kotlin("test")` - для `org.jetbrains.kotlin:kotlin-test` версии, совпадающей с плагином.
|
||||
|
||||
## Файлы сборки
|
||||
| Файл | Назначение |
|
||||
|---|---|
|
||||
| `settings.gradle.kts` | имя проекта, список модулей, репозитории плагинов |
|
||||
| `build.gradle.kts` | конфигурация модуля |
|
||||
| `gradle.properties` | флаги Gradle, память JVM |
|
||||
| `gradle/libs.versions.toml` | version catalog |
|
||||
| `gradlew` / `gradlew.bat` | wrapper - фиксирует версию Gradle |
|
||||
| `gradle/wrapper/gradle-wrapper.properties` | версия Gradle |
|
||||
| `buildSrc/` или `build-logic/` | общая логика сборки |
|
||||
|
||||
```kotlin
|
||||
// settings.gradle.kts
|
||||
rootProject.name = "myapp"
|
||||
|
||||
include("core", "api", "app")
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositories { mavenCentral() }
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
# gradle.properties
|
||||
org.gradle.jvmargs=-Xmx4g
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.configuration-cache=true
|
||||
kotlin.daemon.jvmargs=-Xmx2g
|
||||
kotlin.incremental=true
|
||||
```
|
||||
Четыре флага дают основной выигрыш в скорости: параллельная сборка модулей, кеш результатов, кеш конфигурации и инкрементальная компиляция Kotlin.
|
||||
|
||||
## Конфигурации зависимостей
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
dependencies {
|
||||
implementation("io.ktor:ktor-client-core:3.0.0") // нужна при компиляции и в рантайме
|
||||
api("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2") // + видна потребителям модуля
|
||||
compileOnly("org.jetbrains:annotations:26.0.0") // только при компиляции
|
||||
runtimeOnly("ch.qos.logback:logback-classic:1.5.18") // только в рантайме
|
||||
testImplementation(kotlin("test"))
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
|
||||
}
|
||||
```
|
||||
| Конфигурация | Компиляция модуля | Рантайм | Видна потребителям |
|
||||
|---|---|---|---|
|
||||
| `implementation` | да | да | нет |
|
||||
| `api` | да | да | да |
|
||||
| `compileOnly` | да | нет | нет |
|
||||
| `runtimeOnly` | нет | да | да |
|
||||
| `testImplementation` | только тесты | только тесты | нет |
|
||||
| `ksp` | обработка аннотаций | нет | нет |
|
||||
|
||||
Разница `implementation` и `api` - главное решение в многомодульном проекте. `implementation` скрывает транзитивную зависимость: при её изменении перекомпилируется только ваш модуль. `api` экспортирует тип в публичное API модуля - без неё потребитель не увидит типы из зависимости в ваших сигнатурах.
|
||||
|
||||
Правило: `api` только тогда, когда тип из зависимости появляется в публичных сигнатурах. Иначе `implementation` - он быстрее пересобирается и не навязывает зависимость.
|
||||
|
||||
Для библиотеки нужен плагин `java-library`:
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0"
|
||||
`java-library`
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
|
||||
}
|
||||
```
|
||||
Обратные кавычки вокруг `java-library` нужны из-за дефиса: это идентификатор Kotlin, а не строка.
|
||||
|
||||
## Version catalog
|
||||
Версии в одном месте, типизированный доступ из DSL:
|
||||
```
|
||||
# gradle/libs.versions.toml
|
||||
[versions]
|
||||
kotlin = "2.4.0"
|
||||
coroutines = "1.11.0"
|
||||
ktor = "3.0.0"
|
||||
|
||||
[libraries]
|
||||
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
|
||||
ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||
|
||||
[bundles]
|
||||
async = ["coroutines-core", "ktor-client"]
|
||||
|
||||
[plugins]
|
||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
ksp = { id = "com.google.devtools.ksp", version = "2.4.0-2.0.0" }
|
||||
```
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
dependencies {
|
||||
implementation(libs.bundles.async)
|
||||
implementation(libs.coroutines.core)
|
||||
testImplementation(libs.kotlin.test)
|
||||
testImplementation(libs.coroutines.test)
|
||||
}
|
||||
```
|
||||
Дефис в TOML превращается в точку в DSL: `coroutines-core` → `libs.coroutines.core`. Каталог подхватывается автоматически по пути `gradle/libs.versions.toml`, настройка не нужна.
|
||||
|
||||
Преимущество перед `val kotlinVersion = "2.4.0"` в начале файла: каталог работает во всех модулях и виден Dependabot/Renovate для автообновлений.
|
||||
|
||||
## Настройка компилятора
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_21)
|
||||
languageVersion.set(KotlinVersion.KOTLIN_2_4)
|
||||
apiVersion.set(KotlinVersion.KOTLIN_2_2)
|
||||
allWarningsAsErrors.set(true)
|
||||
progressiveMode.set(true)
|
||||
freeCompilerArgs.addAll(
|
||||
"-Xjsr305=strict",
|
||||
"-opt-in=kotlin.RequiresOptIn",
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
| Опция | Зачем |
|
||||
|---|---|
|
||||
| `jvmToolchain(n)` | Gradle сам скачает нужный JDK |
|
||||
| `jvmTarget` | версия байт-кода |
|
||||
| `languageVersion` | какие возможности языка разрешены |
|
||||
| `apiVersion` | какой API stdlib доступен |
|
||||
| `allWarningsAsErrors` | предупреждения ломают сборку |
|
||||
| `progressiveMode` | исправления применяются сразу |
|
||||
| `freeCompilerArgs` | произвольные флаги |
|
||||
|
||||
`jvmToolchain` предпочтительнее `jvmTarget`: он настраивает и Kotlin, и Java, и запуск тестов, устраняя расхождения между машинами. Подробнее про флаги: [kotlinc](kotlinc.md)
|
||||
|
||||
## Таски
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
tasks.register("generateVersion") {
|
||||
val outputDir = layout.buildDirectory.dir("generated/version")
|
||||
val version = project.version.toString()
|
||||
outputs.dir(outputDir)
|
||||
doLast {
|
||||
val file = outputDir.get().file("Version.kt").asFile
|
||||
file.parentFile.mkdirs()
|
||||
file.writeText("const val APP_VERSION = \"$version\"\n")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("compileKotlin") {
|
||||
dependsOn("generateVersion")
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
maxParallelForks = Runtime.getRuntime().availableProcessors() / 2
|
||||
testLogging {
|
||||
events("passed", "failed", "skipped")
|
||||
}
|
||||
}
|
||||
```
|
||||
Важная деталь: значения проекта (`project.version`) читаются **вне** `doLast`, в фазе конфигурации. Обращение к `project` внутри `doLast` несовместимо с configuration cache и даёт предупреждение или ошибку.
|
||||
|
||||
Частые таски:
|
||||
|
||||
| Таск | Что делает |
|
||||
|---|---|
|
||||
| `build` | компиляция + тесты + сборка артефактов |
|
||||
| `assemble` | только артефакты, без тестов |
|
||||
| `test` | тесты |
|
||||
| `clean` | удалить `build/` |
|
||||
| `run` | запуск (плагин `application`) |
|
||||
| `dependencies` | дерево зависимостей |
|
||||
| `tasks --all` | список всех тасков |
|
||||
| `kotlinDslAccessorsReport` | доступные аксессоры DSL |
|
||||
|
||||
```
|
||||
./gradlew build --no-daemon
|
||||
./gradlew test --tests "com.example.UserServiceTest"
|
||||
./gradlew dependencies --configuration runtimeClasspath
|
||||
./gradlew build --scan # отчёт о производительности
|
||||
./gradlew --profile build # локальный профиль сборки
|
||||
```
|
||||
|
||||
## Многомодульный проект
|
||||
```
|
||||
myapp/
|
||||
├── settings.gradle.kts
|
||||
├── build.gradle.kts # общая конфигурация
|
||||
├── gradle/libs.versions.toml
|
||||
├── core/build.gradle.kts
|
||||
├── api/build.gradle.kts
|
||||
└── app/build.gradle.kts
|
||||
```
|
||||
```kotlin
|
||||
// корневой build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0" apply false
|
||||
}
|
||||
|
||||
subprojects {
|
||||
apply(plugin = "org.jetbrains.kotlin.jvm")
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
dependencies {
|
||||
"testImplementation"(kotlin("test"))
|
||||
}
|
||||
}
|
||||
```
|
||||
```kotlin
|
||||
// app/build.gradle.kts
|
||||
dependencies {
|
||||
implementation(project(":core"))
|
||||
implementation(project(":api"))
|
||||
}
|
||||
```
|
||||
`apply false` в корне означает «добавить плагин в classpath, но не применять к корневому проекту» - корневой модуль обычно не содержит кода.
|
||||
|
||||
Строковая форма `"testImplementation"(...)` нужна внутри `subprojects`: типизированный аксессор там недоступен, потому что плагин ещё не применён на момент конфигурации.
|
||||
|
||||
Для серьёзных проектов вместо `subprojects` используют convention plugins в `buildSrc/` или `build-logic/` - они работают с configuration cache и дают нормальную типизацию. Подробнее: [Структура проекта](Структура%20проекта.md)
|
||||
|
||||
## Kotlin DSL против Groovy DSL
|
||||
| Groovy (`build.gradle`) | Kotlin (`build.gradle.kts`) |
|
||||
|---|---|
|
||||
| `implementation 'group:art:1.0'` | `implementation("group:art:1.0")` |
|
||||
| `ext.version = '1.0'` | `val version = "1.0"` |
|
||||
| `task hello { ... }` | `tasks.register("hello") { ... }` |
|
||||
| `apply plugin: 'java'` | `plugins { java }` |
|
||||
| `sourceCompatibility = 1.8` | `java.sourceCompatibility = JavaVersion.VERSION_1_8` |
|
||||
| динамический, без проверок | типизированный, с автодополнением |
|
||||
| быстрее первая конфигурация | требует компиляции скрипта |
|
||||
|
||||
Kotlin DSL - рекомендуемый вариант с Gradle 8. Основная цена - первая сборка медленнее (скрипт компилируется), зато ошибки в конфигурации видны в IDE, а не при запуске.
|
||||
|
||||
## Публикация
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0"
|
||||
`java-library`
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
group = "com.example"
|
||||
version = "1.0.0"
|
||||
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
create<MavenPublication>("maven") {
|
||||
from(components["java"])
|
||||
pom {
|
||||
name.set("My Library")
|
||||
description.set("Описание библиотеки")
|
||||
licenses {
|
||||
license {
|
||||
name.set("Apache-2.0")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
`withSourcesJar()` важен для Kotlin: без исходников IDE не покажет значения по умолчанию и KDoc. `./gradlew publishToMavenLocal` публикует в `~/.m2` для локальной проверки.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java-мир | Kotlin-мир |
|
||||
> |---|---|
|
||||
> | Maven `pom.xml` или Gradle Groovy | Gradle Kotlin DSL |
|
||||
> | `<dependency><scope>compile</scope>` | `implementation` / `api` |
|
||||
> | `maven-compiler-plugin` | `jvmToolchain` / `compilerOptions` |
|
||||
> | `<properties>` для версий | `libs.versions.toml` |
|
||||
> | annotation processor path | конфигурация `ksp` |
|
||||
> | XML, декларативно | код, императивно |
|
||||
>
|
||||
> **Kotlin DSL - код, а не данные.** В Maven `pom.xml` - декларативный документ: инструменты его читают и модифицируют надёжно. `build.gradle.kts` - программа: в ней можно написать цикл, условие, вызов функции. Это даёт гибкость (генерация тасков, вычисляемые версии) и создаёт проблему - статически понять, что делает сборка, нельзя. Отсюда configuration cache: Gradle вынужден кешировать результат исполнения скрипта.
|
||||
>
|
||||
> **`implementation` против `compile` в Maven - это про скорость сборки.** В Maven `compile`-scope экспортирует зависимость транзитивно всегда: потребитель видит всё дерево. Gradle разделил на `api` и `implementation`, и это принципиально: изменение `implementation`-зависимости не вызывает перекомпиляцию потребителей. На больших проектах разница в минутах.
|
||||
>
|
||||
> **Toolchain решает проблему, которой в Maven нет в таком виде.** Maven полагается на JDK, под которым запущен, плюс `maven.compiler.release`. В Kotlin нужно согласовать три вещи: JDK компиляции, `jvmTarget` Kotlin и `targetCompatibility` Java - и расхождение даёт ошибку «Inconsistent JVM-target compatibility». `jvmToolchain(n)` настраивает всё сразу и скачивает JDK при необходимости.
|
||||
>
|
||||
> **Version catalog - ответ на `<dependencyManagement>`.** В Maven версии централизуются через BOM и `<properties>`, в Gradle долго не было стандарта (каждый писал свои `ext`-переменные). `libs.versions.toml` типизирован, виден в IDE и понятен инструментам автообновления.
|
||||
>
|
||||
> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`kotlin("jvm") version` обязателен только в одном месте.** Указание версии в нескольких модулях даёт ошибку «Plugin request for plugin already on the classpath must not include a version». В подмодулях - `kotlin("jvm")` без версии, в корне - с версией и `apply false`.
|
||||
>
|
||||
> **Не указывайте stdlib вручную.** `implementation(kotlin("stdlib"))` избыточен: плагин добавляет его сам, причём правильной версии. Явное указание другой версии даёт конфликт.
|
||||
>
|
||||
> **`jvmTarget` без toolchain ломается при смене JDK.** Локально JDK 21, в CI 17 - сборка упадёт. `jvmToolchain(21)` делает результат воспроизводимым.
|
||||
>
|
||||
> **Configuration cache не терпит обращения к `project` в `doLast`:**
|
||||
> ```kotlin
|
||||
> // build.gradle.kts
|
||||
> // неправильно: project читается во время выполнения
|
||||
> tasks.register("bad") {
|
||||
> doLast { println(project.version) }
|
||||
> }
|
||||
>
|
||||
> // правильно: значение захвачено в фазе конфигурации
|
||||
> tasks.register("good") {
|
||||
> val v = project.version.toString()
|
||||
> doLast { println(v) }
|
||||
> }
|
||||
> ```
|
||||
> Первый вариант даёт ошибку «invocation of 'Task.project' at execution time is unsupported».
|
||||
>
|
||||
> **`api` вместо `implementation` по привычке замедляет сборку.** Транзитивно экспортированная зависимость заставляет перекомпилировать всех потребителей при её изменении. На большом проекте это разница между инкрементальной сборкой за 5 секунд и полной за 2 минуты.
|
||||
>
|
||||
> **`gradlew` нужно коммитить, включая `gradle-wrapper.jar`.** Без него сборка на другой машине использует локальный Gradle другой версии. Некоторые политики безопасности запрещают JAR в репозитории - тогда нужен `gradle/wrapper/gradle-wrapper.properties` плюс проверка контрольной суммы через `validation-action` в CI.
|
||||
>
|
||||
> **Дефис в имени из каталога превращается в точку.** `coroutines-test` в TOML - это `libs.coroutines.test` в DSL. Попытка написать `libs.coroutines-test` не скомпилируется, а `libs.coroutinesTest` не найдётся.
|
||||
>
|
||||
> **Плагины компилятора должны совпадать по версии с Kotlin.** `kotlin("plugin.serialization") version "2.3.0"` при основном плагине 2.4.0 даёт ошибку несовместимости. Держите обе версии в `[versions] kotlin`.
|
||||
>
|
||||
> **Версия KSP привязана к версии Kotlin.** Формат `2.4.0-2.0.0`: первая часть - версия Kotlin, вторая - KSP. Обновление Kotlin требует одновременного обновления KSP. Подробнее: [KSP](KSP.md)
|
||||
>
|
||||
> **`./gradlew clean build` почти всегда лишнее.** `clean` отключает инкрементальную компиляцию и кеш, превращая 10-секундную сборку в минутную. Используйте только при реальных проблемах с кешем.
|
||||
>
|
||||
> **Нехватка памяти демона проявляется как медленная сборка, а не как ошибка.** При `-Xmx` меньше нужного JVM тратит время на GC. Для среднего проекта `org.gradle.jvmargs=-Xmx4g` и `kotlin.daemon.jvmargs=-Xmx2g` - разумный минимум.
|
||||
>
|
||||
> **Устаревшая `apiVersion` вместе с `allWarningsAsErrors` роняет сборку.** В Kotlin 2.4 значение `KOTLIN_2_1` даёт «API version 2.1 is deprecated and its support will be removed in a future version», а `-Werror` превращает предупреждение в ошибку. Поддерживается примерно три предыдущих версии - при обновлении Kotlin проверяйте, что указанная `apiVersion` ещё не устарела.
|
||||
>
|
||||
> **`tasks.create` устарел в пользу `tasks.register`.** `create` конфигурирует таск немедленно, при каждой сборке; `register` - лениво, только если таск нужен. На проектах с сотнями тасков разница заметна.
|
||||
>
|
||||
> **Groovy- и Kotlin-скрипты нельзя смешивать в одном файле.** Переход на Kotlin DSL - это переписывание всего `build.gradle` в `build.gradle.kts`; наличие обоих файлов в модуле даёт ошибку.
|
||||
|
||||
Далее: [Структура проекта](Структура%20проекта.md)
|
||||
Reference in New Issue
Block a user