Kotlin под капотом: нюансы использования аннотаций
Котлин очень лаконичный язык, но когда его код компилируется в Java bytecode, то изящные конструкции kotlin распадаются на развесистые и монструозные конструкции Java. При этом применение аннотаций может сыграть с вами злую шутку.
Давайте рассмотрим это на примере аннотации @JsonProperty и обычного data класса.
Я столкнулся с этой проблемой в реальном кейсе 6 лет назад и с разбора этой ошибки началось мое увлекательное изучение того, как kotlin работает под капотом и как он генерирует свой bytecode.
По сути тот случай дал толчок моему изучению kotlin и в конечном счете сделал из меня kotlin эксперта.
Предположим у нас есть вот такой data класс
data class SomeData(@JsonProperty("id") val someDataId: String)
Из кода логично предположить, что аннотация @JsonProperty будет применена к полю someDataId, ведь мы указываем ее именно для поля. То есть наш Json файл должен содержать поле "id" вместо "someDataId" и это должно работать при чтении и записи файла.
Но так ли это? Давайте разбираться.
Если мы декомпилируем kotlin код, то мы увидим, что в Java этот класс выглядит вот так
public static final class SomeData { @NotNull // private field private String someDataId; @NotNull // field getter public final String getSomeDataId() { return this.someDataId; } // constructor with field param public SomeData(@NotNull String someDataId) { Intrinsics.checkNotNullParameter(someDataId, "someDataId"); super(); this.someDataId = someDataId; }
Как видите, наше поле someDataId преобразовалось в приватное поле, геттер для поля и параметр конструктора класса. Так к чему же из этого будет применена аннотация?
Если мы посмотрим определение аннотации @JsonProperty, то увидим, что она может быть применена к приватному полю, методу и параметру метода. То есть ко всем конструкциям Java, на которые распадается наше поле data класса.
@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotation public @interface JsonProperty
В результате для компилятора kotlin возникает неочевидность. Он может применить нашу аннотацию к полю, геттеру, а также к параметру конструктора. Как вы думаете, к чему он применит аннотацию в данном конкретном случае?
Правильный ответ - к параметру конструктора класса.
Реальный bytecode будет выглядеть вот так и в нем явно видно, что наша аннотация применяется только к параметру конструктора класса.
public static final class SomeData { @NotNull private String someDataId; @NotNull public final String getSomeDataId() { return this.someDataId; } public SomeData(@JsonProperty("id") @NotNull String someDataId) { Intrinsics.checkNotNullParameter(someDataId, "someDataId"); super(); this.someDataId = someDataId; }
В результате наш класс будет правильно считываться из Json, но генерация Json по нашему классу будет работать некорректно. При записи в Json название поля будет "someDataId" вместо ожидаемого "id".
Давайте разбираться, почему kotlin работает именно так.
Правила применения аннотаций в kotlin
Для разрешения подобных коллизий в kotlin существует специальное правило, которое гласит
Если аннотацию можно применить сразу к нескольким конструкциям языка, то она будет по умолчанию применена к первому подходящему use-site из списка:Параметр методаСвойство (недоступно в Java)Поле
А для того, чтобы явно разрешать подобные коллизии на уровне кода, в kotlin существует специальный синтаксис, который позволяет явно указать область применения аннотации.
@[use‑site]:Annotation
Примеры указания области применения аннотации
class Example( @field:JsonProperty("Foo") val foo, // annotate Java field @get:JsonProperty("Bar") val bar, // annotate Java getter @param:JsonProperty("Some") val some // annotate Java constructor parameter )
Вот полный список типов use-site взятый из документации kotlin
- file
- property (annotations with this target are not visible to Java)
- field
- get (property getter)
- set (property setter)
- receiver (receiver parameter of an extension function or property)
- param (constructor parameter)
- setparam (property setter parameter)
- delegate (the field storing the delegate instance for a delegated property)
Выводы
Чтобы наш класс работал правильно, нам нужно указать аннотацию вот так
data class SomeData(@field:JsonProperty("id") var someDataId: String)
А еще лучше вот так, чтобы убрать использование рефлексии для доступа к приватному полю
data class SomeData( @param:JsonProperty("id") @get:JsonProperty("id") var someDataId: String )
Вы можете никогда не столкнуться с такой проблемой, потому что обычно области применения аннотаций подобраны таким образом, чтобы избегать подобных коллизий. Именно это объясняет тот факт, что 99% разработчиков kotlin не знают об этом синтаксисе и возможности явно указывать область применения аннотаций.
Но это полезно знать, чтобы не смотреть на решение вашей проблемы со stackoverflow как на магию, а понимать как это работает и за счет чего это работает.
Изучайте bytecode, который генерирует kotlin. Это даст вам совершенно иной уровень понимания языка и того как работает ваш код.