32 KiB
title, description, tags
| title | description | tags | |||
|---|---|---|---|---|---|
| Аннотации | Объявление аннотаций, цели, retention, use-site targets, чтение через рефлексию, аннотации интеропа |
|
Аннотация - метаданные, прикреплённые к объявлению. Сама по себе она ничего не делает: смысл ей придаёт тот, кто её читает - рефлексия в рантайме, процессор аннотаций в компиляции или сам компилятор.
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)+ контейнерпросто @RepeatableFoo.classв параметреFoo::class{1, 2}для массива[1, 2]getAnnotation(Foo.class)findAnnotation<Foo>()аннотации на файл невозможны @file:JvmName(...)Use-site targets существуют потому, что свойство - не поле. В Java
@Column private String nameоднозначна: цель одна. В Kotlinval 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 не имеет проверяемых исключений, поэтому без@ThrowsJava-код не сможет написать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