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

32 KiB
Raw Blame History

title, description, tags
title description tags
Аннотации Объявление аннотаций, цели, retention, use-site targets, чтение через рефлексию, аннотации интеропа
kotlin
annotations
reflection

Аннотация - метаданные, прикреплённые к объявлению. Сама по себе она ничего не делает: смысл ей придаёт тот, кто её читает - рефлексия в рантайме, процессор аннотаций в компиляции или сам компилятор.

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

Объявление

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

@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

@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-элементов: поле, геттер, иногда сеттер, а для параметра конструктора ещё и параметр. Аннотация должна попасть на конкретный из них - для этого есть префиксы.

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. Это проверяется легко:

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

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 работает без дополнительных флагов компилятора.

Параметры аннотаций

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

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 даёт читаемое представление с подставленными дефолтами - удобно для отладки.

Повторяемые аннотации

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>(), возвращающий список.

Чтение через рефлексию

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 - интерфейс единый. Подробнее: Рефлексия

Практический пример: валидация

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

Аннотации компилятора

Часть аннотаций обрабатывает сам компилятор - они меняют генерируемый код или проверки.

Аннотация Что делает
@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 вывод типа в билдерах
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 убирает объявление из автодополнения, сохраняя бинарную совместимость.

Аннотации интеропа меняют байт-код заметно:

@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

Аннотации для KSP

Аннотация, предназначенная процессору, не нужна в рантайме:

@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

[!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: решают проблему. Подробнее: Свойства

Retention по умолчанию - RUNTIME, а не CLASS. Java по умолчанию не включает аннотацию в рантайм, поэтому каждая библиотека обязана ставить @Retention(RUNTIME) явно. Kotlin выбрал обратный дефолт - аннотации чаще читают рефлексией, чем процессором. Обратная сторона: аннотация для линтера без явного SOURCE попадёт и в байт-код, и в рантайм, увеличивая артефакт.

Повторяемость не требует класса-контейнера. В Java @Repeatable обязывает объявить отдельную аннотацию-контейнер с массивом - лишний код, который приходится писать для каждой повторяемой аннотации. Kotlin генерирует контейнер сам; findAnnotations<T>() возвращает список, и раскрывать контейнер вручную не нужно.

Аннотации на файл - новая цель, которой в Java нет. @file:JvmName("Utils") меняет имя класса-контейнера для функций верхнего уровня, @file:Suppress подавляет предупреждения во всём файле. В Java единица объявления - всегда класс, поэтому и цели FILE быть не может. Подробнее: Пакеты и импорты

Генерация кода вместо рефлексии - предпочтительный путь. В Java-экосистеме аннотации почти всегда читаются рефлексией: Spring, Jackson, Hibernate строят модель при старте приложения. Kotlin подталкивает к другому: kotlinx.serialization и KSP обрабатывают аннотации при компиляции и генерируют код, поэтому kotlin-reflect не нужен, старт быстрее, а ошибки видны при сборке. Для Kotlin/Native и Kotlin/JS это единственный работающий вариант. Подробнее: KSP

Сравнить: Reflection API в Java

[!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

@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 сообщит, что исключение не бросается. Подробнее: Система исключений

Аннотации на типах требуют явной цели TYPE. List<@Marker String> не скомпилируется, если в @Target нет AnnotationTarget.TYPE - по умолчанию эта цель не включена.

@Target(AnnotationTarget.EXPRESSION) работает только с SOURCE. Выражения не существуют в байт-коде, поэтому RUNTIME с такой целью даст ошибку компиляции.

@all: - новая возможность и ломает сборку на старых версиях Kotlin. В 2.4 работает без флагов, в более ранних версиях требует -Xannotation-target-all или недоступна вовсе. Для библиотек с широкой совместимостью пока надёжнее перечислять цели вручную.

Чтение аннотаций рефлексией стоит дорого при первом обращении. Это тот же разбор метаданных, что и у остальной рефлексии: ~150 мс на класс впервые. Кешируйте результат, а лучше переносите обработку в KSP. Подробнее: Рефлексия

Обфускация ломает аннотации с именами в строках. @Column("user_id") переживёт R8, а вот поиск свойства по имени из аннотации - нет: само свойство переименуется. Нужны правила -keep.

Далее: Kotlin