Dto spring что это
Spring валидация входных DTO в Kotlin. Краткая инструкция для backend-разработчика
При переходе с Java на Kotlin многие вопросы приходится решать заново, а точнее по-другому. Два года назад мы начали социальный open source проект BrainUp, базируясь на Kotlin и Spring. Проект сейчас активно развивается, а мы узнаём на практике, что значит разрабатывать Kotlin-проект с нуля, какие удобства язык вносит в нашу жизнь, вместе с тем привнося свои вопросы, задачи, которые надо решать по-новому.
Использование, а точнее не использование data-классов в качестве entity и почему. (напишу статью позже при возможности).
Выбор code style плагина. У нас используется ktlint, инструкция настройки описана в отдельной статье.
Выбор фреймворка тестирования. У нас используется Kotest.
Выбор библиотеки для мокирования. У нас выбрана Mockk.
Варианты использования Kotest и Mockk можно посмотреть у нас в проекте.
Организация валидации входных DTO (Data Transfer Object) с помощью Spring.
Настройка Sonar для Kotlin.
В этой статье расскажу про наш опыт организации валидации входных DTO с помощью Spring, с какими вопросами мы столкнулись в ходе реализации этой идеи в Kotlin и как их решали.
Итак, для добавления валидации в проект нужно пройти эти три шага:
1 шаг. Добавление аннотаций к полям в DTO
В Java мы пользовались такими аннотациями, как @NotNull, @NotEmpty, @NotBlank и др., например:
Но такой вариант, переписанный на Kotlin, работать не будет:
Kotlin рабочий самый простой вариант будет выглядеть так:
Теперь рассмотрим подробнее валидации полей разных типов на реальных примерах.
1.1 Валидации для полей String работает как ожидается, вот интересные примеры из нашего проекта:
1.2 Валидация для типов дат, например LocalDateTime, работает тоже как ожидается:
То есть отправляя такой json < "audiometryTaskId": null >в контроллер, мы не словим ожидаемую ошибку валидации, а увидим, что было проставлено в поле audiometryTaskId значение 0. Ищем на stackoverflow, да есть такое.
Рабочее решение выглядит несколько несуразно:
Здесь поле audiometryTaskId объявлено как nullable, но аннотация говорит об обратном. Для принития этого кода, необходимо иметь в голове фразу: «By making the field nullable, you’re allowing it to be constructed, so that the JSR 303 validation can run on the object. As validator doesn’t run until the object is constructed», — что означает для этих типов для валидации необходим объект, который сначала должен быть создан, т.е. сделать поля nullable для возможности создания:
И уже далее по созданному объекту будет произведена Spring-овая валидация, далее это значение в DTO можно спокойно использовать как не nullable:
audiometryHistoryRequest.audiometryTaskId!!
При вызове функции с audiometryTaskId=null, получим MethodArgumentNotValidException:
Stacktrace
Улучшить данный вариант можно добавив читабельное сообщение message (смотрите 3й шаг откуда это сообщение берется):
В этом случае defaultMessage будет заменён нашим, и можно будет увидеть именно определённое нами сообщение в response:
Controller response
2 шаг. Добавление аннотации @Validated в контроллер.
Добавление аннотации @Validated в контроллер перед DTO, которую необходимо проверить при вызове данного end-point.
Например:
3 шаг. Добавление файла с сообщениями об ошибках (опционально).
Добавление файла с сообщениями об ошибках errorMessages.properties в папку resources, если хотите вынести сообщения в одно место.
errorMessages.properties
На этом с валидацией всё, всем желаю удачи!
Настройка валидации DTO в Spring Framework
Всем привет! Сегодня мы коснёмся валидации данных, входящих через Data Transfer Object (DTO), настроим аннотации и видимости — так, чтобы получать и отдавать только то, что нам нужно.
Итак, у нас есть DTO-класс UserDto, с соответствующими полями:
Я опускаю конструкторы и геттеры-сеттеры — уверен, вы умеете их создавать, а увеличивать в 3-4 раза код смысла не вижу — представим, что они уже есть.
Мы будем принимать DTO через контроллер с CRUD-методами. Опять же, я не буду писать все методы CRUD — для чистоты эксперимента нам хватит пары. Пусть это будут create и updateName.
Для наглядности их тоже пришлось упростить. Таким образом, мы получаем какой-то JSON, который преобразуется в UserDto, и возвращаем UserDto, который преобразуется в JSON и отправляется на клиент.
Теперь предлагаю ознакомиться с теми несколькими аннотациями валидации, с которыми мы будем работать.
Со всеми аннотациями можно ознакомиться в библиотеке javax.validation.constraints. Итак, настроим наше DTO таким образом, чтобы сразу получать валидированый объект для дальнейшего перевода в сущность и сохранения в БД. Те поля, которые должны быть заполнены, мы пометим NotNull, также пометим e-mail:
Мы задали настройки валидации для DTO — должны быть заполнены все поля, кроме id — он генерируется в БД. Добавим валидацию в контроллер:
Настроенная таким образом валидация подойдёт к созданию нового пользователя, но не подойдёт для обновления существующих — ведь для этого нам нужно будет получить id (который задан как null), а также, пропустить поля login, password и email, поскольку в updateName мы изменяем только имя. То есть, нам нужно получить id и name, и ничего больше. И здесь нам потребуются интерфейсы видимости.
Создадим прямо в классе DTO интерфейс (для наглядности, я рекомендую выносить такие вещи в отдельный класс, а лучше, в отдельный пакет, например, transfer). Интерфейс будет называться New, второй будет называться Exist, от которого мы унаследуем UpdateName (в дальнейшем мы сможем наследовать от Exist другие интерфейсы видимости, мы же не одно имя будем менять):
Теперь мы пометим наши аннотации интерфейсом New.
Теперь эти аннотации работают только при указании интерфейса New. Нам остаётся только задать аннотации для того случая, когда нам потребуется апдейтить поле name (напомню, нам нужно указать не-нулловвыми id и name, остальные нулловыми). Вот как это выглядит:
Теперь нам осталось задать необходимые настройки в контроллерах, прописать интерфейс, чтобы задать валидацию:
Теперь для каждого метода будет вызываться свой набор настроек.
Итак, мы разобрались, как валидировать входные данные, теперь осталось валидировать выходные. Это делается при помощи аннотации @JsonView.
Сейчас в выходном DTO, который мы отдаём обратно, содержатся все поля. Но, предположим, нам не нужно никогда отдавать пароль (кроме исключительных случаев).
Для валидации выходного DTO добавим ещё два интерфейса, которые будут отвечать за видимость выходных данных — Details (для отображения пользователям) и AdminDetails (для отображения только админам). Интерфейсы могут наследоваться друг от друга, но для простоты восприятия сейчас мы делать этого не будем — достаточно примера со входными данными на этот счёт.
Теперь мы можем аннотировать поля так, как нам нужно (видны все, кроме пароля):
Осталось пометить нужные методы контроллера:
А когда-нибудь в другой раз мы пометим аннотацией @JsonView(AdminDetails.class) метод, который будет дёргать только пароль. Если же мы хотим, чтобы админ получал всю информацию, а не только пароль, аннотируем соответствующим образом все нужные поля:
Надеюсь, эта статья помогла разобраться с валидацией входных DTO и видимостью данных выходных.
Как начать писать микросервис на Spring Boot, чтобы потом не болела голова
Привет! Меня зовут Женя, я Java-разработчик в Usetech, в последнее время много работаю с микросервисной архитектурой, и в этой статье хотела бы поделиться некоторыми моментами, на которые может быть полезно обратить внимание, когда вы пишете новый микросервис на Spring Boot.
Опытные разработчики могут счесть приведенные рекомендации очевидными, однако все они взяты из практики работы над реальными проектами.
1. Оставляем контроллеры тонкими
В традиционной слоистой архитектуре класс контроллера принимает запросы и направляет их сервису, а сервис занимается бизнес-логикой. Однако иногда в методах контроллера можно встретить какие-либо проверки входных параметров, а также преобразование Entity в DTO.
С одной стороны, маппинг занимает всего одну строку, да и проверка на отсутствие результата смотрится вполне логично. Однако в подобном случае нарушается принцип единой ответственности контроллера. Пока валидация или маппинг простые, пара лишних строк кода в методе контроллера совсем не бросаются в глаза, но в дальнейшем логика как валидации, так и маппинга может усложниться, и тогда станет очевидно, что контроллер не только принимает и перенаправляет запросы, но еще и занимается бизнес-логикой.
Чтобы не пришлось потом проводить объемный рефакторинг, лучше сразу, пока микросервис еще содержит минимальную функциональность, сделать все контроллеры «тонкими», лишь вызывающими методы сервиса, а валидацию и маппинг осуществлять в сервисном классе и возвращать из него сразу DTO.
Метод контроллера после рефакторинга:
Метод сервиса после рефакторинга:
2. Используем разные DTO для разных случаев
Представьте микросервис, который отдает некие данные в виде DTO по REST API, а также пишет сообщения в виде DTO в Kafka. В начале жизни микросервиса состав данных, которые нужно отдать по REST и передать в Kafka, может быть одинаков, что может спровоцировать нас использовать один и тот же DTO в обоих случаях. Проблема усугубится, если мы используем один DTO для еще большего количества разных клиентов.
Если в дальнейшем изменятся требования по составу данных для различных случаев, и мы решим выделить отдельные DTO, то появится риск в процессе выполнения этой задачи пропустить место, где нужно произвести замену на новое DTO, или наоборот заменить, где не нужно. Лучшим вариантом будет сразу предусмотреть отдельные DTO для разных случаев, даже если сейчас они совпадают по полям.
3. Вычищаем WARN-ы, пока их мало
После того, как первая задача, реализующая минимальную функциональность нового микросервиса, готова, важно не забыть обратить внимание на предупреждения, которые выводит Spring Boot в логах, и по возможности сразу исправить их, чтобы они не накопились как снежный ком.
Пример WARN, который выводится даже у «пустого» Spring Boot 2 приложения с Hibernate:
spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
Это предупреждение сообщает, что по-умолчанию в Spring Boot 2 включен режим Open Session In View, при котором сессия Hibernate держится открытой все время обработки HTTP-запроса.
Хотя режим Open Session In View позволяет избежать LazyInitializationException, которое возникает если мы пытаемся получить данные, когда сессия Hibernate уже закрыта, тем не менее, его использование является анти-паттерном. Этот режим провоцирует проблемы с производительностью приложения, поскольку приложение держит долгое соединение с базой данных, а также значительно увеличивается количество запросов, так как каждая связанная с сущностью коллекция будет загружена отдельным запросом (проблема n+1). Подробнее об этом можно прочитать в статье.
Для того, чтобы отключить режим Open Session In View нужно сделать как раз то, что написано в предупреждении — добавить в application.yml настройку:
4. Кэшируем контекст в тестах
Что заставляет контекст перезапускаться заново:
Чтобы контекст кэшировался нужно устранить все перечисленные причины, т.е. явные различия в настройках тестовых классов. Мне подошел подход с созданием базового абстрактного тестового класса, который:
Иногда в этом классе также может быть удобно держать метод очистки, запускаемый после каждого теста ( @AfterEach ), однако это уже дело вкуса/особенностей тестов.
Тестовые классы, в свою очередь, наследуются от абстрактного базового класса, не содержат конфигурации и даже полей, а только setUp метод и сами тестовые методы.
Буду рада, если какой-то из пунктов окажется для вас полезным в работе.
Не самые очевидные советы по написанию DTO на Java
Сегодня приложения зачастую имеют распределенный характер. Для подключения к другим сервисам нужно писать больше кода — и при этом стараться сделать его простым.
Чтобы воспользоваться данными из внешней службы, мы обычно преобразуем полезную нагрузку JSON в объект передачи данных (Data Transfer Object, DTO). Код, обрабатывающий DTO, быстро усложняется, но с этим могут помочь несколько советов. Вполне возможно писать DTO, с которыми легче взаимодействовать и которые облегчают написание и чтение кода. Если объединить их вместе — можно упростить себе работу.
Сериализация DTO “по учебнику”
Начнем с типичного способа работы с JSON. Вот структура JSON. Этот JSON представляет пиццу “Реджина”.
PizzaDto — «старый добрый Java-объект», POJO: объект со свойствами, геттерами, сеттерами и всем остальным. Он отражает структуру JSON, поэтому преобразование между объектом и JSON занимает всего одну строку. Вот пример этого с библиотекой Jackson:
Преобразование простое и прямолинейное. В чем же тогда проблема?
В реальной жизни DTO бывают довольно сложными. Код для создания и инициализации DTO может включать вплоть до десятков строк. Иногда больше. Это проблема, потому что сложный код содержит больше ошибок и менее чувствителен к изменениям.
Моя первая попытка упростить создание DTO — воспользоваться неизменяемым DTO: таким, который нельзя модифицировать после создания.
Такой подход может показаться странным, если вы не знакомы с этой идеей, поэтому давайте сосредоточимся на ней поподробнее.
Создание неизменяемых DTO
Если говорить просто, то объект неизменяемый, если его состояние не может поменяться после сборки.
У неизменяемого объекта нет сеттера. Все его свойства — окончательные и должны быть инициализированы при построении.
Это важно, потому что пицца “Реджина” без грибов — уже определенно не пицца “Реджина”.
Если серьезнее, то Джошуа Блох, автор книги “Java: эффективное программирование”, дает такую рекомендацию для создания неизменяемых классов:
“Если в вашем классе есть какие-либо поля, которые ссылаются на изменяемые объекты, убедитесь, что клиенты класса не могут получать ссылки на эти объекты”. — Джошуа Блох
Если какое-либо свойство вашего DTO является изменяемым, вам необходимо сделать защитные копии. С их помощью вы предотвратите модификацию вашего DTO извне.
Примечание: начиная с Java 16, существует более краткий способ создания неизменяемых классов через записи.
Хорошо. Теперь у нас есть неизменяемый DTO. Но как это упрощает код?
Преимущества неизменяемости
Неизменяемость приносит много преимуществ, но вот мое любимое: неизменяемые переменные не имеют побочных эффектов.
Рассмотрим на примере. В этом фрагменте кода есть ошибка:
После выполнения этого кода пицца не содержит ожидаемого состояния. Какая строка вызвала проблему?
Попробуем два ответа: сначала с изменяемой переменной, а затем с неизменяемой.
С неизменяемыми переменными отладка становится проще. Но это еще не все.
Поскольку pizza — неизменяемый объект, verify() не может просто исправить его. Придется создавать и возвращать измененную пиццу, а клиентский код необходимо адаптировать:
В этой новой версии очевидно, что метод verify() возвращает новую исправленную пиццу. Неизменяемость делает код более понятным. Его становится легче читать и легче развивать.
Есть и другие преимущества В своей книге Джошуа Блох просто рекомендует “минимизировать изменчивость”.
“Неизменяемые классы проще проектировать, реализовывать и использовать, чем изменяемые классы. Они менее подвержены ошибкам и более безопасны”. — Джошуа Блох
Теперь возникает интересный вопрос: можем ли мы поступать так же с DTO?
Неизменяемые DTO… А это осмысленно?
Цель DTO — передача данных между процессами. Объект инициализируется, а затем его состояние не должно меняться. Либо он будет сериализован в JSON, либо будет использоваться клиентом. Это делает неизменность естественной. Неизменяемый DTO будет передавать данные между процессами с гарантией.
Как оказалось, это не соответствует истине.
Неизменяемые DTO с Jackson
Jackson — самая распространенная JSON-библиотека для Java.
Когда у DTO есть геттеры и сеттеры, Jackson может сопоставить объект с JSON без какой-либо дополнительной настройки. Но с неизменяемыми объектами Jackson нуждается в небольшой помощи. Ему нужно знать, как собирать объект.
Вот и все. Теперь нас есть неизменяемый DTO, который Jackson может преобразовать в JSON и обратно в объект.
Неизменяемые DTO с Gson и Moshi
Есть две альтернативы Jackson: Gson и Moshi.
С помощью этих библиотек еще проще преобразовать JSON в неизменяемый DTO, потому что им не нужны никакие дополнительные аннотации.
Но почему Jackson вообще требует аннотаций, в отличие от Gson и Moshi?
Никакой магии. Дело в том, что, когда Gson и Moshi генерируют объект из JSON, они создают и инициализируют его путем отражения. Кроме того, они не задействуют конструкторы.
Я не большой поклонник такого подхода. Он вводит в заблуждение, потому что разработчик может вложить некоторую логику в конструктор и никогда не узнать, что он не вызывается. По сравнению с этим, Jackson представляется гораздо более безопасным.
Избегайте нулевых значений
У Jackson есть еще одно преимущество. Если поместить в конструктор некоторую логику, он будет вызываться всегда, независимо от того, создан ли DTO кодом приложения или сгенерирован из JSON.
Можно воспользоваться этим преимуществом для избегания значений null и улучшить конструктор для инициализации полей с ненулевыми значениями.
В приведенном ниже фрагменте кода поля инициализируются пустыми значениями, когда входные данные равны нулю.
Так вы напишете меньше кода и повысите надежность. Что может быть лучше?
И последнее по счету, но не по важности: создавайте DTO со строителями
Есть еще один совет, как упростить инициализацию DTO. В комплекте с каждым DTO я создаю Builder. Он предоставляет свободный API для облегчения инициализации DTO.
Вот пример создания PizzaDto через сборщик:
С помощью сложных DTO разработчики делают код более выразительным. Этот шаблон настолько великолепен, что Джошуа Блох почти начинает с него свою книгу “Java: эффективное программирование”.
“Такой клиентский код легко писать и, что более важно, читать”. — Джошуа Блох
Вот пример для PizzaDto :
Некоторые пользуются Lombok для создания конструкторов во время компиляции. Это упрощает DTO.
Я предпочитаю генерировать код конструктора с помощью плагина Builder generator IntelliJ. Затем можно добавить перегрузки методов, как в предыдущем фрагменте кода. Конструктор таким образом становится более гибким, а клиентский код — более компактным.
Заключение
Вот основные советы, которые я держу в голове при написании DTO. Соединенные вместе, они действительно улучшат ваш код. Кодовая база становится легче для чтения, проще в обслуживании и, в конечном счете, так проще делиться ею с вашей командой.
DTO vs POCO vs Value Object
Определения DTO, POCO и Value Object
Вначале небольшая ремарка по поводу Value Object. В C# существует похожая концепция, называемая Value Type. Это всего лишь деталь имплементации того, как объекты хранятся в памяти и мы не будем касаться этого. Value Object, о котором пойдет речь, — понятие из среды DDD (Domain-Driven Design).
Ок, давайте начнем. Вы возможно заметили, что такие понятия как DTO, Value Object и POCO часто используются как синонимы. Но действительно ли они означают одно и то же?
DTO — это класс, содержащий данные без какой-либо логики для работы с ними. DTO обычно используются для передачи данных между различными приложениями, либо между слоями внутри одного приложения. Их можно рассматривать как хранилище информации, единственная цель которого — передать эту информацию получателю.
С другой стороны, Value Object — это полноценный член вашей доменной модели. Он подчиняется тем же правилам, что и сущности (Entities). Единственное отличие между Value Object и Entity в том, что у Value Object-а нет собственной идентичности. Это означает, что два Value Object-а с одинаковыми свойствами могут считаться идентичными, в то время как две сущности отличаются друг от друга даже в случае если их свойства полностью совпадают.
Value Object-ы могут содержать логику и обычно они не используются для передачи информации между приложениями.
POJO был представлен Мартином Фаулером в качестве альтернативы для JavaBeans и других «тяжелых» enterprise-конструкций, которые были популярны в ранних 2000-х.
Основной целью POJO было показать, что домен приложения может быть успешно смоделирован без использования JavaBeans. Более того, JavaBeans вообще не должны быть использованы для этой цели.
Другой хороший пример анти-POCO подхода — Entity Framework до версии 4.0. Каждый класс, сгенерированный EF, наследовал от EntityObject, что привносило в домен логику, специфичную для EF. Начиная с версии 4, Entity Framework добавил возможность работать с POCO моделью — возможность использовать классы, которые не наследуются от EntityObject.
Таким образом, понятие POCO означает использование настолько простых классов насколько возможно для моделирования предметной области. Это понятие помогает придерживаться принципов YAGNI, KISS и остальных best practices. POCO классы могут содержать логику.
Корреляция между понятиями
Есть ли связи между этими тремя понятиями? В первую очередь, DTO и Value Object отражают разные концепции и не могут использоваться взаимозаменяемо. С другой стороны, POCO — это надмножество для DTO и Value Object:
Другими словами, Value Object и DTO не наследуют никаким сторонним компонентам и таким образом являются POCO. В то же время, POCO — это более широкое понятие: это может быть Value Object, Entity, DTO или любой другой класс в том случае если он не наследует компонентам, не относящимся напрямую к решаемой вами проблеме.
Вот свойства каждого из них:
Заметьте, что POCO-класс может и иметь, и не иметь собственной идентичности, т.к. он может быть как Value Object, так и Entity. Также, POCO может содержать, а может и не содержать логику внутри себя. Это зависит от того, является ли POCO DTO.
Заключение
Вышесказанное в статье можно суммировать следующим образом: