Практически любой программист на java в своей жизни писал RestController, но мало кто задумывается правильно ли он это делает. Даже если вы опытный программист, у вас могут возникнуть вопросы на которые я постараюсь ответить. В статье будут затронуты такие фреймворки как spring boot версии 1.5 и 2.0, а также quarkus — недавно появившийся соперник spring boot от red hat.

image


Проблема


Долгое время программировал на java и spring boot 1.5. Но возникла потребность написать новый проект:

  1. Имею json для интеграции 1600 строк, некоторые классы имеют 100 полей
  2. Захотелось опробовать kotlin и quarkus.
  3. Написать rest controller, который бы умел работать с kotlin data class без аннотаций и без привлечения магии lombok. Хочется, чтобы data class был небольшого размера

Вы наверное догадались, что kotlin data class — это неизменяемые класс или immutable. Класс у которого конструктор содержит все поля. Я большой приверженец такой концепции; после создания класса, его нельзя изменить, в нем нет сеттеров. Как в мире докер image не может изменен, так и дата класс, который попал в контроллер это то, чего нельзя менять.

Давай те рассмотрим возможные пути решения проблемы, точнее как в современных проектах можно написать контроллер:

Стандартный вариант. вручную на spring или spring boot 1.5


Rest контроллеры появились достаточно давно и типичным примером их использования на java, который есть во всех туториалах является следующий пример.

Просто создаем простейший контроллер

@RestController
public class FruitController {
   @PostMapping("/fruit")
   public void greeting(@RequestBody Fruit request) {
      System.out.println(request);
   }
}

И создаем POJO(plain old java object)

public class Fruit {
    public String name;
    public String description;
    public Fruit() {
    }
    public Fruit(String name, String description) {
        this.name = name;
        this.description = description;
    }
}

тут могут быть вариации с private полями и getters/setters или lombok аннотациями, но не суть.
Замечательно, мы создали первый рест контроллер он работает. Для 90% случаев рабочий вариант. Можно здесь и остановиться.

Проблемы:

Нарушена концепция immutable.
Немного многословный класс данных.


Можем ли мы сделать immutable?


Естественной реакцией на эту проблему это удаление конструктора по умолчанию и запрет редактировать поля класса.

@Getter
public class Fruit {
    private String name;
    private String description;
//    public Fruit() {
//    }
    public Fruit(String name, String description) {
        this.name = name;
        this.description = description;
    }
}

Но теперь возникает проблема, оказывается что библиотека (скорее всего jackson) не может создать класс. Наиболее вероятно вы увидим ошибку вроде No default constructor found.

Значит jackson сначала создавал класс с помощью конструктора без параметров, а потом вызывал getter/setter. Какой ужас. Ведь есть же конструктор со всеми параметрами? Но к сожалению когда класс откомпилирован, параметры выглядят примерно так.

image

Т.е. во время выполнения java ничего не знает о именах параметров в конструкторе. Компилятор их теряет.

Второй вариант spring или spring boot 1.5 + аннотации как спасенье


Итак мы осознали, что хотим immutable class, и знаем что пользуемся jackson. Тогда на помощь приходят анотатации.

@Getter
public class Fruit {
    private String name;
    private String description;

    @JsonCreator
    public Fruit(@JsonProperty("name") String name, @JsonProperty("description")String description) {
        this.name = name;
        this.description = description;
    }
}

В данный момент в нашем проекте на sping boot 1.5 этими аннотациями буквально пестрит все.
А если вы возьмете популярный генератор jsonschema2pojo, то он сгенерирует еще больше аннотаций. Честно говоря мне они не нравится.

Попробуйте скопировать туда:

{
	"description": "description",
	"name": "name"  
}

На выходе получаем(можно зажмуриться и пролистать):

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({
        "description",
        "name"
})
public class Example {

    @JsonProperty("description")
    private String description;
    @JsonProperty("name")
    private String name;

    @JsonProperty("description")
    public String getDescription() {
        return description;
    }

    @JsonProperty("description")
    public void setDescription(String description) {
        this.description = description;
    }

    @JsonProperty("name")
    public String getName() {
        return name;
    }

    @JsonProperty("name")
    public void setName(String name) {
        this.name = name;
    }
}

Минусы: С аннотациями сильно раздувается класс. Очень многословно, на любителя.

Третий вариант spring или spring boot 1.5 + флаг lombok


Спасибо Throwable я процитирую из комментария:

«Если Вы используете Lombok, то есть лучший способ — прописать в lombok.config:

lombok.allArgsConstructor.addConstructorProperties = true

Это сгенерит на конструкторе @java.beans.ConstructorProperties, который Jackson умеет понимать.»

Замечательный вариант. Он наверняка бы спас меня от многословности аннотаций.

