Files
obsidian/Kotlin/Рефлексия/Аннотации.md
T

654 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Аннотации
description: Объявление аннотаций, цели, retention, use-site targets, чтение через рефлексию, аннотации интеропа
tags:
- kotlin
- annotations
- reflection
---
Аннотация - метаданные, прикреплённые к объявлению. Сама по себе она ничего не делает: смысл ей придаёт тот, кто её читает - рефлексия в рантайме, процессор аннотаций в компиляции или сам компилятор.
```kotlin
import kotlin.reflect.full.findAnnotation
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Table(val name: String, val schema: String = "public")
@Table("users")
class User(val id: Int, val name: String)
fun main() {
val table = User::class.findAnnotation<Table>()
println("таблица=${table?.name}, схема=${table?.schema}")
}
```
```
таблица=users, схема=public
```
## Объявление
```kotlin
annotation class Simple
annotation class WithParams(
val name: String,
val priority: Int = 0,
val tags: Array<String> = [],
)
fun main() {
println("объявлены")
}
```
```
объявлены
```
Правила для аннотаций жёстче, чем для обычных классов:
- нет тела, нет функций и состояния - только параметры конструктора;
- все параметры - `val`;
- типы параметров ограничены: примитивы, `String`, `KClass`, `enum`, другая аннотация, массивы перечисленного;
- нельзя `null` в качестве значения по умолчанию - сам тип не может быть nullable.
## Цели - @Target
```kotlin
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY,
AnnotationTarget.VALUE_PARAMETER,
)
annotation class Marker
@Marker
class Annotated(@Marker val prop: Int) {
@Marker
fun method() = Unit
}
fun main() {
println("применено к четырём местам")
}
```
```
применено к четырём местам
```
| Цель | Куда |
|---|---|
| `CLASS` | класс, интерфейс, `object`, аннотация |
| `FUNCTION` | функция, не конструктор |
| `PROPERTY` | свойство (только Kotlin-код) |
| `FIELD` | backing field свойства |
| `VALUE_PARAMETER` | параметр функции или конструктора |
| `CONSTRUCTOR` | конструктор |
| `PROPERTY_GETTER`, `PROPERTY_SETTER` | аксессоры |
| `LOCAL_VARIABLE` | локальная переменная |
| `TYPE` | использование типа: `List<@Marker String>` |
| `TYPE_PARAMETER` | параметр типа |
| `EXPRESSION` | выражение (требует `SOURCE`) |
| `FILE` | файл, через `@file:` |
| `ANNOTATION_CLASS` | мета-аннотация |
Без `@Target` аннотация применима почти везде, кроме `TYPE`, `EXPRESSION`, `FILE` и `TYPE_PARAMETER` - их нужно указывать явно.
## Хранение - @Retention
```kotlin
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.CLASS)
annotation class Src
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS)
annotation class Bin
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Run
@Target(AnnotationTarget.CLASS)
annotation class NoRetention
@Src @Bin @Run
class All
@NoRetention
class Default
fun main() {
println("All=${All::class.annotations.map { it.annotationClass.simpleName }}")
println("Default=${Default::class.annotations.map { it.annotationClass.simpleName }}")
}
```
```
All=[Run]
Default=[NoRetention]
```
| Retention | В байт-коде | Видна рефлексии |
|---|---|---|
| `SOURCE` | нет | нет |
| `BINARY` | да | нет |
| `RUNTIME` (по умолчанию) | да | да |
Ключевое отличие от Java: **по умолчанию `RUNTIME`**, а не `CLASS`. Поэтому `@NoRetention` без явного указания видна через рефлексию - в Java аналогичная аннотация была бы невидима. Для аннотаций, которые читает только KSP или линтер, стоит явно ставить `SOURCE`: это убирает их из байт-кода.
## Use-site targets
Свойство в Kotlin компилируется в несколько Java-элементов: поле, геттер, иногда сеттер, а для параметра конструктора ещё и параметр. Аннотация должна попасть на конкретный из них - для этого есть префиксы.
```kotlin
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.javaConstructor
import kotlin.reflect.jvm.javaField
import kotlin.reflect.jvm.javaGetter
import kotlin.reflect.jvm.javaSetter
@Target(
AnnotationTarget.PROPERTY,
AnnotationTarget.FIELD,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER,
AnnotationTarget.VALUE_PARAMETER,
)
@Retention(AnnotationRetention.RUNTIME)
annotation class Mark(val where: String)
class Targets(
@Mark("без префикса") val implicit: Int,
@field:Mark("field") val onField: Int,
@get:Mark("getter") val onGetter: Int,
@property:Mark("property") val onProperty: Int,
) {
@set:Mark("setter")
var mutable: Int = 0
}
fun main() {
for (p in Targets::class.memberProperties.sortedBy { it.name }) {
val prop = p.annotations.filterIsInstance<Mark>().map { it.where }
val field = p.javaField?.annotations?.filterIsInstance<Mark>()?.map { it.where }.orEmpty()
val getter = p.javaGetter?.annotations?.filterIsInstance<Mark>()?.map { it.where }.orEmpty()
println("${p.name}: property=$prop field=$field getter=$getter")
}
val ctor = Targets::class.primaryConstructor!!.javaConstructor!!
ctor.parameterAnnotations.forEachIndexed { i, anns ->
println("параметр $i: ${anns.filterIsInstance<Mark>().map { it.where }}")
}
val mutable = Targets::class.memberProperties.first { it.name == "mutable" }
val setter = (mutable as KMutableProperty1<*, *>).javaSetter
println("setter=${setter?.annotations?.filterIsInstance<Mark>()?.map { it.where }}")
}
```
```
implicit: property=[без префикса] field=[] getter=[]
mutable: property=[] field=[] getter=[]
onField: property=[] field=[field] getter=[]
onGetter: property=[] field=[] getter=[getter]
onProperty: property=[property] field=[] getter=[]
параметр 0: [без префикса]
параметр 1: []
параметр 2: []
параметр 3: []
setter=[setter]
```
| Префикс | Куда попадёт |
|---|---|
| `@property:` | свойство (видно только Kotlin-рефлексии) |
| `@field:` | backing field |
| `@get:` | геттер |
| `@set:` | сеттер |
| `@param:` | параметр конструктора |
| `@setparam:` | параметр сеттера |
| `@receiver:` | получатель функции-расширения |
| `@delegate:` | поле делегата |
| `@file:` | файл, до объявления `package` |
| `@all:` | все применимые цели сразу |
### Правило выбора по умолчанию
Без префикса аннотация применяется к **первой подходящей** цели из списка, в порядке: `param` → `property` → `field`. Это проверяется легко:
```kotlin
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.javaConstructor
import kotlin.reflect.jvm.javaField
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class Multi
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class PropOrField
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class FieldOnly
class C(
@Multi val a: Int,
@PropOrField val b: Int,
@FieldOnly val c: Int,
)
fun report(name: String) {
val p = C::class.memberProperties.first { it.name == name }
val onProperty = p.annotations.map { it.annotationClass.simpleName }
val onField = p.javaField?.annotations?.map { it.annotationClass.simpleName }.orEmpty()
val index = C::class.primaryConstructor!!.parameters.indexOfFirst { it.name == name }
val onParam = C::class.primaryConstructor!!.javaConstructor!!
.parameterAnnotations[index].map { it.annotationClass.simpleName }
println("$name: property=$onProperty field=$onField param=$onParam")
}
fun main() {
report("a")
report("b")
report("c")
}
```
```
a: property=[Multi] field=[] param=[Multi]
b: property=[PropOrField] field=[] param=[]
c: property=[] field=[FieldOnly] param=[]
```
Обратите внимание на `a`: аннотация, применимая и к параметру, и к свойству, попала **на оба** - на параметр конструктора и на Kotlin-свойство (но не на поле). Именно здесь возникают проблемы с Java-библиотеками: Jackson или Hibernate читают поля через Java Reflection, а там аннотации нет - нужен явный `@field:`.
### @all
```kotlin
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.javaConstructor
import kotlin.reflect.jvm.javaField
import kotlin.reflect.jvm.javaGetter
@Target(
AnnotationTarget.PROPERTY,
AnnotationTarget.FIELD,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.VALUE_PARAMETER,
)
@Retention(AnnotationRetention.RUNTIME)
annotation class Mark
class WithAll(@all:Mark val x: Int)
fun main() {
val p = WithAll::class.memberProperties.first()
println("property=${p.annotations.size}")
println("field=${p.javaField?.annotations?.size}")
println("getter=${p.javaGetter?.annotations?.size}")
val params = WithAll::class.primaryConstructor!!.javaConstructor!!.parameterAnnotations
println("param=${params[0].size}")
}
```
```
property=1
field=1
getter=1
param=1
```
`@all:` - решение давней проблемы: раньше приходилось писать `@field:Mark @get:Mark @param:Mark` подряд. В Kotlin 2.4 работает без дополнительных флагов компилятора.
## Параметры аннотаций
```kotlin
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation
interface Validator
class NotBlank : Validator
class MaxLength : Validator
enum class Mode { FAST, SAFE }
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Validators(val value: Array<KClass<out Validator>>)
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Levels(val value: IntArray, val names: Array<String>)
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Nested(val inner: Levels)
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class WithEnum(val mode: Mode)
@Validators([NotBlank::class, MaxLength::class])
@Levels(value = [1, 2, 3], names = ["low", "mid", "high"])
@Nested(Levels([9], ["x"]))
@WithEnum(Mode.SAFE)
class Configured
fun main() {
val kc = Configured::class
println("классы=${kc.findAnnotation<Validators>()!!.value.map { it.simpleName }}")
val levels = kc.findAnnotation<Levels>()!!
println("IntArray=${levels.value.toList()} names=${levels.names.toList()}")
println("вложенная=${kc.findAnnotation<Nested>()!!.inner.value.toList()}")
println("enum=${kc.findAnnotation<WithEnum>()!!.mode}")
}
```
```
классы=[NotBlank, MaxLength]
IntArray=[1, 2, 3] names=[low, mid, high]
вложенная=[9]
enum=SAFE
```
Массивы задаются литералом `[a, b]` - старый синтаксис `arrayOf(a, b)` тоже работает, но литерал короче. Классы передаются как `KClass`, то есть `Foo::class`, а не `Foo::class.java`.
## Значения по умолчанию и equals
```kotlin
import kotlin.reflect.full.findAnnotation
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class HasDefaults(val a: Int = 1, val b: String = "x", val c: IntArray = [1, 2])
@HasDefaults
class WithDefaults
@HasDefaults(a = 5)
class Partial
fun main() {
val d = WithDefaults::class.findAnnotation<HasDefaults>()!!
println("дефолты a=${d.a} b=${d.b} c=${d.c.toList()}")
val p = Partial::class.findAnnotation<HasDefaults>()!!
println("частично a=${p.a} b=${p.b}")
println("toString=$p")
println("равенство=${d == WithDefaults::class.findAnnotation<HasDefaults>()}")
}
```
```
дефолты a=1 b=x c=[1, 2]
частично a=5 b=x
toString=@HasDefaults(a=5, b="x", c={1, 2})
равенство=true
```
Экземпляры аннотаций сравниваются по значениям параметров, а `toString` даёт читаемое представление с подставленными дефолтами - удобно для отладки.
## Повторяемые аннотации
```kotlin
import kotlin.reflect.full.findAnnotations
@Repeatable
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Tag(val value: String)
@Tag("a")
@Tag("b")
class Tagged
fun main() {
println("все теги=${Tagged::class.findAnnotations<Tag>().map { it.value }}")
println("через annotations=${Tagged::class.annotations.filterIsInstance<Tag>().map { it.value }}")
}
```
```
все теги=[a, b]
через annotations=[a, b]
```
В Java для повторяемых аннотаций нужен явный контейнер (`@Repeatable(Tags.class)` плюс класс `Tags` с массивом). В Kotlin достаточно `@Repeatable`, а для чтения есть `findAnnotations<T>()`, возвращающий список.
## Чтение через рефлексию
```kotlin
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Table(val name: String)
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Column(val name: String, val nullable: Boolean = false)
@Table("users")
class User(
@Column("user_id") val id: Int,
@Column("full_name", nullable = true) val name: String?,
)
fun main() {
val kc = User::class
println("hasAnnotation=${kc.hasAnnotation<Table>()}")
println("findAnnotation=${kc.findAnnotation<Table>()?.name}")
for (p in kc.memberProperties.sortedBy { it.name }) {
val column = p.findAnnotation<Column>()
println("${p.name}: column=${column?.name} nullable=${column?.nullable}")
}
}
```
```
hasAnnotation=true
findAnnotation=users
id: column=user_id nullable=false
name: column=full_name nullable=true
```
| Функция | Что возвращает |
|---|---|
| `annotations` | список всех `RUNTIME`-аннотаций элемента |
| `findAnnotation<T>()` | первую аннотацию типа `T` или `null` |
| `findAnnotations<T>()` | все аннотации типа `T` (для `@Repeatable`) |
| `hasAnnotation<T>()` | есть ли аннотация |
| `annotationClass` | `KClass` самой аннотации |
Эти функции есть у `KClass`, `KProperty`, `KFunction` и `KParameter` - интерфейс единый. Подробнее: [Рефлексия](Рефлексия.md)
## Практический пример: валидация
```kotlin
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class NotBlank(val message: String = "не должно быть пустым")
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Range(val min: Int, val max: Int)
data class Form(
@NotBlank val name: String,
@NotBlank("email обязателен") val email: String,
@Range(18, 120) val age: Int,
)
fun validate(obj: Any): List<String> {
val errors = mutableListOf<String>()
for (p in obj::class.memberProperties) {
p.isAccessible = true
val value = p.call(obj)
p.findAnnotation<NotBlank>()?.let { rule ->
if ((value as? String).isNullOrBlank()) errors += "${p.name}: ${rule.message}"
}
p.findAnnotation<Range>()?.let { rule ->
val number = value as? Int ?: return@let
if (number !in rule.min..rule.max) {
errors += "${p.name}: должно быть от ${rule.min} до ${rule.max}"
}
}
}
return errors.sorted()
}
fun main() {
println(validate(Form("Иван", "ivan@example.com", 30)))
println(validate(Form("", "", 5)))
}
```
```
[]
[age: должно быть от 18 до 120, email: email обязателен, name: не должно быть пустым]
```
Это рабочая схема, но она платит рефлексией в рантайме: разбор метаданных при первом обращении и потеря проверок на этапе компиляции. Тот же результат даёт KSP-процессор, генерирующий код валидации при сборке. Подробнее: [KSP](../Инструментарий/KSP.md)
## Аннотации компилятора
Часть аннотаций обрабатывает сам компилятор - они меняют генерируемый код или проверки.
| Аннотация | Что делает |
|---|---|
| `@Deprecated` | предупреждение или ошибка при использовании |
| `@Suppress("NAME")` | подавляет предупреждение в области применения |
| `@JvmName` | имя в байт-коде |
| `@JvmStatic` | статический метод для членов `companion object` |
| `@JvmOverloads` | перегрузки для параметров со значениями по умолчанию |
| `@JvmField` | публичное поле вместо геттера |
| `@JvmInline` | обязательна для `value class` |
| `@Throws` | добавляет `throws` в сигнатуру для Java |
| `@OptIn`, `@RequiresOptIn` | механизм согласия на нестабильный API |
| `@PublishedApi` | доступ к `internal` из `public inline` |
| `@Volatile`, `@Synchronized` | то же, что в Java |
| `@Transient` | исключение из сериализации |
| `@DslMarker` | ограничение области в DSL |
| `@BuilderInference` | вывод типа в билдерах |
```kotlin
import kotlin.reflect.full.findAnnotation
@Deprecated("используйте new()", ReplaceWith("new()"), DeprecationLevel.WARNING)
fun old() = 1
fun new() = 2
fun main() {
val d = ::old.findAnnotation<Deprecated>()
println("message=${d?.message}")
println("level=${d?.level}")
println("replaceWith=${d?.replaceWith?.expression}")
}
```
```
message=используйте new()
level=WARNING
replaceWith=new()
```
`ReplaceWith` понимает IDE: она предлагает автозамену вызова. `DeprecationLevel.ERROR` делает использование ошибкой компиляции, `HIDDEN` убирает объявление из автодополнения, сохраняя бинарную совместимость.
Аннотации интеропа меняют байт-код заметно:
```kotlin
@file:JvmName("InteropUtils")
import java.io.IOException
import java.lang.reflect.Modifier
class Interop {
@Throws(IOException::class)
fun mayThrow() = Unit
@JvmField
val field = 1
@get:JvmName("fetchValue")
val value = 2
@JvmOverloads
fun withDefaults(a: Int, b: Int = 1, c: Int = 2) = a + b + c
companion object {
@JvmStatic
fun staticFn() = 3
}
}
fun main() {
val c = Interop::class.java
println("методы=${c.declaredMethods.map { it.name }.sorted()}")
println("поля=${c.declaredFields.map { it.name }.sorted()}")
println("throws=${c.getDeclaredMethod("mayThrow").exceptionTypes.map { it.simpleName }}")
println("статические=${c.declaredMethods.filter { Modifier.isStatic(it.modifiers) }.map { it.name }.sorted()}")
}
```
```
методы=[fetchValue, mayThrow, staticFn, withDefaults, withDefaults, withDefaults, withDefaults$default]
поля=[Companion, field, value]
throws=[IOException]
статические=[staticFn, withDefaults$default]
```
`@JvmOverloads` дал три перегрузки `withDefaults`, `@get:JvmName` переименовал геттер в `fetchValue`, `@JvmField` превратил свойство в поле без геттера. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
## Аннотации для KSP
Аннотация, предназначенная процессору, не нужна в рантайме:
```kotlin
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class GenerateBuilder
@GenerateBuilder
data class Person(val name: String, val age: Int)
fun main() {
println("в рантайме=${Person::class.annotations.size}")
}
```
```
в рантайме=0
```
`SOURCE` убирает аннотацию из байт-кода полностью - KSP читает исходники, а не классы. Это уменьшает размер артефакта и исключает случайное чтение рефлексией. Подробнее: [KSP](../Инструментарий/KSP.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `@interface Foo { String value(); }` | `annotation class Foo(val value: String)` |
> | `String value() default "x"` | `val value: String = "x"` |
> | retention по умолчанию `CLASS` | по умолчанию `RUNTIME` |
> | `@Target(ElementType.FIELD)` | `@Target(AnnotationTarget.FIELD)` |
> | нет понятия свойства | `@property:`, `@field:`, `@get:`, `@set:` |
> | `@Repeatable(Container.class)` + контейнер | просто `@Repeatable` |
> | `Foo.class` в параметре | `Foo::class` |
> | `{1, 2}` для массива | `[1, 2]` |
> | `getAnnotation(Foo.class)` | `findAnnotation<Foo>()` |
> | аннотации на файл невозможны | `@file:JvmName(...)` |
>
> **Use-site targets существуют потому, что свойство - не поле.** В Java `@Column private String name` однозначна: цель одна. В Kotlin `val name: String` порождает поле, геттер и Kotlin-свойство как отдельную сущность в метаданных, а в конструкторе ещё и параметр - аннотация должна выбрать одну цель. По умолчанию выбирается первая применимая в порядке `param` → `property` → `field`, и это главная причина, по которой Java-библиотеки «не видят» аннотацию: Jackson читает поле, а аннотация оказалась на параметре. Префикс `@field:` или новый `@all:` решают проблему. Подробнее: [Свойства](../ООП/Свойства.md)
>
> **Retention по умолчанию - `RUNTIME`, а не `CLASS`.** Java по умолчанию не включает аннотацию в рантайм, поэтому каждая библиотека обязана ставить `@Retention(RUNTIME)` явно. Kotlin выбрал обратный дефолт - аннотации чаще читают рефлексией, чем процессором. Обратная сторона: аннотация для линтера без явного `SOURCE` попадёт и в байт-код, и в рантайм, увеличивая артефакт.
>
> **Повторяемость не требует класса-контейнера.** В Java `@Repeatable` обязывает объявить отдельную аннотацию-контейнер с массивом - лишний код, который приходится писать для каждой повторяемой аннотации. Kotlin генерирует контейнер сам; `findAnnotations<T>()` возвращает список, и раскрывать контейнер вручную не нужно.
>
> **Аннотации на файл - новая цель, которой в Java нет.** `@file:JvmName("Utils")` меняет имя класса-контейнера для функций верхнего уровня, `@file:Suppress` подавляет предупреждения во всём файле. В Java единица объявления - всегда класс, поэтому и цели `FILE` быть не может. Подробнее: [Пакеты и импорты](../Синтаксис%20и%20типы/Базовый%20синтаксис/Пакеты%20и%20импорты.md)
>
> **Генерация кода вместо рефлексии - предпочтительный путь.** В Java-экосистеме аннотации почти всегда читаются рефлексией: Spring, Jackson, Hibernate строят модель при старте приложения. Kotlin подталкивает к другому: `kotlinx.serialization` и KSP обрабатывают аннотации при компиляции и генерируют код, поэтому `kotlin-reflect` не нужен, старт быстрее, а ошибки видны при сборке. Для Kotlin/Native и Kotlin/JS это единственный работающий вариант. Подробнее: [KSP](../Инструментарий/KSP.md)
>
> Сравнить: [Reflection API в Java](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md)
> [!warning] Подводные камни
> **Аннотация без префикса может не попасть туда, куда нужно Java-библиотеке.** Проверено: аннотация с целями `VALUE_PARAMETER` и `PROPERTY` на `val` в конструкторе попадает на параметр и Kotlin-свойство, но **не на поле** - Jackson, Hibernate и JPA её не увидят. Нужен `@field:` или `@all:`.
>
> **`@property:` видна только Kotlin-рефлексии.** В Java-представлении такой аннотации нет вообще: она хранится в метаданных Kotlin. Библиотека на Java Reflection её не найдёт никогда.
>
> **Retention по умолчанию `RUNTIME` раздувает байт-код.** Аннотации для KSP, линтеров и внутренних проверок стоит помечать `@Retention(AnnotationRetention.SOURCE)` - иначе они остаются в классах и видны рефлексии без необходимости.
>
> **`BINARY` не видна рефлексии, хотя лежит в байт-коде.** Проверено: из `@Src @Bin @Run` рефлексия вернёт только `Run`. Это частая причина «аннотация есть в коде, а `findAnnotation` даёт `null`».
>
> **`null` в качестве значения аннотации невозможен.** Типы параметров не могут быть nullable, поэтому «отсутствие значения» кодируют пустой строкой или сигнальной константой. Это ограничение формата аннотаций в JVM, а не Kotlin.
>
> **Параметры аннотаций ограничены по типам.** `data class`, коллекции, `Duration` и функции использовать нельзя - только примитивы, `String`, `KClass`, `enum`, другая аннотация и массивы из них. Попытка передать `List<String>` вместо `Array<String>` не скомпилируется.
>
> **`KClass` в параметре, а не `Class`.** `@Validators([NotBlank::class])` - правильно, `NotBlank::class.java` в аннотации не примут. При чтении обратно получается `KClass`, и для Java-API нужен `.java`.
>
> **`@JvmStatic` работает только в `companion object` и `object`.** На обычном методе класса даёт ошибку компиляции - статических методов у Kotlin-классов не бывает. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md)
>
> **`@JvmOverloads` генерирует перегрузки только справа налево.** Для `f(a, b = 1, c = 2)` появятся `f(a)`, `f(a, b)`, `f(a, b, c)` - вызвать `f(a, c)` из Java не получится. Проверено на реальном байт-коде.
>
> **`@Throws` обязательна для Java-вызывающих.** Kotlin не имеет проверяемых исключений, поэтому без `@Throws` Java-код не сможет написать `catch (IOException e)` - компилятор Java сообщит, что исключение не бросается. Подробнее: [Система исключений](../Исключения/Система%20исключений.md)
>
> **Аннотации на типах требуют явной цели `TYPE`.** `List<@Marker String>` не скомпилируется, если в `@Target` нет `AnnotationTarget.TYPE` - по умолчанию эта цель не включена.
>
> **`@Target(AnnotationTarget.EXPRESSION)` работает только с `SOURCE`.** Выражения не существуют в байт-коде, поэтому `RUNTIME` с такой целью даст ошибку компиляции.
>
> **`@all:` - новая возможность и ломает сборку на старых версиях Kotlin.** В 2.4 работает без флагов, в более ранних версиях требует `-Xannotation-target-all` или недоступна вовсе. Для библиотек с широкой совместимостью пока надёжнее перечислять цели вручную.
>
> **Чтение аннотаций рефлексией стоит дорого при первом обращении.** Это тот же разбор метаданных, что и у остальной рефлексии: ~150 мс на класс впервые. Кешируйте результат, а лучше переносите обработку в KSP. Подробнее: [Рефлексия](Рефлексия.md)
>
> **Обфускация ломает аннотации с именами в строках.** `@Column("user_id")` переживёт R8, а вот поиск свойства по имени из аннотации - нет: само свойство переименуется. Нужны правила `-keep`.
Далее: [Kotlin](../Kotlin.md)