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

Комментарии (4)


  1. damiruali
    22.11.2023 21:10
    +1

    я swift начал изучать))


  1. odisseylm
    22.11.2023 21:10

    В любой книге по котлину это конечно же не написано... Наверное, или написано? (с).


    1. MaxSidorov Автор
      22.11.2023 21:10

      Не уверен, возможно где то и написано. Но я не встречал. Про это мало кто знает, так как редко сталкиваются. Но все кто работали с Jackson должны были сталкиваться.


  1. karp4004
    22.11.2023 21:10

    этот вопрос - для начинающего мидла (условно)