Минусы:

Но я хочу использовать kotlin без lombok.
Я узнал этот вариант слишком поздно.


Четвертый вариант Spring boot 2.0


@Getter
public class Fruit {
    private String name;
    private String description;
    public Fruit( String name, String description) {
        this.name = name;
        this.description = description;
    }
}

На удивление spring boot 2.0 спокойно работает с таким immutable классом. А также с его братом близнецом kotlin data class

data class Fruit(
 val name : String,
 val description : String)

Казалось бы java в рантайме не знает имен параметров в конструкторе, но почему то spring boot2 уже умеет работать с data class. Итак заглянем в spring-boot-starter-parent, там добавилась поддержка kotlin.

<plugin>
             <groupId>org.jetbrains.kotlin</groupId>
...
              <configuration>
                        <javaParameters>true</javaParameters>  
              </configuration>
</plugin>
<plugin>
              <artifactId>maven-compiler-plugin</artifactId>
              <configuration>
                        <parameters>true</parameters>
              </configuration>
</plugin>

Расшифровываю. Для того чтобы имена параметров в конструкторе класса не терялись при рантайме компилятору необходимо передать флаг javac -parameters И spring boot 2.0 это и делает.

Пример проекта на spring boot 2.

Пятый вариант. Quarkus + kotlin


У кваркуса есть пример rest service-а, аналог моего первого варианта. Т.е. rest контроллер по старинке. Однако, если вы хотите использовать его с kotlin вам придется добавить флаги как это сделал spring boot 2.

Описание проблемы тут. Пример, как добавить в кваркус поддержку kotlin data class тут.

Выводы


Можно пользоваться первым простейшим вариантом создания rest контроллеров, но я бы посоветовал бы двигаться в сторону immutable классов. Пишите на kotlin и вам не понадобится lombok. Код должен стать легче и проще. Я уверен, что создатели spring осознано шли на добавление javac -parameters в опции компилятора и в этом не должно быть криминала. Всем удачи на пути к идеальному коду.

Всем спасибо за внимание!

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


  1. lampa
    14.12.2019 00:28

    Т.е. вся проблема в том, что приходящие данные мы можем изменить через сеттер? И очищение боди сеттера не вариант?


    1. studiedlist
      15.12.2019 21:58

      Это плохая практика. Только вы будете знать об «особенности» класса, и, вероятно, другому разработчику придется поломать голову чтобы понять, что его коллега решил вот так изящно избавиться от проблемы


  1. Throwable
    14.12.2019 18:03
    +2

    Значит jackson сначала создавал класс с помощью конструктора без параметров, а потом вызывал getter/setter.

    Ну да, так все делают. Практически всем библиотекам сериализации необходим пустой конструктор (обычно приватный) — только Java Serialization позволено инициировать "пустые" объекты без конструктора, хотя есть способы обхода этого. Для установки данных Jackson может использовть как геттеры-сеттеры, так и прямой доступ к полям любой видимости.


    компилятору необходимо передать флаг javac -parameters

    Плохой способ. Ни одна из существующих IDE не подцепит это, поэтому придется устанавливать дополнительно в настройках проекта. И вообще код качественно не должет зависеть от флагов компиляции. Если Вы используете Lombok, то есть лучший способ — прописать в lombok.config:


    lombok.allArgsConstructor.addConstructorProperties = true

    Это сгенерит на конструкторе @java.beans.ConstructorProperties, который Jackson умеет понимать.


    1. serg-bs Автор
      16.12.2019 15:55

      Огромное спасибо за комментарий.


  1. ris58h
    14.12.2019 21:55

    Как-то сумбурно, на мой взгляд. Могли бы вы структурировать эту статью: проблема, описание, возможные решения, вывод?
    Проблема, как я понял, в том, что вам не нравится многословность аннотаций у boot 1.5, но я так и не понял из текста решена ли она в boot 2, или она решается подключением Kotlin к boot 1.5, или и тем и тем одновременно. И что, в итоге, сделали вы?


    1. serg-bs Автор
      15.12.2019 21:13

      Спасибо за комментарий, постараюсь перекомпоновать. Я хотел показать как происходила эволюция в проектах, которые я видел. Изначально у меня была проблема что quarkus + kotlin data class не работают, spring boot 1.5 + kotlin data class не работают, Хотя spring boot 2 + kotlin data class работает. В итоге добавление флага компилятора спасает, т.е. залезаем в pom файл spring-boot-starter-parent и копируем флаги, например в quarkus проект и это спасает.


  1. kontiky
    15.12.2019 18:34

    Getter — это аннотация Jackson или Lombok?


    1. serg-bs Автор
      16.12.2019 15:56

      Lombok