Как описываются DTO объекты: вручную или с помощью библиотек
Что проверяют
Какой у вас реальный опыт с DTO в современной Java и понимаете ли вы цену каждого инструмента. Следом обычно спрашивают, чем вы маппите сущности в DTO и почему не отдаёте сущность JPA наружу напрямую.
Как отвечать
Разделите вопрос на два: как описать сам класс и как заполнять его данными.
Описание класса:
record: с Java 16 это стандартный выбор для DTO. Неизменяемость,equals,hashCode,toStringиз коробки, Jackson работает с записями без дополнительных настроек;- Lombok:
@Valueдля неизменяемых объектов,@Builderпри многих необязательных полях,@Jacksonized, чтобы Jackson собирал объект через билдер. Цена: annotation processor, плагин в IDE и завязка на внутренностиjavac, из-за которой при переходе на новую JDK иногда ждут обновления Lombok; - генерация из контракта: при API-first подходе DTO создаёт OpenAPI Generator из спецификации, и руками их не пишут вовсе.
Маппинг:
- вручную: статический метод
from(entity)или конструктор. Прозрачно, но при двадцати полях одно легко забыть; - MapStruct: генерирует обычный Java-код маппера на этапе компиляции, без рефлексии. С
unmappedTargetPolicy = ReportingPolicy.ERRORзабытое поле ломает сборку; - ModelMapper: сопоставляет поля по именам через рефлексию в рантайме. Быстро на старте, но ошибки всплывают уже в работе.
Итог для ответа: record плюс MapStruct или ручной маппинг, Lombok там, где записи неудобны.
Где спотыкаются
Ставят @Data на JPA-сущность и хвалят за удобство. Сгенерированные equals и hashCode трогают ленивые связи, а toString на двусторонней связи уходит в бесконечную рекурсию.
Отдают сущность наружу вместо DTO. Контракт API намертво привязывается к схеме базы, а ленивые поля при сериализации дают LazyInitializationException или лишние запросы.
Путают назначение: говорят, что DTO нужен ради сокращения сетевых вызовов. Изначально паттерн действительно был про это, но в Spring-приложении его главная роль другая: отделить контракт API от внутренней модели.