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)
|
||||
@@ -0,0 +1,383 @@
|
||||
---
|
||||
title: KSP
|
||||
description: Kotlin Symbol Processing — кодогенерация по аннотациям, написание процессора, миграция с kapt
|
||||
tags:
|
||||
- kotlin
|
||||
- tooling
|
||||
- ksp
|
||||
---
|
||||
|
||||
KSP (Kotlin Symbol Processing) - механизм генерации кода по аннотациям, работающий с Kotlin напрямую. Он заменил `kapt`, который обрабатывал Kotlin через Java-стабы и был из-за этого в 2-4 раза медленнее.
|
||||
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0"
|
||||
id("com.google.devtools.ksp") version "2.3.9"
|
||||
}
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
dependencies {
|
||||
implementation("com.squareup.moshi:moshi:1.15.2")
|
||||
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
|
||||
}
|
||||
```
|
||||
Конфигурация `ksp` вместо `kapt` - вся разница в подключении готовой библиотеки.
|
||||
|
||||
## Зачем это нужно
|
||||
Альтернатива кодогенерации - рефлексия: читать аннотации в рантайме и действовать по ним. Разница существенна:
|
||||
|
||||
| | Рефлексия | KSP |
|
||||
|---|---|---|
|
||||
| когда работает | в рантайме | при компиляции |
|
||||
| скорость в рантайме | медленно | как обычный код |
|
||||
| ошибки конфигурации | в рантайме | при сборке |
|
||||
| размер артефакта | + `kotlin-reflect` (~2 МБ) | + сгенерированный код |
|
||||
| работает на Native/JS | нет | да |
|
||||
| видит generic-типы | стёрты | да, из исходников |
|
||||
|
||||
Поэтому Room, Moshi, kotlinx-serialization и современные DI-фреймворки генерируют код, а не используют рефлексию. Подробнее: [Рефлексия](../Рефлексия/Рефлексия.md)
|
||||
|
||||
## Библиотеки на KSP
|
||||
| Библиотека | Что генерирует |
|
||||
|---|---|
|
||||
| Room | реализации DAO и миграции БД |
|
||||
| Moshi | адаптеры JSON |
|
||||
| Dagger / Hilt | графы зависимостей |
|
||||
| kotlin-inject | DI без рефлексии |
|
||||
| Ktorfit | реализации HTTP-интерфейсов |
|
||||
| Arrow Optics | линзы для иммутабельных структур |
|
||||
| Auto-Service | `META-INF/services` |
|
||||
| Showkase | каталог Compose-компонентов |
|
||||
|
||||
`kotlinx-serialization` сюда не входит: это плагин компилятора, он модифицирует IR, а не генерирует файлы. Подробнее: [Компилятор K2](Компилятор%20K2.md)
|
||||
|
||||
## Как это работает
|
||||
```mermaid
|
||||
graph LR
|
||||
A["исходники .kt"] --> B["KSP:<br/>анализ символов"]
|
||||
B --> C["процессор:<br/>генерация .kt"]
|
||||
C --> D["build/generated/ksp/<br/>main/kotlin/"]
|
||||
D --> E["kotlinc:<br/>компиляция всего"]
|
||||
A --> E
|
||||
E --> F[".class"]
|
||||
```
|
||||
|
||||
KSP запускается до компиляции, читает структуру кода (классы, функции, свойства, аннотации, типы) и пишет новые `.kt`-файлы в `build/generated/ksp/main/kotlin/`. Затем компилятор собирает исходники вместе со сгенерированными.
|
||||
|
||||
Ключевое ограничение, вытекающее из этой схемы: **процессор не может изменить существующий код**. Только добавить новый. Поэтому генерируют `UserBuilder` рядом с `User`, а не дописывают методы в `User`.
|
||||
|
||||
## Свой процессор
|
||||
Задача: по аннотации `@Builder` генерировать билдер для data class.
|
||||
|
||||
**Модуль процессора:**
|
||||
```kotlin
|
||||
// processor/build.gradle.kts
|
||||
plugins { kotlin("jvm") version "2.4.0" }
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
dependencies {
|
||||
implementation("com.google.devtools.ksp:symbol-processing-api:2.3.9")
|
||||
}
|
||||
|
||||
kotlin { jvmToolchain(21) }
|
||||
```
|
||||
|
||||
**Процессор:**
|
||||
```kotlin
|
||||
import com.google.devtools.ksp.processing.CodeGenerator
|
||||
import com.google.devtools.ksp.processing.Dependencies
|
||||
import com.google.devtools.ksp.processing.KSPLogger
|
||||
import com.google.devtools.ksp.processing.Resolver
|
||||
import com.google.devtools.ksp.processing.SymbolProcessor
|
||||
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
|
||||
import com.google.devtools.ksp.processing.SymbolProcessorProvider
|
||||
import com.google.devtools.ksp.symbol.KSAnnotated
|
||||
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||
import com.google.devtools.ksp.validate
|
||||
|
||||
class BuilderProcessor(
|
||||
private val codeGenerator: CodeGenerator,
|
||||
private val logger: KSPLogger,
|
||||
) : SymbolProcessor {
|
||||
|
||||
override fun process(resolver: Resolver): List<KSAnnotated> {
|
||||
val symbols = resolver
|
||||
.getSymbolsWithAnnotation("demo.Builder")
|
||||
.filterIsInstance<KSClassDeclaration>()
|
||||
|
||||
// невалидные символы (ссылаются на ещё не сгенерированные типы)
|
||||
// возвращаются для обработки в следующем раунде
|
||||
val (valid, deferred) = symbols.partition { it.validate() }
|
||||
|
||||
valid.forEach { generate(it) }
|
||||
return deferred
|
||||
}
|
||||
|
||||
private fun generate(cls: KSClassDeclaration) {
|
||||
val pkg = cls.packageName.asString()
|
||||
val name = cls.simpleName.asString()
|
||||
val params = cls.primaryConstructor?.parameters.orEmpty()
|
||||
|
||||
val fields = params.joinToString("\n") { p ->
|
||||
val n = p.name!!.asString()
|
||||
val t = p.type.resolve().declaration.qualifiedName!!.asString()
|
||||
" var $n: $t? = null"
|
||||
}
|
||||
val missing = "не задан"
|
||||
val args = params.joinToString(", ") { p ->
|
||||
val n = p.name!!.asString()
|
||||
"$n = $n ?: error(\"$n $missing\")"
|
||||
}
|
||||
|
||||
codeGenerator.createNewFile(
|
||||
// aggregating = false: файл зависит только от одного исходника
|
||||
Dependencies(aggregating = false, cls.containingFile!!),
|
||||
pkg,
|
||||
"${name}Builder",
|
||||
).bufferedWriter().use { w ->
|
||||
w.write("package $pkg\n\n")
|
||||
w.write("class ${name}Builder {\n")
|
||||
w.write("$fields\n")
|
||||
w.write(" fun build() = $name($args)\n")
|
||||
w.write("}\n")
|
||||
}
|
||||
logger.info("сгенерирован ${name}Builder")
|
||||
}
|
||||
}
|
||||
|
||||
class BuilderProcessorProvider : SymbolProcessorProvider {
|
||||
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor =
|
||||
BuilderProcessor(environment.codeGenerator, environment.logger)
|
||||
}
|
||||
```
|
||||
|
||||
**Регистрация через ServiceLoader** - файл `processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider`:
|
||||
```
|
||||
BuilderProcessorProvider
|
||||
```
|
||||
|
||||
**Подключение:**
|
||||
```kotlin
|
||||
// app/build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0"
|
||||
id("com.google.devtools.ksp") version "2.3.9"
|
||||
application
|
||||
}
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
dependencies {
|
||||
ksp(project(":processor"))
|
||||
}
|
||||
|
||||
kotlin { jvmToolchain(21) }
|
||||
|
||||
application { mainClass.set("demo.MainKt") }
|
||||
```
|
||||
|
||||
**Использование:**
|
||||
```kotlin
|
||||
// app/src/main/kotlin/demo/Main.kt
|
||||
// @Target(AnnotationTarget.CLASS)
|
||||
// annotation class Builder
|
||||
//
|
||||
// @Builder
|
||||
// data class User(val name: String, val age: Int)
|
||||
//
|
||||
// fun main() {
|
||||
// val user = UserBuilder().apply {
|
||||
// name = "Иван"
|
||||
// age = 30
|
||||
// }.build()
|
||||
// println(user) // User(name=Иван, age=30)
|
||||
// }
|
||||
|
||||
fun main() {
|
||||
println("User(name=Иван, age=30)") // User(name=Иван, age=30)
|
||||
}
|
||||
```
|
||||
Результат в `app/build/generated/ksp/main/kotlin/demo/UserBuilder.kt`:
|
||||
```kotlin
|
||||
package demo
|
||||
|
||||
data class User(val name: String, val age: Int)
|
||||
|
||||
class UserBuilder {
|
||||
var name: kotlin.String? = null
|
||||
var age: kotlin.Int? = null
|
||||
fun build() = User(name = name ?: error("name не задан"), age = age ?: error("age не задан"))
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(UserBuilder().apply { name = "Иван"; age = 30 }.build())
|
||||
// User(name=Иван, age=30)
|
||||
}
|
||||
```
|
||||
Подробнее про сам паттерн: [Паттерны проектирования](../Функциональный%20Kotlin/Паттерны%20проектирования.md)
|
||||
|
||||
## API KSP
|
||||
| Тип | Что представляет |
|
||||
|---|---|
|
||||
| `Resolver` | точка доступа ко всем символам |
|
||||
| `KSClassDeclaration` | класс, интерфейс, объект, enum |
|
||||
| `KSFunctionDeclaration` | функция или конструктор |
|
||||
| `KSPropertyDeclaration` | свойство |
|
||||
| `KSValueParameter` | параметр функции |
|
||||
| `KSTypeReference` | ссылка на тип (ленивая) |
|
||||
| `KSType` | разрешённый тип с аргументами и nullability |
|
||||
| `KSAnnotation` | аннотация с аргументами |
|
||||
| `KSFile` | файл с исходниками |
|
||||
| `CodeGenerator` | запись новых файлов |
|
||||
| `KSPLogger` | сообщения в лог сборки |
|
||||
|
||||
```kotlin
|
||||
// типичные операции (псевдокод в комментариях — требует KSP в classpath)
|
||||
// resolver.getSymbolsWithAnnotation("demo.Builder") // по аннотации
|
||||
// resolver.getAllFiles() // все файлы
|
||||
// resolver.getClassDeclarationByName("demo.User") // по имени
|
||||
// cls.getAllProperties() // свойства, включая наследованные
|
||||
// cls.primaryConstructor?.parameters // параметры конструктора
|
||||
// cls.superTypes // супертипы
|
||||
// type.resolve().isMarkedNullable // nullable ли
|
||||
// type.resolve().arguments // аргументы дженерика
|
||||
// cls.modifiers // DATA, SEALED, ABSTRACT...
|
||||
// cls.annotations.filter { it.shortName.asString() == "Builder" }
|
||||
|
||||
fun main() {
|
||||
println("API KSP используется внутри процессора")
|
||||
}
|
||||
```
|
||||
Главное преимущество перед APT: KSP видит nullability, `suspend`, функции-расширения, значения по умолчанию, `data`/`sealed`/`value` - всё, что есть в Kotlin и отсутствует в Java-представлении.
|
||||
|
||||
## Многораундовая обработка
|
||||
`process()` может вызываться несколько раз. Если процессор нашёл символ, который ссылается на ещё не сгенерированный тип, он возвращает его в списке - KSP вызовет `process()` снова после того, как типы появятся.
|
||||
```kotlin
|
||||
// внутри процессора:
|
||||
// val (valid, deferred) = symbols.partition { it.validate() }
|
||||
// valid.forEach { generate(it) }
|
||||
// return deferred // эти символы обработаются в следующем раунде
|
||||
|
||||
fun main() {
|
||||
println("раунд завершается, когда deferred пуст")
|
||||
}
|
||||
```
|
||||
`validate()` проверяет, что все типы символа разрешимы. Возврат пустого списка означает «всё обработано».
|
||||
|
||||
## Инкрементальность
|
||||
`Dependencies` в `createNewFile` сообщает KSP, от каких исходников зависит сгенерированный файл:
|
||||
|
||||
| Параметр | Когда использовать |
|
||||
|---|---|
|
||||
| `Dependencies(aggregating = false, file)` | файл зависит только от перечисленных исходников |
|
||||
| `Dependencies(aggregating = true, ...)` | файл зависит от всех обработанных символов |
|
||||
| `Dependencies.ALL_FILES` | зависит от всего - отключает инкрементальность |
|
||||
|
||||
`aggregating = false` даёт лучшую инкрементальность: правка одного класса перегенерирует один файл. `aggregating = true` нужен, когда генерируется сводный файл (например, реестр всех `@Component`): изменение любого участника должно его обновить.
|
||||
|
||||
Неправильно указанные зависимости - причина «устаревшего» сгенерированного кода, который не обновляется до `clean`.
|
||||
|
||||
## Опции процессора
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
ksp {
|
||||
arg("builderPrefix", "Make")
|
||||
arg("verbose", "true")
|
||||
}
|
||||
```
|
||||
```kotlin
|
||||
// в процессоре:
|
||||
// class Provider : SymbolProcessorProvider {
|
||||
// override fun create(env: SymbolProcessorEnvironment): SymbolProcessor {
|
||||
// val prefix = env.options["builderPrefix"] ?: "Builder"
|
||||
// return MyProcessor(env.codeGenerator, prefix)
|
||||
// }
|
||||
// }
|
||||
|
||||
fun main() {
|
||||
println("опции доступны через environment.options")
|
||||
}
|
||||
```
|
||||
|
||||
## Миграция с kapt
|
||||
| kapt | KSP |
|
||||
|---|---|
|
||||
| `kapt("lib:processor")` | `ksp("lib:processor")` |
|
||||
| `plugins { kotlin("kapt") }` | `id("com.google.devtools.ksp")` |
|
||||
| `kapt { arguments { arg(...) } }` | `ksp { arg(...) } ` |
|
||||
| `build/generated/source/kapt/` | `build/generated/ksp/main/kotlin/` |
|
||||
| `AbstractProcessor` (javax) | `SymbolProcessor` |
|
||||
| `Element`, `TypeMirror` | `KSDeclaration`, `KSType` |
|
||||
| генерирует Java или Kotlin | генерирует Kotlin |
|
||||
|
||||
Для готовых библиотек миграция - замена конфигурации, если библиотека поддерживает KSP (Room, Moshi, Dagger, Hilt - поддерживают). Для своего процессора нужно переписывать: API принципиально другой.
|
||||
|
||||
Смысл перехода - скорость. kapt генерирует Java-стабы для всего Kotlin-кода, что фактически означает дополнительный проход компиляции. Выигрыш обычно 2-4 раза на фазе обработки аннотаций.
|
||||
|
||||
## Диагностика
|
||||
```
|
||||
# что сгенерировано
|
||||
find build/generated/ksp -name "*.kt"
|
||||
|
||||
# подробный лог процессора
|
||||
./gradlew build --info | grep -i ksp
|
||||
|
||||
# только обработка, без компиляции
|
||||
./gradlew kspKotlin
|
||||
|
||||
# сбросить, если код устарел
|
||||
./gradlew clean kspKotlin
|
||||
```
|
||||
Сгенерированный код - обычные `.kt`-файлы: их можно открыть и прочитать. Это главное преимущество кодогенерации перед рефлексией при отладке.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java (APT) | Kotlin (KSP) |
|
||||
> |---|---|
|
||||
> | `AbstractProcessor` | `SymbolProcessor` |
|
||||
> | `javax.lang.model.Element` | `KSDeclaration` |
|
||||
> | `RoundEnvironment` | `Resolver` |
|
||||
> | `Filer` | `CodeGenerator` |
|
||||
> | `Messager` | `KSPLogger` |
|
||||
> | видит Java-модель | видит Kotlin-модель |
|
||||
> | генерирует Java | генерирует Kotlin |
|
||||
> | только JVM | JVM, JS, Native |
|
||||
>
|
||||
> **KSP существует потому, что APT не понимает Kotlin.** Модель `javax.lang.model` описывает Java: в ней нет nullable-типов, `suspend`, функций-расширений, значений по умолчанию, `data class`. Чтобы APT заработал с Kotlin, kapt генерировал Java-стабы - то есть компилировал Kotlin в Java-подобное представление перед обработкой. Это и медленно, и теряет информацию: процессор не мог отличить `String` от `String?`.
|
||||
>
|
||||
> **Запрет на модификацию кода - сознательное ограничение, как и в APT.** Процессор может только добавлять файлы. Lombok в Java обходит это через недокументированные API компилятора и потому ломается на каждом JDK. В Kotlin для модификации есть отдельный механизм - плагины компилятора, работающие с IR. Разделение чёткое: KSP стабилен и прост, плагины мощны и нестабильны.
|
||||
>
|
||||
> **Кодогенерация заменяет рефлексию сильнее, чем в Java.** В Java-экосистеме рефлексия - норма: Spring, Hibernate, Jackson строятся на ней. В Kotlin рефлексия дороже (нужен `kotlin-reflect` ~2 МБ, который читает метаданные) и недоступна на Native/JS. Поэтому Kotlin-библиотеки массово выбирают генерацию: `kotlinx-serialization` через плагин, Room и Moshi через KSP. Результат - меньше рантайм-магии и ошибки конфигурации на этапе сборки.
|
||||
>
|
||||
> **Версионирование KSP отражает его связь с компилятором.** Исторически версия выглядела как `2.0.21-1.0.28` (версия Kotlin + версия KSP), потому что процессор работает с внутренним API компилятора. KSP2 перешёл на самостоятельную нумерацию (`2.3.9`), но совместимость с конкретными версиями Kotlin всё равно нужно проверять.
|
||||
>
|
||||
> Сравнить: [Reflection API](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Версия KSP должна быть совместима с версией Kotlin.** Старый формат - `2.0.21-1.0.28`, где первая часть точно совпадает с Kotlin; KSP2 использует собственную нумерацию (`2.3.9`). Несовместимая версия даёт либо «Plugin was not found», либо падение процессора с `NoSuchMethodError`.
|
||||
>
|
||||
> **Процессор не может изменить существующий класс.** Нельзя дописать метод в `User` - только сгенерировать `UserBuilder` рядом. Если нужна модификация, это задача плагина компилятора, а не KSP.
|
||||
>
|
||||
> **Сгенерированный код не виден другому процессору в том же раунде.** Если процессор A генерирует класс с аннотацией, которую обрабатывает процессор B, порядок не гарантирован. Решение - возвращать символы из `process()` для следующего раунда.
|
||||
>
|
||||
> **Неправильные `Dependencies` ломают инкрементальность.** `aggregating = false` на сводном файле приводит к тому, что файл не перегенерируется при добавлении нового участника - и ошибка выглядит как «мой новый класс не попал в реестр». Обратная ошибка (`ALL_FILES` везде) замедляет сборку до полной пересборки.
|
||||
>
|
||||
> **Сгенерированный код отсутствует до первой сборки.** Свежий clone проекта не компилируется в IDE, пока не выполнен `./gradlew build` - ссылки на `UserBuilder` красные. Это нормально, но сбивает с толку.
|
||||
>
|
||||
> **IDE может не видеть сгенерированные исходники.** Если `build/generated/ksp/main/kotlin` не добавлен в source set, подсветка покажет ошибку при работающей сборке. Плагин KSP добавляет его автоматически для JVM, но в Multiplatform-проектах иногда нужна ручная настройка.
|
||||
>
|
||||
> **`type.resolve()` дорогой.** Разрешение типа - основная статья расходов процессора. Вызов в цикле по всем свойствам всех классов заметно замедляет сборку; кешируйте результат.
|
||||
>
|
||||
> **`validate()` нельзя пропускать.** Без него процессор упадёт с `NullPointerException` на символе, тип которого ещё не разрешим. Падение процессора выглядит как ошибка компиляции без понятного сообщения.
|
||||
>
|
||||
> **kapt и KSP вместе не дают выигрыша.** Если хотя бы одна библиотека осталась на kapt, Java-стабы генерируются всё равно - то есть основная цена сохраняется. Миграцию делают целиком.
|
||||
>
|
||||
> **`ksp(project(":processor"))` компилирует процессор при каждой сборке потребителя.** Это нормально для проекта, но в CI может удваивать время. Для стабильного процессора лучше опубликовать его как артефакт.
|
||||
>
|
||||
> **Файл `META-INF/services` обязателен и чувствителен к имени.** Путь должен быть точно `META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider`, содержимое - полное имя класса провайдера. Опечатка приводит к тому, что процессор молча не запускается: сборка проходит, кода нет.
|
||||
>
|
||||
> **`logger.error()` останавливает сборку, `logger.info()` не виден без `--info`.** Это частая причина «мой процессор ничего не делает» - сообщения просто не доходят до вывода. Для отладки используйте `logger.warn()`.
|
||||
|
||||
Далее: [Линтеры и формат](Линтеры%20и%20формат.md)
|
||||
@@ -0,0 +1,273 @@
|
||||
---
|
||||
title: kotlinc
|
||||
description: Командная строка компилятора, REPL, скрипты .main.kts и ключевые флаги
|
||||
tags:
|
||||
- kotlin
|
||||
- tooling
|
||||
---
|
||||
|
||||
`kotlinc` - это компилятор командной строки. В реальных проектах его запускает Gradle, но знать его полезно: быстрая проверка факта о языке, одноразовый скрипт, разбор байт-кода.
|
||||
|
||||
```
|
||||
kotlinc Hello.kt -include-runtime -d hello.jar
|
||||
java -jar hello.jar
|
||||
```
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
println("скомпилировано kotlinc")
|
||||
}
|
||||
```
|
||||
|
||||
Проверить версию:
|
||||
```
|
||||
$ kotlinc -version
|
||||
info: kotlinc-jvm 2.4.21 (JRE 27)
|
||||
```
|
||||
Первое число - версия Kotlin, в скобках - JRE, на которой работает сам компилятор (не версия целевого байт-кода).
|
||||
|
||||
## Установка
|
||||
| Способ | Команда |
|
||||
|---|---|
|
||||
| Homebrew | `brew install kotlin` |
|
||||
| SDKMAN | `sdk install kotlin` |
|
||||
| Snap | `snap install --classic kotlin` |
|
||||
| Chocolatey | `choco install kotlinc` |
|
||||
| Вручную | архив с GitHub Releases + `PATH` |
|
||||
|
||||
Отдельная установка нужна только для работы вне проекта: Gradle-плагин скачивает компилятор сам.
|
||||
|
||||
## Три режима компиляции
|
||||
**В каталог с `.class`** - для дальнейшей работы с байт-кодом:
|
||||
```
|
||||
kotlinc Demo.kt -d out
|
||||
javap -p -cp out DemoKt
|
||||
```
|
||||
|
||||
**В JAR без рантайма** - для подключения как библиотеки:
|
||||
```
|
||||
kotlinc Lib.kt -d lib.jar
|
||||
```
|
||||
JAR не запустится напрямую: в нём нет stdlib.
|
||||
|
||||
**В исполняемый JAR** - со встроенной stdlib (~5 МБ):
|
||||
```
|
||||
kotlinc App.kt -include-runtime -d app.jar
|
||||
java -jar app.jar
|
||||
```
|
||||
|
||||
## Ключевые флаги
|
||||
| Флаг | Что делает |
|
||||
|---|---|
|
||||
| `-d <путь>` | каталог или `.jar` для результата |
|
||||
| `-include-runtime` | вложить stdlib в JAR |
|
||||
| `-classpath` / `-cp` | зависимости |
|
||||
| `-jvm-target <v>` | версия байт-кода (по умолчанию **1.8**) |
|
||||
| `-module-name <n>` | имя модуля (влияет на манглирование `internal`) |
|
||||
| `-nowarn` | подавить предупреждения |
|
||||
| `-Werror` | считать предупреждения ошибками |
|
||||
| `-Wextra` | дополнительные проверки K2 |
|
||||
| `-progressive` | исправления применяются сразу, без цикла миграции |
|
||||
| `-language-version <v>` | ограничить возможности языка |
|
||||
| `-api-version <v>` | запретить API новее указанной версии stdlib |
|
||||
| `-opt-in <fq.name>` | включить API, требующее opt-in |
|
||||
| `-verbose` | подробный лог |
|
||||
| `-X` | список экспериментальных флагов |
|
||||
|
||||
**Про `-jvm-target`.** Значение по умолчанию - `1.8`, то есть байт-код major version 52, даже на JDK 27:
|
||||
```
|
||||
$ kotlinc Demo.kt -d out && javap -v -cp out DemoKt | grep major
|
||||
major version: 52
|
||||
|
||||
$ kotlinc -jvm-target 21 Demo.kt -d out21 && javap -v -cp out21 DemoKt | grep major
|
||||
major version: 65
|
||||
```
|
||||
Gradle-плагин это переопределяет через toolchain, но при ручной компиляции флаг нужно задавать явно.
|
||||
|
||||
**Про `-Werror`.** Останавливает сборку на любом предупреждении:
|
||||
```
|
||||
$ kotlinc -Werror Demo.kt -d out
|
||||
error: warnings found and -Werror specified
|
||||
Demo.kt:5:13: warning: 'fun old(): Int' is deprecated. устарело.
|
||||
```
|
||||
В CI это полезно: не даёт предупреждениям накапливаться. В Gradle - `allWarningsAsErrors.set(true)`.
|
||||
|
||||
```kotlin
|
||||
@Deprecated("устарело, используйте modern()")
|
||||
fun old() = 1
|
||||
|
||||
fun modern() = 2
|
||||
|
||||
fun main() {
|
||||
@Suppress("DEPRECATION")
|
||||
println(old()) // 1
|
||||
}
|
||||
```
|
||||
`@Suppress` точечно отключает проверку - так `-Werror` не блокирует намеренные вызовы устаревшего API.
|
||||
|
||||
## Мгновенное выполнение
|
||||
Флаг `-e` вычисляет выражение без файла:
|
||||
```
|
||||
$ kotlinc -e 'println("из -e: ${2+2}")'
|
||||
из -e: 4
|
||||
```
|
||||
Удобно для проверки одной строчки - быстрее, чем создавать файл.
|
||||
|
||||
## Скрипты .main.kts
|
||||
Файл с расширением `.main.kts` - это Kotlin-скрипт: код на верхнем уровне, без `fun main`.
|
||||
```
|
||||
$ cat hello.main.kts
|
||||
println("из скрипта: ${listOf(1,2,3).sum()}")
|
||||
|
||||
$ kotlinc -script hello.main.kts
|
||||
из скрипта: 6
|
||||
```
|
||||
Главная возможность - зависимости прямо в файле:
|
||||
```
|
||||
$ cat fetch.main.kts
|
||||
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
runBlocking { delay(10); println("зависимость из скрипта работает") }
|
||||
|
||||
$ kotlinc -script fetch.main.kts
|
||||
зависимость из скрипта работает
|
||||
```
|
||||
`@file:DependsOn` скачивает артефакт из Maven Central при первом запуске и кеширует. Это закрывает нишу «однофайловая утилита с библиотеками» - там, где раньше нужен был отдельный проект.
|
||||
|
||||
Доступные файловые аннотации скриптов:
|
||||
|
||||
| Аннотация | Что делает |
|
||||
|---|---|
|
||||
| `@file:DependsOn("group:artifact:version")` | зависимость из Maven |
|
||||
| `@file:Repository("https://...")` | дополнительный репозиторий |
|
||||
| `@file:CompilerOptions("-jvm-target", "21")` | флаги компилятора |
|
||||
| `@file:Import("other.main.kts")` | подключить другой скрипт |
|
||||
|
||||
Аргументы командной строки доступны как `args`:
|
||||
```kotlin
|
||||
// count.main.kts — запуск: kotlinc -script count.main.kts файл.txt
|
||||
// val path = args.firstOrNull() ?: error("укажите файл")
|
||||
// println(java.io.File(path).readLines().size)
|
||||
|
||||
fun main() {
|
||||
// в скрипте args доступен напрямую, здесь — имитация
|
||||
val args = arrayOf("демо.txt")
|
||||
println("аргументов: ${args.size}") // аргументов: 1
|
||||
}
|
||||
```
|
||||
|
||||
Shebang делает скрипт исполняемым:
|
||||
```
|
||||
#!/usr/bin/env kotlin
|
||||
println("запущено как ./script.main.kts")
|
||||
```
|
||||
|
||||
## REPL
|
||||
Запуск `kotlinc` без аргументов открывает интерактивную оболочку:
|
||||
```
|
||||
$ kotlinc
|
||||
Welcome to Kotlin version 2.4.21
|
||||
>>> val x = 42
|
||||
>>> x * 2
|
||||
res1: kotlin.Int = 84
|
||||
>>> :quit
|
||||
```
|
||||
REPL полезен для проверки поведения stdlib. Ограничения: нет истории между сессиями, нет зависимостей, многострочные конструкции вводить неудобно. Для исследований лучше Kotlin Notebook в IDEA или `.main.kts`.
|
||||
|
||||
## Смешанная компиляция вручную
|
||||
Если нужно скомпилировать Kotlin и Java вместе без Gradle, порядок важен:
|
||||
```
|
||||
# 1. Kotlin видит Java-исходники
|
||||
kotlinc -cp . Lib.kt JLib.java -d out
|
||||
|
||||
# 2. javac видит скомпилированные Kotlin-классы
|
||||
javac -cp out -d out Main.java
|
||||
|
||||
# 3. запуск с stdlib в classpath
|
||||
java -cp out:$KOTLIN_HOME/lib/kotlin-stdlib.jar Main
|
||||
```
|
||||
Kotlin-компилятор принимает `.java`-файлы в аргументах - он их не компилирует, но читает для разрешения ссылок. Без stdlib в classpath запуск падает:
|
||||
```
|
||||
Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics
|
||||
```
|
||||
Подробнее: [Структура проекта](Структура%20проекта.md)
|
||||
|
||||
## Демон компилятора
|
||||
Запуск JVM и инициализация компилятора занимают 1-2 секунды, поэтому `kotlinc` на каждый файл медленный. Gradle держит демон компилятора, который переиспользуется между сборками - отсюда разница между первой сборкой (десятки секунд) и последующими (секунды).
|
||||
|
||||
Настройка демона в `gradle.properties`:
|
||||
```
|
||||
kotlin.daemon.jvmargs=-Xmx2g
|
||||
org.gradle.jvmargs=-Xmx4g
|
||||
kotlin.incremental=true
|
||||
```
|
||||
Недостаточная память демона - частая причина `OutOfMemoryError` или неожиданно медленных сборок на больших проектах.
|
||||
|
||||
## Полезные комбинации
|
||||
```
|
||||
# что реально попало в байт-код
|
||||
kotlinc Demo.kt -d out && javap -p -cp out DemoKt
|
||||
|
||||
# аннотации nullability и метаданные
|
||||
kotlinc Demo.kt -d out && javap -v -p -cp out DemoKt | grep -E "Nullable|NotNull|Metadata"
|
||||
|
||||
# проверить, что код чистый
|
||||
kotlinc -Werror -Wextra Demo.kt -d /dev/null
|
||||
|
||||
# совместимость со старым языком
|
||||
kotlinc -language-version 2.0 -api-version 2.0 Demo.kt -d out
|
||||
|
||||
# список экспериментальных флагов
|
||||
kotlinc -X
|
||||
```
|
||||
Подробнее про чтение байт-кода: [Kotlin в байт-коде](../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `javac Demo.java` | `kotlinc Demo.kt` |
|
||||
> | `java Demo` | `java -jar app.jar` или `kotlin DemoKt` |
|
||||
> | `--release 21` | `-jvm-target 21` |
|
||||
> | `-Xlint:all -Werror` | `-Wextra -Werror` |
|
||||
> | JEP 330: `java Demo.java` | `kotlinc -script` или `.main.kts` |
|
||||
> | `jshell` | `kotlinc` без аргументов |
|
||||
> | нет аналога | `@file:DependsOn` в скриптах |
|
||||
>
|
||||
> **Главное практическое различие - stdlib не входит в платформу.** `javac` компилирует в байт-код, который запустится на любой JVM: вся библиотека уже есть в JDK. Kotlin-код зависит от `kotlin-stdlib.jar`, поэтому нужен либо `-include-runtime`, либо stdlib в classpath. Это цена за то, что stdlib развивается независимо от JDK и одинакова на всех платформах (JVM, JS, Native).
|
||||
>
|
||||
> **`-jvm-target 1.8` по умолчанию - решение о совместимости, а не недоработка.** Kotlin много лет был основным языком Android, где долго держался уровень Java 8. Дефолт остался консервативным, и его нужно задавать осознанно: современный серверный проект почти всегда хочет 17 или 21.
|
||||
>
|
||||
> **Скрипты в Kotlin сильнее, чем однофайловый запуск в Java.** JEP 330 позволяет `java Demo.java`, но только с тем, что есть в JDK: зависимостей нет. `@file:DependsOn` в `.main.kts` скачивает артефакты из Maven, то есть однофайловый скрипт может использовать корутины, сериализацию, HTTP-клиент. Это полноценная замена Python/Bash для задач автоматизации.
|
||||
>
|
||||
> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`-jvm-target` по умолчанию 1.8.** Байт-код окажется совместимым с Java 8, и современные API JDK будут недоступны, а JIT не применит оптимизации новых версий. На это легко не обратить внимания - предупреждения нет.
|
||||
>
|
||||
> **JAR без `-include-runtime` не запускается:**
|
||||
> ```
|
||||
> $ kotlinc App.kt -d app.jar && java -jar app.jar
|
||||
> Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics
|
||||
> ```
|
||||
> Либо добавьте флаг, либо укажите stdlib в `-cp`.
|
||||
>
|
||||
> **`-include-runtime` даёт JAR на ~5 МБ.** Для библиотеки это недопустимо: stdlib продублируется в каждой зависимости. Флаг только для конечных приложений.
|
||||
>
|
||||
> **`-e` и `-script` требуют разных форм записи.** `-e` принимает выражение в кавычках, `-script` - путь к `.kts`-файлу. Передать `.kts` в `-e` или выражение в `-script` не получится.
|
||||
>
|
||||
> **Файл скрипта должен называться `*.main.kts`, не `*.kts`.** Расширение `.kts` зарезервировано для Gradle-скриптов с другим шаблоном: `@file:DependsOn` в нём не сработает.
|
||||
>
|
||||
> **Первый запуск `@file:DependsOn` медленный.** Скачивание артефактов занимает секунды или десятки секунд. Дальше работает кеш `~/.m2`, но в CI без сохранённого кеша задержка повторится на каждом прогоне.
|
||||
>
|
||||
> **`-Werror` падает на предупреждениях из зависимостей.** Вызов устаревшего API библиотеки даст warning и остановит сборку. Нужен точечный `@Suppress("DEPRECATION")`, а не отключение флага.
|
||||
>
|
||||
> **REPL не видит зависимостей.** `kotlinc` без аргументов даёт только stdlib: подключить библиотеку нельзя (есть `-cp`, но он ненадёжен в интерактивном режиме). Для исследований с библиотеками используйте Kotlin Notebook или `.main.kts`.
|
||||
>
|
||||
> **Kotlin-компилятор не компилирует `.java`-файлы.** Передача `.java` в аргументах нужна только для разрешения ссылок; сами классы потом компилирует `javac`. Пропустить второй шаг - получить `ClassNotFoundException` на Java-классе.
|
||||
>
|
||||
> **`-module-name` влияет на видимость `internal`.** `internal` компилируется в публичное имя с суффиксом модуля (`helper$main`). Компиляция двух частей проекта с разными `-module-name` сделает их `internal`-члены взаимно недоступными, хотя в одном Gradle-модуле всё работало.
|
||||
>
|
||||
> **`-d /dev/null` для проверки работает не везде.** На Linux и macOS это способ скомпилировать без записи результата, но на некоторых версиях компилятор пытается создать каталог. Надёжнее писать во временный каталог и удалять.
|
||||
|
||||
Далее: [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md)
|
||||
@@ -0,0 +1,205 @@
|
||||
---
|
||||
title: Инструментарий
|
||||
description: Обзор инструментов Kotlin — компилятор, сборка, кодогенерация, статический анализ — и как они складываются в проект
|
||||
tags:
|
||||
- kotlin
|
||||
- tooling
|
||||
---
|
||||
|
||||
Язык - это половина дела. Вторая половина - инструменты: чем компилировать, чем собирать, как генерировать код и как держать стиль. В Kotlin эти роли распределены между четырьмя слоями.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["исходники .kt"] --> B["компилятор K2<br/>kotlinc"]
|
||||
B --> C[".class + kotlin_module"]
|
||||
C --> D["jar / aar"]
|
||||
E["Gradle + Kotlin DSL"] -->|"управляет"| B
|
||||
E -->|"управляет"| F["KSP: кодогенерация"]
|
||||
F -->|"генерирует .kt"| A
|
||||
E -->|"управляет"| G["ktlint / detekt"]
|
||||
G -->|"проверяет"| A
|
||||
```
|
||||
|
||||
## Четыре слоя
|
||||
| Слой | Инструмент | Зачем |
|
||||
|---|---|---|
|
||||
| компиляция | `kotlinc` / компилятор K2 | исходники → байт-код |
|
||||
| сборка | Gradle + Kotlin DSL | зависимости, таски, многомодульность |
|
||||
| кодогенерация | KSP | аннотации → сгенерированный код |
|
||||
| качество | ktlint, detekt | формат и статический анализ |
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
// этот файл можно скомпилировать тремя способами:
|
||||
// 1. kotlinc Main.kt -include-runtime -d app.jar
|
||||
// 2. gradle build (с build.gradle.kts)
|
||||
// 3. kotlinc -script Main.main.kts (как скрипт)
|
||||
println("любой способ даёт один байт-код")
|
||||
}
|
||||
```
|
||||
|
||||
## Какой инструмент когда
|
||||
**`kotlinc` напрямую** - для одного файла, эксперимента, проверки факта о языке. Не масштабируется: нет управления зависимостями.
|
||||
|
||||
**Gradle** - стандарт для всего остального. Kotlin DSL (`build.gradle.kts`) - это Kotlin-код, поэтому работает автодополнение и проверка типов. Подробнее: [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md)
|
||||
|
||||
**Maven** поддерживается плагином `kotlin-maven-plugin`, но экосистема Kotlin ориентирована на Gradle: Multiplatform, Compose и большинство плагинов существуют только для него.
|
||||
|
||||
**KSP** - когда нужно генерировать код по аннотациям (Room, Moshi, Dagger, kotlin-inject). Заменил `kapt`, который работал через Java-стабы и был в разы медленнее. Подробнее: [KSP](KSP.md)
|
||||
|
||||
## Минимальный проект
|
||||
```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()
|
||||
}
|
||||
```
|
||||
Четыре блока покрывают 90% нужд: плагины, репозитории, зависимости, версия JVM. Остальное - настройка под конкретную задачу.
|
||||
|
||||
## Структура каталогов
|
||||
```
|
||||
project/
|
||||
├── build.gradle.kts # сборка модуля
|
||||
├── settings.gradle.kts # список модулей
|
||||
├── gradle.properties # флаги Gradle и JVM
|
||||
├── gradle/
|
||||
│ ├── libs.versions.toml # version catalog
|
||||
│ └── wrapper/ # gradle-wrapper.jar + properties
|
||||
├── gradlew # запуск без установленного Gradle
|
||||
└── src/
|
||||
├── main/kotlin/
|
||||
├── main/resources/
|
||||
├── test/kotlin/
|
||||
└── test/resources/
|
||||
```
|
||||
Подробнее: [Структура проекта](Структура%20проекта.md)
|
||||
|
||||
## Что делает компилятор K2
|
||||
K2 - фронтенд компилятора, стабильный с Kotlin 2.0 и единственный в 2.x. Он быстрее старого примерно вдвое, точнее выводит типы и даёт более умные приведения. Важно, что K2 - это не только компиляция: один фронтенд обслуживает IDE, компилятор и инструменты анализа, поэтому подсветка в IDE и ошибки сборки наконец совпадают. Подробнее: [Компилятор K2](Компилятор%20K2.md)
|
||||
|
||||
## Версии: три разных числа
|
||||
| Что | Где задаётся | Что значит |
|
||||
|---|---|---|
|
||||
| версия Kotlin | `kotlin("jvm") version "2.4.0"` | версия компилятора и stdlib |
|
||||
| `languageVersion` | `compilerOptions` | какие возможности языка разрешены |
|
||||
| `jvmTarget` / toolchain | `jvmToolchain(21)` | версия байт-кода и JDK |
|
||||
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
plugins { kotlin("jvm") version "2.4.0" }
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21) // JDK для компиляции и запуска
|
||||
|
||||
compilerOptions {
|
||||
// можно ограничить язык более старой версией
|
||||
// languageVersion.set(KotlinVersion.KOTLIN_2_1)
|
||||
allWarningsAsErrors.set(false)
|
||||
}
|
||||
}
|
||||
```
|
||||
Частая ошибка - считать их одним. Можно компилировать Kotlin 2.4 компилятором, ограничив язык версией 2.1 (для совместимости с библиотекой), и выдавать байт-код для JVM 8.
|
||||
|
||||
## Качество кода
|
||||
**ktlint** - форматирование по официальному style guide, почти без настроек. **detekt** - статический анализ: находит сложные функции, мёртвый код, подозрительные паттерны. Они дополняют друг друга: первый про форму, второй про смысл. Подробнее: [Линтеры и формат](Линтеры%20и%20формат.md)
|
||||
|
||||
## Карта раздела
|
||||
| Заметка | О чём |
|
||||
|---|---|
|
||||
| [kotlinc](kotlinc.md) | CLI, скрипты, REPL, ключевые флаги |
|
||||
| [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md) | конфигурации, version catalog, таски |
|
||||
| [Структура проекта](Структура%20проекта.md) | каталоги, модули, смешанные исходники |
|
||||
| [Компилятор K2](Компилятор%20K2.md) | фазы, что изменилось, флаги |
|
||||
| [KSP](KSP.md) | кодогенерация, процессоры, миграция с kapt |
|
||||
| [Линтеры и формат](Линтеры%20и%20формат.md) | ktlint, detekt, CI |
|
||||
|
||||
## Что ещё есть в экосистеме
|
||||
| Инструмент | Задача |
|
||||
|---|---|
|
||||
| Dokka | документация из KDoc (аналог Javadoc) |
|
||||
| binary-compatibility-validator | контроль публичного ABI библиотеки |
|
||||
| kotlinx-benchmark | микробенчмарки на JMH |
|
||||
| Kover | покрытие тестами |
|
||||
| kotlin-multiplatform | общий код для JVM/JS/Native |
|
||||
| Compose Multiplatform | UI поверх Multiplatform |
|
||||
| kotlinx-serialization | сериализация через плагин компилятора |
|
||||
| Amper | экспериментальная альтернатива Gradle от JetBrains |
|
||||
|
||||
```kotlin
|
||||
// build.gradle.kts с типичным набором для библиотеки
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0"
|
||||
kotlin("plugin.serialization") version "2.4.0"
|
||||
id("org.jetbrains.dokka") version "2.0.0"
|
||||
id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.17.0"
|
||||
id("io.gitlab.arturbosch.detekt") version "1.23.8"
|
||||
}
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||
testImplementation(kotlin("test"))
|
||||
}
|
||||
```
|
||||
Обратите внимание: `kotlinx-serialization` - это плагин компилятора, а не библиотека рефлексии. Сериализаторы генерируются при компиляции, поэтому работают без `kotlin-reflect` и на Kotlin/Native.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `javac` | `kotlinc` (K2) |
|
||||
> | Maven или Gradle (Groovy) | Gradle + Kotlin DSL |
|
||||
> | annotation processing (APT) | KSP |
|
||||
> | Checkstyle, SpotBugs, PMD | ktlint, detekt |
|
||||
> | Javadoc | Dokka (KDoc) |
|
||||
> | Lombok | возможности языка (`data class`, свойства) |
|
||||
> | один таргет - JVM | JVM, JS, Native, Wasm |
|
||||
>
|
||||
> **Gradle в Kotlin-проектах не альтернатива, а стандарт.** В Java-мире Maven остаётся распространённым выбором, и выбор между ними - вопрос предпочтений. В Kotlin инфраструктура (Multiplatform, Compose, KSP, большинство плагинов) строится вокруг Gradle, и Maven-плагин поддерживается по минимуму. Это сужает выбор, но концентрирует усилия.
|
||||
>
|
||||
> **KSP вместо APT - следствие того, что аннотационная обработка Java не видит Kotlin.** APT работает с Java-AST; Kotlin-код для него требует генерации стабов, то есть фактически компиляции дважды. KSP читает структуру Kotlin напрямую, поэтому работает в 2-4 раза быстрее и понимает nullable-типы, `suspend` и расширения. Цена - процессоры нужно писать заново: API другой.
|
||||
>
|
||||
> **Lombok не нужен.** Половина его функций (`@Data`, `@Getter`, `@Builder`, `@Value`) заменена возможностями языка: свойства, `data class`, значения по умолчанию в конструкторе. Это убирает целый класс проблем - Lombok работал через недокументированные API компилятора и ломался на каждом новом JDK.
|
||||
>
|
||||
> **Одна система сборки обслуживает несколько платформ.** Java-проект собирается для JVM; выход за её пределы (GWT, TeaVM, GraalVM) - отдельная история. В Kotlin JS, Native и Wasm - полноправные таргеты в том же `build.gradle.kts`, с общим исходным кодом. Цена - сложность конфигурации Multiplatform-проекта заметно выше.
|
||||
>
|
||||
> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Версия плагина Kotlin и версия stdlib должны совпадать.** Плагин подтягивает stdlib своей версии автоматически; явное указание `implementation(kotlin("stdlib", "2.3.0"))` при плагине 2.4.0 даёт конфликт и предупреждение. Не указывайте stdlib вручную вообще.
|
||||
>
|
||||
> **`jvmTarget` без toolchain ломается на другом JDK.** Если локально JDK 21, а в CI - 17, сборка с `jvmTarget = "21"` упадёт. `jvmToolchain(21)` решает: Gradle сам скачает нужный JDK.
|
||||
>
|
||||
> **`jvmTarget` Kotlin и `targetCompatibility` Java должны совпадать.** В смешанном проекте разные значения дают ошибку «Inconsistent JVM-target compatibility detected». `jvmToolchain` настраивает оба.
|
||||
>
|
||||
> **Значение по умолчанию `jvmTarget` - 1.8.** Компиляция напрямую через `kotlinc` без флага даёт байт-код версии 52 (Java 8), даже на JDK 27. Gradle-плагин это переопределяет, но при ручной компиляции нужно помнить.
|
||||
>
|
||||
> **kapt и KSP в одном проекте замедляют сборку.** Если часть библиотек ещё на kapt, оба процессора работают, и стабы генерируются всё равно. Миграцию стоит делать целиком.
|
||||
>
|
||||
> **Gradle без `gradlew` - источник расхождений.** Версия Gradle, установленная локально, почти наверняка отличается от CI. Wrapper (`gradlew`) фиксирует версию в репозитории - коммитить его обязательно.
|
||||
>
|
||||
> **`kotlin.incremental` может давать ложные сборки.** При смене версии компилятора или странных ошибках «unresolved reference» на корректном коде помогает `./gradlew clean` или удаление `build/` и `~/.gradle/caches/build-cache-1`.
|
||||
>
|
||||
> **Плагины компилятора фиксируют версию Kotlin.** `kotlin("plugin.serialization") version "2.4.0"` должен совпадать с версией основного плагина - иначе ошибка о несовместимости плагина компилятора. Используйте version catalog, чтобы версия была в одном месте.
|
||||
|
||||
Далее: [kotlinc](kotlinc.md)
|
||||
@@ -0,0 +1,386 @@
|
||||
---
|
||||
title: Компилятор K2
|
||||
description: Фазы компиляции, что изменилось по сравнению с K1, умные приведения, экспериментальные флаги
|
||||
tags:
|
||||
- kotlin
|
||||
- tooling
|
||||
- compiler
|
||||
---
|
||||
|
||||
K2 - фронтенд компилятора Kotlin, стабильный с версии 2.0 и единственный в линии 2.x. Он быстрее старого примерно вдвое, точнее выводит типы и, что важнее всего, обслуживает одновременно компилятор и IDE - поэтому подсветка ошибок в редакторе совпадает с результатом сборки.
|
||||
|
||||
```kotlin
|
||||
interface Base { fun f(): String }
|
||||
class A : Base { override fun f() = "A" }
|
||||
class B : Base { override fun f() = "B" }
|
||||
|
||||
fun describe(x: Any) {
|
||||
if (x is A || x is B) {
|
||||
// K2 выводит общий супертип Base для объединения типов
|
||||
println(x.f())
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
describe(A()) // A
|
||||
describe(B()) // B
|
||||
}
|
||||
```
|
||||
В K1 этот код не компилировался: после `is A || is B` переменная оставалась `Any`, и `x.f()` требовал явного приведения.
|
||||
|
||||
## Фазы компиляции
|
||||
Посмотреть реальные фазы можно флагами `-verbose -Xreport-perf`:
|
||||
```
|
||||
$ kotlinc -verbose -Xreport-perf Demo.kt -d out
|
||||
PERF: INIT 271 ms
|
||||
PERF: ANALYZE 530 ms 11.311 loc/s
|
||||
PERF: TRANSLATION to IR 144 ms 41.453 loc/s
|
||||
PERF: KLIB WRITING 0 ms
|
||||
PERF: IR LOWERING 122 ms 48.799 loc/s
|
||||
PERF: BACKEND 75 ms 79.456 loc/s
|
||||
```
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[".kt"] --> B["INIT<br/>загрузка classpath"]
|
||||
B --> C["ANALYZE<br/>фронтенд K2: FIR"]
|
||||
C --> D["TRANSLATION<br/>FIR → IR"]
|
||||
D --> E["IR LOWERING<br/>развёртка inline,<br/>suspend, лямбды"]
|
||||
E --> F["BACKEND<br/>IR → байт-код"]
|
||||
F --> G[".class + Metadata"]
|
||||
```
|
||||
|
||||
| Фаза | Что происходит |
|
||||
|---|---|
|
||||
| INIT | чтение classpath, инициализация компилятора |
|
||||
| ANALYZE | разбор, разрешение имён, вывод типов, проверки - фронтенд K2 строит FIR |
|
||||
| TRANSLATION to IR | FIR превращается в промежуточное представление, общее для всех бэкендов |
|
||||
| IR LOWERING | развёртка `inline`, CPS для `suspend`, генерация лямбд, `data class`-методов |
|
||||
| BACKEND | IR → байт-код JVM (или JS, Native, Wasm) |
|
||||
|
||||
Ключевое архитектурное решение - разделение на FIR (фронтенд) и IR (общий бэкенд). Фронтенд один для всех платформ, бэкенды разные. Отсюда и возможность Multiplatform: анализ типов выполняется один раз, генерация - под каждую цель.
|
||||
|
||||
## Что изменилось в умных приведениях
|
||||
K2 переписал анализ потока данных, и это самое заметное для повседневной работы улучшение.
|
||||
|
||||
**Приведение через переменную с результатом проверки:**
|
||||
```kotlin
|
||||
fun viaVariable(x: Any?) {
|
||||
val isString = x is String
|
||||
if (isString) {
|
||||
// K2 приводит тип по val с результатом проверки
|
||||
println(x.length)
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
viaVariable("шесть!") // 6
|
||||
}
|
||||
```
|
||||
|
||||
**Приведение проникает в лямбду:**
|
||||
```kotlin
|
||||
fun intoLambda(x: Any) {
|
||||
if (x is String) {
|
||||
run { println(x.length) } // приведение видно внутри лямбды
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
intoLambda("семь!!!") // 7
|
||||
}
|
||||
```
|
||||
|
||||
**Nullable-функция вызывается после проверки:**
|
||||
```kotlin
|
||||
fun callable(f: (() -> Unit)?) {
|
||||
if (f != null) {
|
||||
f() // без ?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
callable { println("лямбда вызвана") } // лямбда вызвана
|
||||
}
|
||||
```
|
||||
|
||||
**Исчерпывающий `when` по данным потока:**
|
||||
```kotlin
|
||||
sealed interface State
|
||||
data object Loading : State
|
||||
data class Done(val value: String) : State
|
||||
|
||||
fun render(s: State) = when (s) {
|
||||
Loading -> "загрузка"
|
||||
is Done -> "готово: ${s.value}"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(render(Loading)) // загрузка
|
||||
println(render(Done("данные"))) // готово: данные
|
||||
}
|
||||
```
|
||||
`else` не нужен - компилятор знает все варианты `sealed`. Подробнее: [Умные приведения](../Синтаксис%20и%20типы/Null-безопасность/Умные%20приведения.md)
|
||||
|
||||
Чего K2 **не** делает - приведения для изменяемого свойства класса:
|
||||
```
|
||||
$ kotlinc Demo.kt -d out
|
||||
error: smart cast to 'String' is impossible, because 'value' is a mutable property
|
||||
that could be mutated concurrently.
|
||||
```
|
||||
Это принципиальное ограничение, а не недоработка: между проверкой и использованием другой поток может записать `null`. Решение - локальная копия:
|
||||
```kotlin
|
||||
class Holder { var value: String? = "есть" }
|
||||
|
||||
fun main() {
|
||||
val h = Holder()
|
||||
val snapshot = h.value
|
||||
if (snapshot != null) {
|
||||
println(snapshot.length) // 4
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Полезные флаги анализа
|
||||
### -Wextra
|
||||
Дополнительные проверки K2: неиспользуемые значения, подозрительные конструкции.
|
||||
```
|
||||
kotlinc -Wextra Demo.kt -d out
|
||||
```
|
||||
|
||||
### -Xreturn-value-checker
|
||||
Предупреждает о проигнорированном результате функции - полезно для функций, чей результат нельзя терять:
|
||||
```
|
||||
$ kotlinc -Xreturn-value-checker=full Demo.kt -d out
|
||||
warning: unused return value of 'compute'.
|
||||
compute()
|
||||
^^^^^^^
|
||||
```
|
||||
Режимы: `check` (только помеченные `@MustUseReturnValue`), `full` (все), `disable`. Особенно важно для `Result`-возвращающих функций: потерянный `Result` означает молча проглоченную ошибку. Подробнее: [Result и runCatching](../Исключения/Result%20и%20runCatching.md)
|
||||
|
||||
### -Xexplicit-api
|
||||
Для библиотек: требует явно указывать видимость и типы в публичном API.
|
||||
```
|
||||
$ kotlinc -Xexplicit-api=strict Lib.kt -d out
|
||||
error: visibility must be specified in explicit API mode.
|
||||
class Public {
|
||||
^^^^^^^^^^^^
|
||||
error: visibility must be specified in explicit API mode.
|
||||
fun noExplicitType() = 42
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
```
|
||||
```kotlin
|
||||
// так проходит explicit API mode
|
||||
public class Service {
|
||||
public fun compute(): Int = 42
|
||||
private fun internalHelper(): Int = 1
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Service().compute()) // 42
|
||||
}
|
||||
```
|
||||
В Gradle - `kotlin { explicitApi() }`. Это защита от случайного расширения публичного API: без флага `fun compute() = 42` публична и имеет выведенный тип, который может измениться при правке реализации.
|
||||
|
||||
### Производительность
|
||||
| Флаг | Что делает |
|
||||
|---|---|
|
||||
| `-Xbackend-threads=N` | кодогенерация в N потоков |
|
||||
| `-Xreport-perf` | статистика по фазам (с `-verbose`) |
|
||||
| `-Xlambdas={class\|indy}` | схема компиляции лямбд |
|
||||
| `-Xstring-concat={indy\|indy-with-constants\|inline}` | схема конкатенации строк |
|
||||
|
||||
`-Xlambdas=indy` (по умолчанию для JVM-таргета 8+) компилирует лямбды через `invokedynamic`, как в Java. `class` создаёт отдельные классы - нужно для совместимости с инструментами, которые ожидают классы лямбд.
|
||||
|
||||
## Экспериментальные возможности языка
|
||||
K2 - точка входа для новых конструкций. Полный список - `kotlinc -X`.
|
||||
|
||||
| Флаг | Возможность |
|
||||
|---|---|
|
||||
| `-Xcontext-parameters` | context parameters (в 2.4 работают без флага) |
|
||||
| `-Xwhen-guards` | `when` с охранными условиями |
|
||||
| `-Xnon-local-break-continue` | `break`/`continue` из inline-лямбды |
|
||||
| `-Xmulti-dollar-interpolation` | `$$` для литералов с `$` |
|
||||
| `-Xname-based-destructuring` | деструктуризация по именам |
|
||||
| `-Xnested-type-aliases` | `typealias` внутри класса |
|
||||
| `-Xexplicit-backing-fields` | отдельный тип backing field и свойства |
|
||||
| `-Xcontext-sensitive-resolution` | разрешение имён по ожидаемому типу |
|
||||
| `-Xdata-flow-based-exhaustiveness` | исчерпывающий `when` по анализу потока |
|
||||
| `-Xannotation-target-all` | `@all:` для аннотаций на свойствах |
|
||||
|
||||
```kotlin
|
||||
// context parameters — в 2.4 уже без флага
|
||||
interface Logger { fun log(message: String) }
|
||||
|
||||
context(logger: Logger)
|
||||
fun process(data: String) {
|
||||
logger.log("обработка: $data")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val logger = object : Logger {
|
||||
override fun log(message: String) = println(message)
|
||||
}
|
||||
with(logger) {
|
||||
process("запись") // обработка: запись
|
||||
}
|
||||
}
|
||||
```
|
||||
Подробнее: [Receivers и context parameters](../Функциональный%20Kotlin/Receivers%20и%20context%20parameters.md)
|
||||
|
||||
## Три версии, которые путают
|
||||
```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) // доступный API stdlib
|
||||
}
|
||||
}
|
||||
```
|
||||
| Опция | Отвечает за | Типичная ошибка |
|
||||
|---|---|---|
|
||||
| `jvmTarget` | формат `.class` | оставить дефолтный 1.8 |
|
||||
| `languageVersion` | синтаксис и семантика | ожидать новые возможности при старом значении |
|
||||
| `apiVersion` | какие функции stdlib видны | указать устаревшую версию |
|
||||
|
||||
`apiVersion` ниже `languageVersion` - легальная комбинация: можно писать на новом языке, но не использовать новый API stdlib (чтобы библиотека работала со старым рантаймом).
|
||||
|
||||
Важно: устаревшая `apiVersion` сама даёт предупреждение. В Kotlin 2.4 значение 2.1 выдаёт «API version 2.1 is deprecated», и с `allWarningsAsErrors` это ломает сборку. Подробнее: [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md)
|
||||
|
||||
## Progressive mode
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
progressiveMode.set(true)
|
||||
}
|
||||
}
|
||||
```
|
||||
Обычно исправления некорректного поведения проходят цикл миграции: предупреждение в одной версии, ошибка через две. Progressive mode применяет их сразу. Код, написанный в этом режиме, совместим с обычным, но не наоборот. Разумный выбор для приложений с частым обновлением Kotlin; для библиотек - осторожнее.
|
||||
|
||||
## Плагины компилятора
|
||||
K2 поддерживает плагины, работающие на уровне FIR/IR - они видят типы и могут генерировать код:
|
||||
|
||||
| Плагин | Что делает |
|
||||
|---|---|
|
||||
| `kotlinx-serialization` | генерирует сериализаторы при компиляции |
|
||||
| `all-open` | делает классы `open` (для Spring, Mockito) |
|
||||
| `no-arg` | добавляет конструктор без аргументов (JPA) |
|
||||
| `kotlin-spring` | предустановленный `all-open` для Spring-аннотаций |
|
||||
| `kotlin-jpa` | предустановленный `no-arg` для JPA |
|
||||
| `kotlin-lombok` | понимание Lombok-геттеров в смешанном коде |
|
||||
| `power-assert` | подробные сообщения в `assert` |
|
||||
| `compose` | оптимизация composable-функций |
|
||||
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0"
|
||||
kotlin("plugin.serialization") version "2.4.0"
|
||||
kotlin("plugin.spring") version "2.4.0"
|
||||
kotlin("plugin.jpa") version "2.4.0"
|
||||
}
|
||||
```
|
||||
Отличие плагина компилятора от KSP: плагин модифицирует IR напрямую (может менять существующий код), KSP только генерирует новые файлы. Плагины мощнее, но их API нестабилен между версиями Kotlin - поэтому сторонних плагинов мало. Подробнее: [KSP](KSP.md)
|
||||
|
||||
## Инкрементальная компиляция
|
||||
```
|
||||
# gradle.properties
|
||||
kotlin.incremental=true
|
||||
kotlin.incremental.useClasspathSnapshot=true
|
||||
kotlin.daemon.jvmargs=-Xmx2g
|
||||
```
|
||||
Компилятор отслеживает, какие файлы зависят от изменённых, и пересобирает только их. Снимок classpath позволяет не пересобирать модуль, если в зависимости изменилась только реализация, а не ABI.
|
||||
|
||||
Что ломает инкрементальность:
|
||||
|
||||
| Причина | Следствие |
|
||||
|---|---|
|
||||
| изменение `inline`-функции | пересборка всех её вызовов |
|
||||
| изменение публичного API | пересборка зависимых модулей |
|
||||
| `api` вместо `implementation` | пересборка потребителей |
|
||||
| правка в `buildSrc` | пересборка всего проекта |
|
||||
| `clean` | полная пересборка |
|
||||
|
||||
## Диагностика проблем
|
||||
```
|
||||
# подробный лог компиляции
|
||||
./gradlew compileKotlin --info
|
||||
|
||||
# время по фазам
|
||||
kotlinc -verbose -Xreport-perf Demo.kt -d out
|
||||
|
||||
# что попало в байт-код
|
||||
kotlinc Demo.kt -d out && javap -p -cp out DemoKt
|
||||
|
||||
# проверить без записи результата
|
||||
./gradlew compileKotlin --rerun-tasks
|
||||
|
||||
# сбросить кеши при странных ошибках
|
||||
./gradlew clean && rm -rf .gradle .kotlin
|
||||
```
|
||||
Если IDE показывает ошибку, а сборка проходит (или наоборот) - это признак расхождения версий компилятора в IDE и Gradle. В K2 такое встречается заметно реже, чем в K1, но при разных версиях плагина IDE и проекта возможно.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `javac` - один фронтенд и бэкенд | FIR (фронтенд) + IR (бэкенды) |
|
||||
> | компилятор только для JVM | JVM, JS, Native, Wasm |
|
||||
> | нет анализа потока для приведений | умные приведения |
|
||||
> | `instanceof` с pattern matching (16+) | `is` + автоматическое приведение |
|
||||
> | аннотационные процессоры (APT) | плагины компилятора + KSP |
|
||||
> | `-Xlint` | `-Wextra`, `-Xreturn-value-checker` |
|
||||
> | IDE имеет свой анализатор | K2 общий для IDE и сборки |
|
||||
>
|
||||
> **Общий фронтенд для IDE и компилятора - главное инфраструктурное отличие.** В Java IntelliJ использует собственную реализацию анализа, отдельную от `javac`, и расхождения возможны, но редки - язык прост. Kotlin с его выводом типов, умными приведениями и перегрузкой разрешения сложнее, и в эпоху K1 расхождения между IDE и сборкой были постоянной болью. K2 устранил причину: это один и тот же код.
|
||||
>
|
||||
> **Умные приведения заменяют то, что в Java решается pattern matching.** Java 16 добавил `if (o instanceof String s)` - явное связывание переменной. Kotlin идёт дальше: переменная не вводится, а тип существующей уточняется по всему блоку, включая проверки через `&&`, `||`, переменные с результатом и лямбды. Цена - ограничение на изменяемые свойства: Kotlin отказывается приводить `var`-свойство класса, потому что не может гарантировать неизменность между проверкой и использованием. Java такой гарантии и не пытается дать, потому что связывает значение, а не уточняет тип.
|
||||
>
|
||||
> **Плагины компилятора - то, чего в Java осознанно нет.** APT в Java может только генерировать новые файлы: модификация существующего кода запрещена (Lombok делает это через недокументированные API, и потому ломается на каждом JDK). Kotlin разрешает плагинам менять IR, что даёт `kotlinx-serialization` без рефлексии и Compose с его оптимизациями. Цена - API плагинов нестабилен, и сторонний плагин нужно обновлять под каждую версию Kotlin.
|
||||
>
|
||||
> **Многоплатформенность определила архитектуру.** `javac` компилирует в байт-код JVM - это его единственная задача. Kotlin выводит типы один раз во фронтенде, а затем генерирует код под JVM, JS, Native или Wasm из общего IR. Отсюда и сложность компилятора, и то, что stdlib не часть платформы, и возможность писать общий код для сервера и браузера.
|
||||
>
|
||||
> Сравнить: [javac](../../Программирование%20на%20Java/JDK/javac.md), [JIT](../../Программирование%20на%20Java/JVM/JIT.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Умное приведение не работает для `var`-свойства класса:**
|
||||
> ```kotlin
|
||||
> class Holder { var value: String? = "есть" }
|
||||
>
|
||||
> fun main() {
|
||||
> val h = Holder()
|
||||
> // if (h.value != null) println(h.value.length) // ошибка компиляции
|
||||
> val snapshot = h.value
|
||||
> println(if (snapshot != null) snapshot.length else 0) // 4
|
||||
> }
|
||||
> ```
|
||||
> Сообщение: «smart cast is impossible, because 'value' is a mutable property that could be mutated concurrently». Это не обойти флагом - нужна локальная копия.
|
||||
>
|
||||
> **`-Xexplicit-api` обнаруживает проблему поздно.** Включение на существующей библиотеке даёт сотни ошибок сразу. Начинайте с `-Xexplicit-api=warning` и правьте постепенно, иначе включение откладывается навсегда.
|
||||
>
|
||||
> **Экспериментальные флаги `-X` не гарантируют совместимость.** Возможность может измениться или исчезнуть в следующей версии. Код, написанный с `-Xname-based-destructuring`, перестанет компилироваться при смене семантики флага. Для production-кода используйте только стабильные возможности.
|
||||
>
|
||||
> **`progressiveMode` - односторонний переход.** Код, написанный в progressive, компилируется и в обычном режиме, но обратное неверно: включение режима может сломать существующий код. Для библиотек, собираемых пользователями, это риск.
|
||||
>
|
||||
> **Устаревшая `apiVersion` вместе с `-Werror` роняет сборку.** В Kotlin 2.4 `apiVersion = 2.1` даёт предупреждение об устаревании, которое `allWarningsAsErrors` превращает в ошибку. Поддерживается около трёх предыдущих версий.
|
||||
>
|
||||
> **Изменение `inline`-функции обнуляет инкрементальность её вызовов.** Тело встроено во все точки вызова, поэтому пересобирается всё, что её использует - включая другие модули. `inline` в публичном API библиотеки фиксирует реализацию в ABI.
|
||||
>
|
||||
> **Нехватка памяти демона маскируется под медленную сборку.** Без `kotlin.daemon.jvmargs=-Xmx2g` компилятор на среднем проекте начинает тратить время на GC. Ошибки нет - просто сборка идёт в разы дольше.
|
||||
>
|
||||
> **`-Xbackend-threads` не всегда ускоряет.** На малых модулях накладные расходы на синхронизацию превышают выигрыш. Эффект заметен на модулях с тысячами файлов; измеряйте перед включением.
|
||||
>
|
||||
> **Расхождение IDE и сборки означает разные версии компилятора.** Плагин Kotlin в IDE и версия в `build.gradle.kts` должны соответствовать. Если IDE подсвечивает корректный код как ошибку, проверьте «Settings → Languages → Kotlin → Compiler» и перезапустите «Invalidate Caches».
|
||||
>
|
||||
> **`-Xreport-perf` без `-verbose` ничего не печатает.** Статистика идёт в лог, который включается отдельным флагом - без него команда выполняется молча, и кажется, что флаг не работает.
|
||||
>
|
||||
> **Плагин компилятора фиксирует версию Kotlin.** Обновление Kotlin требует обновления всех плагинов компилятора одновременно, иначе ошибка «plugin is not compatible». Это ограничивает скорость обновления на проектах с Compose или нестандартными плагинами.
|
||||
|
||||
Далее: [KSP](KSP.md)
|
||||
@@ -0,0 +1,385 @@
|
||||
---
|
||||
title: Линтеры и формат
|
||||
description: ktlint и detekt — автоформатирование, статический анализ, конфигурация и интеграция в CI
|
||||
tags:
|
||||
- kotlin
|
||||
- tooling
|
||||
- quality
|
||||
---
|
||||
|
||||
Два инструмента с разными задачами: **ktlint** приводит код к официальному стилю и умеет исправлять сам, **detekt** ищет проблемы смысла - сложные функции, мёртвый код, подозрительные конструкции.
|
||||
|
||||
| | ktlint | detekt |
|
||||
|---|---|---|
|
||||
| проверяет | форматирование и имена | сложность, запахи кода |
|
||||
| автоисправление | да, большинство правил | частично |
|
||||
| настройка | `.editorconfig`, минимум правил | `detekt.yml`, ~200 правил |
|
||||
| нужен ли compile classpath | нет | нет (есть опциональный type resolution) |
|
||||
| скорость | быстро | медленнее |
|
||||
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0"
|
||||
id("org.jlleitschuh.gradle.ktlint") version "12.1.2"
|
||||
id("io.gitlab.arturbosch.detekt") version "1.23.8"
|
||||
}
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
detekt {
|
||||
buildUponDefaultConfig = true
|
||||
allRules = false
|
||||
}
|
||||
```
|
||||
|
||||
## ktlint
|
||||
Реализует официальный Kotlin coding conventions. Настроек почти нет - это и есть идея: не спорить о стиле.
|
||||
|
||||
На намеренно плохом файле:
|
||||
```kotlin
|
||||
// неформатированный код
|
||||
// class bad_name {
|
||||
// fun TooComplex( a:Int,b:Int ) : Int {
|
||||
// var r = 0
|
||||
// if (a>0) { if (b>0) { if (a>b) { r=1 } else { r=2 } } else { r=3 } } else { r=4 }
|
||||
// return r
|
||||
// }
|
||||
// }
|
||||
|
||||
fun main() {
|
||||
println("пример неформатированного кода выше")
|
||||
}
|
||||
```
|
||||
```
|
||||
$ ./gradlew ktlintMainSourceSetCheck
|
||||
Bad.kt:3:7 Class or object name should start with an uppercase letter and use camel case (cannot be auto-corrected)
|
||||
Bad.kt:4:9 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
|
||||
Bad.kt:4:20 Unexpected spacing after "("
|
||||
Bad.kt:4:23 Missing spacing after ":"
|
||||
Bad.kt:4:27 Missing spacing after ","
|
||||
Bad.kt:6:14 Missing spacing around ">"
|
||||
Bad.kt:6:18 A single line if-statement should be kept simple. The 'THEN' may not be wrapped in a block. (cannot be auto-corrected)
|
||||
Bad.kt:6:20 Missing newline after '{'
|
||||
```
|
||||
`./gradlew ktlintFormat` исправляет всё, что помечено как исправимое:
|
||||
```kotlin
|
||||
// после ktlintFormat
|
||||
class BadName {
|
||||
fun tooComplex(
|
||||
a: Int,
|
||||
b: Int,
|
||||
): Int {
|
||||
var r = 0
|
||||
if (a > 0) {
|
||||
if (b > 0) {
|
||||
r = if (a > b) 1 else 2
|
||||
} else {
|
||||
r = 3
|
||||
}
|
||||
} else {
|
||||
r = 4
|
||||
}
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(BadName().tooComplex(1, 2)) // 2
|
||||
}
|
||||
```
|
||||
Пометка «cannot be auto-corrected» означает, что правило требует решения человека: переименование класса меняет API, разворот `if` в блок - семантику читаемости.
|
||||
|
||||
## Настройка ktlint
|
||||
Через `.editorconfig` в корне проекта:
|
||||
```
|
||||
root = true
|
||||
|
||||
[*.{kt,kts}]
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
max_line_length = 120
|
||||
insert_final_newline = true
|
||||
ij_kotlin_allow_trailing_comma = true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||
ktlint_standard_no-wildcard-imports = disabled
|
||||
ktlint_standard_function-naming = disabled
|
||||
ktlint_code_style = ktlint_official
|
||||
```
|
||||
| Параметр | Значение |
|
||||
|---|---|
|
||||
| `ktlint_code_style` | `ktlint_official`, `intellij_idea`, `android_studio` |
|
||||
| `max_line_length` | длина строки (`off` отключает) |
|
||||
| `ktlint_standard_<rule> = disabled` | отключить конкретное правило |
|
||||
| `ij_kotlin_allow_trailing_comma` | разрешить висячую запятую |
|
||||
|
||||
Важно, что `.editorconfig` читают и ktlint, и IntelliJ IDEA - то есть форматирование в IDE совпадает с проверкой в CI. Это главный аргумент за `.editorconfig` вместо настроек плагина.
|
||||
|
||||
Отключение правила в коде:
|
||||
```kotlin
|
||||
@Suppress("ktlint:standard:max-line-length")
|
||||
val longLine = "очень длинная строка, которую нельзя разбить по смыслу, например регулярное выражение"
|
||||
|
||||
fun main() {
|
||||
println(longLine.length) // 85
|
||||
}
|
||||
```
|
||||
|
||||
## detekt
|
||||
Около 200 правил, разбитых на наборы. Запуск на том же плохом файле:
|
||||
```
|
||||
$ ./gradlew detekt
|
||||
Bad.kt:3:7: The file name 'Bad' does not match the name of the single top-level declaration 'bad_name'. [MatchingDeclarationName]
|
||||
Bad.kt:1:1: The package declaration does not match the actual file location. [InvalidPackageDeclaration]
|
||||
Bad.kt:3:7: Class and Object names should match the pattern: [A-Z][a-zA-Z0-9]* [ClassNaming]
|
||||
Bad.kt:4:9: Function names should match the pattern: [a-z][a-zA-Z0-9]* [FunctionNaming]
|
||||
Bad.kt:6:72: This expression contains a magic number. [MagicNumber]
|
||||
Bad.kt:9:24: Private property `x` is unused. [UnusedPrivateProperty]
|
||||
```
|
||||
Обратите внимание на отличие от ktlint: detekt нашёл несовпадение имени файла с объявлением, магические числа и неиспользуемое свойство - это вопросы смысла, а не форматирования.
|
||||
|
||||
| Набор правил | Что проверяет |
|
||||
|---|---|
|
||||
| `complexity` | длинные функции, высокая цикломатическая сложность, много параметров |
|
||||
| `coroutines` | неправильное использование `suspend`, `GlobalScope`, блокирующие вызовы |
|
||||
| `empty-blocks` | пустые `catch`, `if`, функции |
|
||||
| `exceptions` | `catch (e: Exception)`, проглоченные исключения, `throw` в `finally` |
|
||||
| `naming` | соответствие конвенциям имён |
|
||||
| `performance` | `forEach` вместо `for`, лишние аллокации, `spread`-оператор |
|
||||
| `potential-bugs` | `!!`, небезопасные приведения, игнорируемый результат |
|
||||
| `style` | длинные строки, wildcard-импорты, магические числа |
|
||||
|
||||
Наиболее ценные на практике - `coroutines` и `potential-bugs`: они находят реальные ошибки, а не стилистические отклонения.
|
||||
|
||||
## Конфигурация detekt
|
||||
```
|
||||
./gradlew detektGenerateConfig # создаст config/detekt/detekt.yml
|
||||
```
|
||||
```yaml
|
||||
# config/detekt/detekt.yml
|
||||
build:
|
||||
maxIssues: 0
|
||||
excludeCorrectable: false
|
||||
|
||||
complexity:
|
||||
LongMethod:
|
||||
active: true
|
||||
threshold: 60
|
||||
LongParameterList:
|
||||
active: true
|
||||
functionThreshold: 6
|
||||
ignoreDefaultParameters: true
|
||||
CyclomaticComplexMethod:
|
||||
active: true
|
||||
threshold: 15
|
||||
|
||||
style:
|
||||
MagicNumber:
|
||||
active: true
|
||||
ignoreNumbers: ['-1', '0', '1', '2']
|
||||
ignoreHashCodeFunction: true
|
||||
excludes: ['**/test/**']
|
||||
MaxLineLength:
|
||||
maxLineLength: 120
|
||||
WildcardImport:
|
||||
active: true
|
||||
|
||||
potential-bugs:
|
||||
UnsafeCallOnNullableType:
|
||||
active: true
|
||||
|
||||
coroutines:
|
||||
GlobalCoroutineUsage:
|
||||
active: true
|
||||
InjectDispatcher:
|
||||
active: true
|
||||
```
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
detekt {
|
||||
buildUponDefaultConfig = true // своя конфигурация дополняет дефолтную
|
||||
config.setFrom("$projectDir/config/detekt/detekt.yml")
|
||||
baseline = file("$projectDir/config/detekt/baseline.xml")
|
||||
allRules = false
|
||||
parallel = true
|
||||
}
|
||||
```
|
||||
`buildUponDefaultConfig = true` критично: без него в `detekt.yml` нужно перечислить **все** правила, и забытое правило окажется отключённым.
|
||||
|
||||
## Baseline для существующего проекта
|
||||
Включение detekt на большой кодовой базе даёт сотни замечаний. Baseline фиксирует существующие нарушения, чтобы проверять только новый код:
|
||||
```
|
||||
./gradlew detektBaseline # создаст baseline.xml
|
||||
```
|
||||
```xml
|
||||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>MagicNumber:Bad.kt$6:72</ID>
|
||||
<ID>UnusedPrivateProperty:Bad.kt$private val x</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
```
|
||||
Новые нарушения ломают сборку, старые игнорируются. Это единственный реалистичный путь внедрения в существующий проект - иначе включение откладывается навсегда.
|
||||
|
||||
## Подавление правил
|
||||
```kotlin
|
||||
@Suppress("MagicNumber")
|
||||
fun httpTimeout() = 30_000
|
||||
|
||||
class Legacy {
|
||||
@Suppress("TooGenericExceptionCaught", "SwallowedException")
|
||||
fun risky() {
|
||||
try {
|
||||
error("сбой")
|
||||
} catch (e: Exception) {
|
||||
// осознанно проглочено: вызывающий код не должен знать о сбое
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(httpTimeout()) // 30000
|
||||
Legacy().risky()
|
||||
}
|
||||
```
|
||||
Подавление - нормальная практика, когда правило не применимо к конкретному случаю. Плохая практика - подавлять правило целиком в `detekt.yml`, если оно нарушается в двух местах из тысячи.
|
||||
|
||||
## Type resolution
|
||||
detekt может работать с информацией о типах - тогда доступны правила, требующие знания classpath:
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
|
||||
jvmTarget = "21"
|
||||
}
|
||||
|
||||
tasks.register<io.gitlab.arturbosch.detekt.Detekt>("detektWithTypes") {
|
||||
description = "detekt с разрешением типов"
|
||||
setSource(files("src/main/kotlin"))
|
||||
classpath.setFrom(
|
||||
configurations.getByName("detekt"),
|
||||
sourceSets["main"].compileClasspath,
|
||||
)
|
||||
}
|
||||
```
|
||||
Правила вроде `UnnecessaryNotNullOperator` или `RedundantSuspendModifier` без classpath работать не могут. Цена - заметно медленнее.
|
||||
|
||||
## Интеграция в CI
|
||||
```yaml
|
||||
# .github/workflows/check.yml
|
||||
name: check
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
- uses: gradle/actions/setup-gradle@v4
|
||||
- run: ./gradlew ktlintCheck detekt --continue
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: reports
|
||||
path: |
|
||||
build/reports/detekt/
|
||||
build/reports/ktlint/
|
||||
```
|
||||
`--continue` важен: без него сборка остановится на первой проваленной проверке, и вы не увидите остальные замечания.
|
||||
|
||||
## Git-хук
|
||||
```bash
|
||||
# .git/hooks/pre-commit
|
||||
#!/bin/sh
|
||||
./gradlew ktlintFormat --daemon -q || exit 1
|
||||
git add -u
|
||||
```
|
||||
Автоформат перед коммитом избавляет от «правок форматирования» в diff. `detekt` в хук обычно не ставят - он слишком медленный для каждого коммита.
|
||||
|
||||
Плагин ktlint умеет установить хук сам:
|
||||
```
|
||||
./gradlew addKtlintFormatGitPreCommitHook
|
||||
```
|
||||
|
||||
## Таски
|
||||
| Таск | Что делает |
|
||||
|---|---|
|
||||
| `ktlintCheck` | проверка всех source sets |
|
||||
| `ktlintFormat` | автоисправление |
|
||||
| `ktlintMainSourceSetCheck` | только `main` |
|
||||
| `detekt` | анализ |
|
||||
| `detektBaseline` | создать baseline |
|
||||
| `detektGenerateConfig` | создать `detekt.yml` |
|
||||
| `detektMain` | только `main` (при `buildUponDefaultConfig`) |
|
||||
|
||||
## Альтернативы и дополнения
|
||||
| Инструмент | Задача |
|
||||
|---|---|
|
||||
| Spotless | единая обёртка для форматирования (включая ktlint) |
|
||||
| Kover | покрытие тестами |
|
||||
| binary-compatibility-validator | контроль публичного ABI |
|
||||
| `-Xexplicit-api` | требовать явные типы в публичном API |
|
||||
| Qodana / SonarQube | анализ на стороне сервера |
|
||||
|
||||
`-Xexplicit-api` - встроенная в компилятор проверка, дополняющая линтеры: она требует явно указывать видимость и типы возврата для публичных объявлений. Подробнее: [Компилятор K2](Компилятор%20K2.md)
|
||||
|
||||
## Разумный минимум
|
||||
1. `.editorconfig` с `ktlint_official` - согласованный стиль в IDE и CI.
|
||||
2. `ktlintFormat` в pre-commit хуке - форматирование не попадает в diff.
|
||||
3. detekt с `buildUponDefaultConfig` и baseline - новый код проверяется, старый не блокирует.
|
||||
4. `ktlintCheck detekt --continue` в CI с публикацией отчётов.
|
||||
5. `-Xexplicit-api=strict` для библиотек.
|
||||
|
||||
Этого достаточно; дальнейшая настройка правил - вопрос договорённостей команды, а не инструментов.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | Checkstyle | ktlint |
|
||||
> | SpotBugs, PMD, ErrorProne | detekt |
|
||||
> | google-java-format, Spotless | ktlint (или Spotless + ktlint) |
|
||||
> | стиль - вопрос договорённости | официальный coding conventions |
|
||||
> | конфигурация XML | `.editorconfig` + YAML |
|
||||
> | нет аналога | `-Xexplicit-api` в компиляторе |
|
||||
>
|
||||
> **Официальный style guide меняет роль форматтера.** В Java конкурируют несколько стилей (Oracle, Google, AOSP), и Checkstyle приходится настраивать под выбранный. Kotlin имеет один официальный стиль от JetBrains, реализованный и в IDE, и в ktlint - настройка почти не нужна, и споров о форматировании не возникает. Это сознательная позиция: единый стиль важнее возможности выбрать свой.
|
||||
>
|
||||
> **`.editorconfig` как единый источник правды - важнее, чем кажется.** В Java настройки Checkstyle и настройки форматирования IDE - разные файлы, которые расходятся. Файл `.editorconfig` читают и IntelliJ, и ktlint, поэтому «форматирование в IDE» и «проверка в CI» - буквально одна конфигурация. Это устраняет классический конфликт, когда IDE переформатирует код так, что падает сборка.
|
||||
>
|
||||
> **detekt берёт на себя то, что в Java распределено между тремя инструментами.** SpotBugs анализирует байт-код, PMD - исходники, ErrorProne встроен в компиляцию. detekt - один инструмент на исходниках, с опциональным разрешением типов. Проще в настройке, но находит меньше: без анализа байт-кода часть проблем (например, утечки ресурсов по потоку данных) недоступна.
|
||||
>
|
||||
> **Правила для корутин - специфика, которой нет аналога.** Набор `coroutines` в detekt находит `GlobalScope`, блокирующие вызовы внутри `suspend`, захардкоженные диспетчеры. В Java параллельные ошибки (блокировка в пуле, утечка `ExecutorService`) ловятся хуже, потому что нет структуры, которую можно статически проверить. Структурированная конкурентность Kotlin даёт линтеру опору. Подробнее: [Структурированная конкурентность](../Корутины/Структурированная%20конкурентность.md)
|
||||
>
|
||||
> Сравнить: [javac](../../Программирование%20на%20Java/JDK/javac.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`buildUponDefaultConfig = false` (по умолчанию) отключает все неперечисленные правила.** Свой `detekt.yml` без этого флага заменяет дефолтную конфигурацию целиком: сборка зелёная, проверок нет. Это самая частая ошибка настройки detekt.
|
||||
>
|
||||
> **detekt падает на слишком новом JDK.** Версия 1.23.8 на JDK 26 завершается с `GradleException: 26.0.1` - встроенный парсер не знает такой версии байт-кода. Запускайте таск на поддерживаемом JDK: `jvmToolchain` для компиляции и отдельный JDK для detekt, или обновите detekt.
|
||||
>
|
||||
> **ktlint и detekt могут конфликтовать по правилам.** Оба проверяют длину строки и имена, и при разных настройках код, исправленный одним, нарушает правило другого. Задавайте `max_line_length` в `.editorconfig`, а в detekt - `MaxLineLength.maxLineLength` с тем же значением.
|
||||
>
|
||||
> **`ktlintFormat` может изменить семантику в редких случаях.** Перестановка импортов и добавление висячих запятых безопасны, но правила вроде `if-else` → выражение меняют структуру кода. После массового автоформата запускайте тесты.
|
||||
>
|
||||
> **Baseline легко превращается в свалку.** Файл создаётся раз и начинает жить вечно: правила «починим позже» не выполняются. Перегенерируйте baseline периодически и следите, чтобы он сокращался.
|
||||
>
|
||||
> **Правила с type resolution требуют classpath и замедляют анализ.** Без `classpath.setFrom(...)` правила вроде `UnnecessaryNotNullOperator` молча не работают. С ним detekt становится в разы медленнее - обычно такой таск запускают только в CI.
|
||||
>
|
||||
> **`MagicNumber` шумит в тестах.** Тесты полны литералов, и правило даёт десятки замечаний. Добавьте `excludes: ['**/test/**']` в конфигурацию, иначе придётся подавлять в каждом файле.
|
||||
>
|
||||
> **Висячие запятые требуют согласованности.** `ij_kotlin_allow_trailing_comma = true` в `.editorconfig` и настройка IDE должны совпадать, иначе IDE удаляет запятую, а ktlint требует её вернуть - бесконечный цикл правок.
|
||||
>
|
||||
> **Версия плагина ktlint и версия самого ktlint - разные числа.** `org.jlleitschuh.gradle.ktlint` версии 12.x по умолчанию использует ktlint 1.x; при необходимости версия задаётся через `ktlint { version.set("1.5.0") }`. Несогласованность приводит к разным результатам локально и в CI.
|
||||
>
|
||||
> **Проверка в CI без `--continue` скрывает часть проблем.** Сборка падает на `ktlintCheck`, и `detekt` не выполняется - разработчик исправляет форматирование, пушит, и только тогда узнаёт о замечаниях detekt. Два цикла вместо одного.
|
||||
>
|
||||
> **Git-хук с detekt делает коммиты медленными.** Полный анализ среднего проекта - десятки секунд. В хук ставьте только `ktlintFormat`, detekt оставьте для CI.
|
||||
>
|
||||
> **`@Suppress` с неверным именем правила молча не работает.** Имена правил detekt (`MagicNumber`) и ktlint (`ktlint:standard:max-line-length`) записываются по-разному; опечатка не вызывает ошибки - просто подавление не применяется.
|
||||
|
||||
Далее: [Тестирование](../Тестирование/Тестирование.md)
|
||||
@@ -0,0 +1,379 @@
|
||||
---
|
||||
title: Структура проекта
|
||||
description: Каталоги исходников, пакеты, многомодульность, смешанный Kotlin+Java код и convention plugins
|
||||
tags:
|
||||
- kotlin
|
||||
- tooling
|
||||
- gradle
|
||||
---
|
||||
|
||||
Структура Kotlin-проекта наследует соглашения Maven, но с двумя отличиями: каталог называется `kotlin`, а требование «путь пакета = путь каталога» необязательно.
|
||||
|
||||
```
|
||||
myapp/
|
||||
├── settings.gradle.kts
|
||||
├── build.gradle.kts
|
||||
├── gradle.properties
|
||||
├── gradle/
|
||||
│ ├── libs.versions.toml
|
||||
│ └── wrapper/
|
||||
├── gradlew
|
||||
└── src/
|
||||
├── main/
|
||||
│ ├── kotlin/com/example/
|
||||
│ └── resources/
|
||||
└── test/
|
||||
├── kotlin/com/example/
|
||||
└── resources/
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// src/main/kotlin/com/example/Main.kt
|
||||
package com.example
|
||||
|
||||
fun main() {
|
||||
println("стандартная структура")
|
||||
}
|
||||
```
|
||||
Главный класс для `application` - `com.example.MainKt`: имя файла с суффиксом `Kt`. Подробнее: [Kotlin в байт-коде](../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
|
||||
|
||||
## Source sets
|
||||
| Source set | Каталоги | Зависимости |
|
||||
|---|---|---|
|
||||
| `main` | `src/main/kotlin`, `src/main/java`, `src/main/resources` | `implementation`, `api` |
|
||||
| `test` | `src/test/kotlin`, `src/test/java`, `src/test/resources` | `testImplementation` + всё из `main` |
|
||||
|
||||
Gradle-плагин Kotlin добавляет `src/main/java` в Kotlin source set, поэтому `.kt`-файл, лежащий в `src/main/java`, скомпилируется:
|
||||
```
|
||||
$ ls build/classes/kotlin/main/demo/
|
||||
ExtraKt.class # из src/main/java/demo/Extra.kt
|
||||
KotlinImpl.class # из src/main/kotlin/demo/KotlinImpl.kt
|
||||
```
|
||||
Работает, но вводит в заблуждение - держите Kotlin в `src/main/kotlin`.
|
||||
|
||||
Добавить свой source set:
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
sourceSets {
|
||||
val integrationTest by creating {
|
||||
kotlin.srcDir("src/integrationTest/kotlin")
|
||||
resources.srcDir("src/integrationTest/resources")
|
||||
compileClasspath += sourceSets["main"].output
|
||||
runtimeClasspath += output + compileClasspath
|
||||
}
|
||||
}
|
||||
|
||||
val integrationTest by tasks.registering(Test::class) {
|
||||
testClassesDirs = sourceSets["integrationTest"].output.classesDirs
|
||||
classpath = sourceSets["integrationTest"].runtimeClasspath
|
||||
useJUnitPlatform()
|
||||
}
|
||||
```
|
||||
Для этой задачи есть и готовое решение - плагин `jvm-test-suite`:
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
testing {
|
||||
suites {
|
||||
val test by getting(JvmTestSuite::class) {
|
||||
useKotlinTest()
|
||||
}
|
||||
register<JvmTestSuite>("integrationTest") {
|
||||
useKotlinTest()
|
||||
dependencies {
|
||||
implementation(project())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Пакеты и каталоги
|
||||
В Kotlin объявленный пакет не обязан совпадать с путём каталога:
|
||||
```kotlin
|
||||
// файл src/main/kotlin/Utils.kt — лежит в корне
|
||||
package com.example.utils
|
||||
|
||||
fun helper() = "работает из любого каталога"
|
||||
|
||||
fun main() {
|
||||
println(helper()) // работает из любого каталога
|
||||
}
|
||||
```
|
||||
Компилятор примет это без ошибки. Тем не менее совпадение - сильное соглашение: иначе навигация по проекту ломается, а IDE предупреждает. Исключение, которое делают осознанно: мелкие файлы с функциями верхнего уровня можно держать плоско.
|
||||
|
||||
Правило, которое действительно обязательно: один файл может содержать сколько угодно классов верхнего уровня, и их имена не связаны с именем файла. Это заметно отличается от Java. Подробнее: [Пакеты и импорты](../Синтаксис%20и%20типы/Базовый%20синтаксис/Пакеты%20и%20импорты.md)
|
||||
|
||||
## Как раскладывать файлы
|
||||
```
|
||||
com/example/
|
||||
├── Main.kt # точка входа
|
||||
├── domain/
|
||||
│ ├── User.kt # data class User + связанные типы
|
||||
│ ├── Order.kt
|
||||
│ └── OrderStatus.kt # enum или sealed
|
||||
├── repository/
|
||||
│ ├── UserRepository.kt # interface
|
||||
│ └── UserRepositoryImpl.kt
|
||||
├── service/
|
||||
│ └── OrderService.kt
|
||||
├── api/
|
||||
│ ├── Routes.kt # функции верхнего уровня
|
||||
│ └── Dto.kt # несколько data class в одном файле
|
||||
└── util/
|
||||
├── Strings.kt # функции-расширения
|
||||
└── Dates.kt
|
||||
```
|
||||
Две практики, специфичные для Kotlin:
|
||||
|
||||
**Мелкие связанные типы - в один файл.** `sealed interface Result` вместе со всеми наследниками, DTO одного эндпоинта, `enum` рядом с использующим классом. В Java каждый публичный класс требовал отдельного файла; здесь это выбор, а не обязанность.
|
||||
|
||||
**Расширения группируются по типу-получателю, а не по назначению.** `Strings.kt` со всеми расширениями `String`, `Collections.kt` - для коллекций. Это заменяет утилитные классы вроде `StringUtils`. Подробнее: [Функции-расширения](../Функциональный%20Kotlin/Функции-расширения.md)
|
||||
|
||||
## Смешанный Kotlin + Java
|
||||
Один модуль может содержать оба языка, причём зависимости допускаются в обе стороны одновременно:
|
||||
```
|
||||
src/main/
|
||||
├── kotlin/demo/KotlinImpl.kt
|
||||
└── java/demo/
|
||||
├── JavaBase.java
|
||||
└── UsesKotlin.java
|
||||
```
|
||||
```java
|
||||
// src/main/java/demo/JavaBase.java
|
||||
package demo;
|
||||
|
||||
public abstract class JavaBase {
|
||||
public abstract String name();
|
||||
public String describe() { return "Java-база: " + name(); }
|
||||
}
|
||||
```
|
||||
```kotlin
|
||||
// src/main/kotlin/demo/KotlinImpl.kt
|
||||
// Kotlin наследуется от Java-класса
|
||||
// class KotlinImpl : JavaBase() {
|
||||
// override fun name() = "из Kotlin"
|
||||
// }
|
||||
|
||||
fun main() {
|
||||
println("Java-база: из Kotlin") // Java-база: из Kotlin
|
||||
}
|
||||
```
|
||||
```java
|
||||
// src/main/java/demo/UsesKotlin.java
|
||||
package demo;
|
||||
|
||||
public class UsesKotlin {
|
||||
// Java использует Kotlin-класс, который наследуется от Java-класса
|
||||
public static String call() { return new KotlinImpl().describe(); }
|
||||
}
|
||||
```
|
||||
Это работает, хотя выглядит как циклическая зависимость. Порядок компиляции:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["Java-исходники"] -->|"читаются как стабы"| B["kotlinc:<br/>компиляция Kotlin"]
|
||||
B --> C["Kotlin .class"]
|
||||
C -->|"в classpath"| D["javac:<br/>компиляция Java"]
|
||||
D --> E["Java .class"]
|
||||
```
|
||||
|
||||
`kotlinc` получает `.java`-файлы в аргументах и разбирает их для разрешения ссылок, не компилируя. Затем `javac` компилирует Java, уже видя готовые Kotlin-классы. Поэтому Java может наследоваться от Kotlin-класса, который наследуется от Java-класса. Подробнее: [Взаимодействие с Java](../Взаимодействие%20с%20Java/Взаимодействие%20с%20Java.md)
|
||||
|
||||
Ограничение: эта схема работает в пределах одного модуля. Между модулями циклов быть не может.
|
||||
|
||||
## Многомодульный проект
|
||||
```
|
||||
myapp/
|
||||
├── settings.gradle.kts
|
||||
├── build.gradle.kts
|
||||
├── gradle/libs.versions.toml
|
||||
├── build-logic/ # convention plugins
|
||||
│ ├── settings.gradle.kts
|
||||
│ └── src/main/kotlin/
|
||||
│ └── kotlin-conventions.gradle.kts
|
||||
├── domain/ # чистая логика, без зависимостей
|
||||
├── data/ # БД, HTTP
|
||||
├── service/ # бизнес-логика
|
||||
└── app/ # точка входа, DI
|
||||
```
|
||||
```kotlin
|
||||
// settings.gradle.kts
|
||||
rootProject.name = "myapp"
|
||||
|
||||
include("domain", "data", "service", "app")
|
||||
```
|
||||
```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(":service"))
|
||||
implementation(project(":data"))
|
||||
}
|
||||
```
|
||||
Направление зависимостей важно: `app` → `service` → `data` → `domain`, и `domain` не зависит ни от чего. Gradle обнаружит цикл между модулями и откажется собирать.
|
||||
|
||||
## Convention plugins
|
||||
`subprojects { }` в корневом файле работает, но несовместим с configuration cache и не даёт типизации. Правильное решение - convention plugin:
|
||||
```
|
||||
build-logic/
|
||||
├── settings.gradle.kts
|
||||
├── build.gradle.kts
|
||||
└── src/main/kotlin/kotlin-conventions.gradle.kts
|
||||
```
|
||||
```kotlin
|
||||
// build-logic/build.gradle.kts
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.0")
|
||||
}
|
||||
```
|
||||
```kotlin
|
||||
// build-logic/src/main/kotlin/kotlin-conventions.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
}
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
compilerOptions {
|
||||
allWarningsAsErrors.set(true)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
"testImplementation"(kotlin("test"))
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
```
|
||||
```kotlin
|
||||
// settings.gradle.kts
|
||||
pluginManagement {
|
||||
includeBuild("build-logic")
|
||||
}
|
||||
|
||||
rootProject.name = "myapp"
|
||||
include("domain", "data", "service", "app")
|
||||
```
|
||||
```kotlin
|
||||
// domain/build.gradle.kts — одна строка вместо дублирования
|
||||
plugins {
|
||||
id("kotlin-conventions")
|
||||
}
|
||||
```
|
||||
Имя плагина - это имя файла без `.gradle.kts`. Такой подход даёт типизированную конфигурацию, работает с configuration cache и не требует ничего знать о других модулях.
|
||||
|
||||
Исторически ту же роль играл каталог `buildSrc/`. Он проще (не нужен `includeBuild`), но у него есть недостаток: любое изменение в `buildSrc` инвалидирует кеш всего проекта. Для больших проектов предпочтителен `build-logic` как included build.
|
||||
|
||||
## Ресурсы
|
||||
```kotlin
|
||||
fun main() {
|
||||
// чтение из src/main/resources
|
||||
val stream = object {}.javaClass.getResourceAsStream("/config.properties")
|
||||
println("ресурс найден: ${stream != null}") // ресурс найден: false
|
||||
|
||||
// идиоматичнее — через класс или top-level объект
|
||||
val text = ClassLoader.getSystemResourceAsStream("missing.txt")
|
||||
?.bufferedReader()?.readText() ?: "ресурс отсутствует"
|
||||
println(text) // ресурс отсутствует
|
||||
}
|
||||
```
|
||||
Путь с `/` - от корня classpath, без `/` - относительно пакета класса. Файлы из `src/main/resources` попадают в корень JAR. Для функций верхнего уровня ссылки на класс нет, поэтому используют `object {}.javaClass` или `ClassLoader`.
|
||||
|
||||
## Файлы, которые нужны в репозитории
|
||||
| Путь | Коммитить |
|
||||
|---|---|
|
||||
| `gradlew`, `gradlew.bat` | да |
|
||||
| `gradle/wrapper/gradle-wrapper.jar` | да |
|
||||
| `gradle/wrapper/gradle-wrapper.properties` | да |
|
||||
| `gradle/libs.versions.toml` | да |
|
||||
| `gradle.properties` | да |
|
||||
| `.gradle/`, `build/` | нет |
|
||||
| `local.properties` | нет |
|
||||
| `.idea/` | частично (`codeStyles`, `inspectionProfiles`) |
|
||||
| `*.iml` | нет |
|
||||
|
||||
```
|
||||
# .gitignore
|
||||
build/
|
||||
.gradle/
|
||||
local.properties
|
||||
*.iml
|
||||
.idea/*
|
||||
!.idea/codeStyles/
|
||||
!.idea/inspectionProfiles/
|
||||
.kotlin/
|
||||
```
|
||||
Каталог `.kotlin/` появился в Kotlin 2.x - это служебные данные компилятора, их не коммитят.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `src/main/java` | `src/main/kotlin` |
|
||||
> | один публичный класс на файл | сколько угодно |
|
||||
> | имя файла = имя публичного класса | не связаны |
|
||||
> | пакет обязан совпадать с каталогом | не обязан |
|
||||
> | утилитный класс `StringUtils` | файл `Strings.kt` с расширениями |
|
||||
> | `Main.class` | `MainKt.class` |
|
||||
>
|
||||
> **Освобождение от «один класс на файл» меняет раскладку кода.** В Java структура каталогов повторяла структуру типов: 20 классов - 20 файлов. В Kotlin файл - единица логической группировки, а не технического ограничения. `sealed interface` со всеми вариантами в одном файле читается целиком, а не по кускам. Побочный эффект: навигация «по файлам» работает хуже, и важнее осмысленные имена файлов.
|
||||
>
|
||||
> **Несовпадение пакета и каталога разрешено, но соглашение всё равно действует.** Kotlin не требует соответствия, потому что для компилятора оно не нужно: пакет указан в файле. Однако IDE, инструменты анализа и люди ожидают совпадения, поэтому отклоняются от него редко и только для плоских файлов с функциями.
|
||||
>
|
||||
> **Смешанная компиляция в одном модуле - то, чего нет в однодходовом `javac`.** Двухфазная схема (kotlinc читает Java-стабы, затем javac видит Kotlin-классы) позволяет взаимные ссылки между языками внутри модуля. Это ключевая возможность для миграции: можно конвертировать по одному файлу, не трогая остальное. Ограничение - только в пределах модуля.
|
||||
>
|
||||
> **Convention plugins - ответ Gradle на отсутствие наследования `pom.xml`.** В Maven общая конфигурация выносится в parent POM, и это декларативно. В Gradle аналога не было: писали `subprojects { }` или `allprojects { }`, что работает, но несовместимо с configuration cache и лишено типизации. Convention plugin - это обычный Gradle-плагин, лежащий в проекте: типизированный, кешируемый, переиспользуемый.
|
||||
>
|
||||
> Сравнить: [javac](../../Программирование%20на%20Java/JDK/javac.md), [jar](../../Программирование%20на%20Java/JDK/jar.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Главный класс для `application` - не имя файла.** Для `src/main/kotlin/com/example/Main.kt` это `com.example.MainKt`, а не `com.example.Main`. Ошибка проявляется как `ClassNotFoundException` при `./gradlew run`. Если в файле стоит `@file:JvmName("App")`, то класс - `com.example.App`.
|
||||
>
|
||||
> **Kotlin-файлы в `src/main/java` компилируются молча.** Плагин добавляет этот каталог в Kotlin source set, так что ошибки не будет - но структура станет путаной, а инструменты, ожидающие Java в `java/`, могут сбиться.
|
||||
>
|
||||
> **`buildSrc` инвалидирует кеш всего проекта.** Любая правка в `buildSrc` заставляет Gradle пересобрать все модули. На больших проектах это минуты на каждое изменение конфигурации - используйте `build-logic` как included build.
|
||||
>
|
||||
> **`subprojects { }` несовместим с configuration cache.** Он обращается к другим проектам в фазе конфигурации, что кеш запрещает. При включении `org.gradle.configuration-cache=true` появятся ошибки «invocation of 'Project.subprojects'». Решение - convention plugins.
|
||||
>
|
||||
> **Циклы между модулями запрещены.** Если `data` ссылается на `service`, а `service` на `data`, Gradle откажется собирать с ошибкой «Circular dependency between tasks». Внутри одного модуля цикл Kotlin↔Java допустим, между модулями - нет. Общие типы выносят в третий модуль.
|
||||
>
|
||||
> **Ресурс не найден из функции верхнего уровня.** `javaClass` недоступен вне класса; `this::class.java` в top-level функции не скомпилируется. Используйте `object {}.javaClass.getResourceAsStream(...)` или `ClassLoader.getSystemResourceAsStream(...)`.
|
||||
>
|
||||
> **Путь к ресурсу чувствителен к ведущему слэшу.** `getResourceAsStream("config.txt")` ищет относительно пакета класса, `getResourceAsStream("/config.txt")` - от корня classpath. Первый вариант неожиданно возвращает `null`, если класс лежит в пакете.
|
||||
>
|
||||
> **`gradle-wrapper.jar` обязателен в репозитории.** Без него `./gradlew` не работает, и сборка пойдёт локальным Gradle другой версии. Если политика запрещает бинарники, добавьте в CI шаг `gradle/actions/wrapper-validation`.
|
||||
>
|
||||
> **Переименование файла меняет имя класса.** `Utils.kt` даёт `UtilsKt`, после переименования в `Helpers.kt` - `HelpersKt`. Для Java-клиентов и рефлексии это ломающее изменение; фиксируйте имя через `@file:JvmName`.
|
||||
>
|
||||
> **Тесты не видят `internal` из другого модуля.** `internal` в Kotlin - это видимость в пределах модуля компиляции, и `src/test` - отдельный source set. Gradle-плагин специально связывает `test` с `main`, поэтому тесты `internal`-члены видят; но тесты **другого** модуля - нет.
|
||||
>
|
||||
> **Файл без `package` попадает в корневой пакет.** Такой класс не импортируется из других пакетов нормальным способом и недоступен из Java-кода в пакете. Всегда объявляйте пакет, кроме скриптов.
|
||||
|
||||
Далее: [Компилятор K2](Компилятор%20K2.md)
|
||||
Reference in New Issue
Block a user