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. Это даст вам совершенно иной уровень понимания языка и того как работает ваш код.