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


ссылка на оригинал статьи https://habr.com/ru/articles/774872/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *