Вступление
Поскольку микросервисы и распределенные приложения быстро захватывают мир разработки, целостность и безопасность данных важны как никогда. Безопасный канал связи и ограниченная передача данных между этими слабосвязанными системами имеют первостепенное значение. В большинстве случаев конечному пользователю или службе требуется доступ не ко всем данным модели, а только к некоторым конкретным частям.
Объекты передачи данных (DTO) регулярно применяются в этих приложениях. DTO - это просто объекты, которые содержат запрошенную информацию о другом объекте. Обычно информация ограничена по объему. Поскольку DTO являются отражением исходных объектов, сопоставители между этими классами играют ключевую роль в процессе преобразования.
В этой статье мы погрузимся в MapStruct - обширный картограф для Java Beans.
Оглавление:
- MapStruct
- Основные сопоставления
- Сопоставление различных исходных и целевых полей
- Сопоставление дочерних сущностей
- Обновление существующих экземпляров
- Внедрение зависимости
- Отображение перечислений
- Сопоставление типов данных
- Добавление собственных методов
- Создание настраиваемых картографов
- @BeforeMapping и @AfterMapping
- Добавление значений по умолчанию
- Добавление выражений Java
- Обработка исключений при отображении
MapStruct
MapStruct - это генератор кода на основе Java с открытым исходным кодом, который создает код для реализации сопоставления.
Он использует обработку аннотаций для создания реализаций классов сопоставления во время компиляции и значительно сокращает объем стандартного кода, который обычно должен быть написан вручную.
Зависимости MapStruct
Если вы используете Maven, установите MapStruct, добавив зависимость:
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
Эта зависимость импортирует основные аннотации MapStruct. Поскольку
MapStruct работает во время компиляции и прикреплен к сборщикам, таким
как Maven и Gradle, нам также придется добавить плагин в <build>
:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Если вы используете Gradle , установить MapStruct так же просто:
plugins {
id 'net.ltgt.apt' version '0.20'
}
apply plugin: 'net.ltgt.apt-idea'
apply plugin: 'net.ltgt.apt-eclipse'
dependencies {
compile "org.mapstruct:mapstruct:${mapstructVersion}"
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
}
net.ltgt.apt
отвечает за обработку аннотаций. Вы можете применить
apt-idea
и apt-eclipse
зависимости от вашей IDE.
Вы можете проверить последнюю версию наMaven Central .
Основные сопоставления
Начнем с базового картирования. У нас будет модель Doctor
DoctorDto
. Для нашего удобства их поля будут иметь одинаковые имена:
public class Doctor {
private int id;
private String name;
}
А также:
public class DoctorDto {
private int id;
private String name;
}
Теперь, чтобы создать сопоставитель между этими двумя, мы создадим
интерфейс DoctorMapper
@Mapper
его с помощью @Mapper, MapStruct
знает, что это сопоставитель между нашими двумя классами:
@Mapper
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
DoctorDto toDto(Doctor doctor);
}
У нас есть INSTANCE
типа DoctorMapper
. Это будет наша «точка входа»
в экземпляр после того, как мы сгенерируем реализацию.
Мы определили toDto()
в интерфейсе, который принимает Doctor
и
возвращает экземпляр DoctorDto
Этого достаточно, чтобы MapStruct знал,
что мы хотим сопоставить экземпляр Doctor
DoctorDto
.
Когда мы создаем / компилируем приложение, плагин процессора аннотаций
DoctorMapper
интерфейс DoctorMapper и генерирует для него реализацию:
public class DoctorMapperImpl implements DoctorMapper {
@Override
public DoctorDto toDto(Doctor doctor) {
if ( doctor == null ) {
return null;
}
DoctorDtoBuilder doctorDto = DoctorDto.builder();
doctorDto.id(doctor.getId());
doctorDto.name(doctor.getName());
return doctorDto.build();
}
}
Класс DoctorMapperImpl
теперь содержит toDto()
который сопоставляет
наши поля Doctor
DoctorDto
.
Теперь, чтобы сопоставить экземпляр Doctor
DoctorDto
, мы должны
сделать:
DoctorDto doctorDto = DoctorMapper.INSTANCE.toDto(doctor);
Примечание. Возможно, вы заметили DoctorDtoBuilder
в приведенной
выше реализации. Мы опускаем реализацию для краткости, так как сборщики
обычно длинны. MapStruct попытается использовать ваш конструктор, если
он присутствует в классе. Если нет, он просто создаст его экземпляр с
помощью ключевого слова new
Если вы хотите узнать больше о шаблоне проектирования Builder на Java , мы вам поможем!
Сопоставление различных исходных и целевых полей
Часто модель и DTO не имеют одинаковых имен полей. Могут быть небольшие различия из-за того, что члены команды назначают свои собственные представления, и как вы хотите упаковать информацию для службы, которая вызывала DTO.
MapStruct предоставляет поддержку для обработки таких ситуаций с помощью
аннотации @Mapping
Различные названия свойств
Давайте обновим Doctor
включив в него specialty
:
public class Doctor {
private int id;
private String name;
private String specialty;
}
А для DoctorDto
добавим поле specialization
public class DoctorDto {
private int id;
private String name;
private String specialization;
}
Теперь нам нужно сообщить нашему DoctorMapper
об этом несоответствии.
Мы сделаем это, установив флаги source
и target
@Mapping
с обоими
этими вариантами:
@Mapper
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctor.specialty", target = "specialization")
DoctorDto toDto(Doctor doctor);
}
Область specialty
Doctor
соответствует области specialization
класса DoctorDto
После компиляции кода обработчик аннотаций сгенерировал эту реализацию:
public class DoctorMapperImpl implements DoctorMapper {
@Override
public DoctorDto toDto(Doctor doctor) {
if (doctor == null) {
return null;
}
DoctorDtoBuilder doctorDto = DoctorDto.builder();
doctorDto.specialization(doctor.getSpecialty());
doctorDto.id(doctor.getId());
doctorDto.name(doctor.getName());
return doctorDto.build();
}
}
Несколько исходных классов
Иногда одного класса недостаточно для создания DTO. Иногда мы хотим
агрегировать значения из нескольких классов в один DTO для конечного
пользователя. Это также делается путем установки соответствующих флагов
в аннотации @Mapping
Создадим еще одну модель Education
:
public class Education {
private String degreeName;
private String institute;
private Integer yearOfPassing;
}
И добавляем новое поле в DoctorDto
:
public class DoctorDto {
private int id;
private String name;
private String degree;
private String specialization;
}
Теперь давайте обновим интерфейс DoctorMapper
@Mapper
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctor.specialty", target = "specialization")
@Mapping(source = "education.degreeName", target = "degree")
DoctorDto toDto(Doctor doctor, Education education);
}
Мы добавили еще один @Mapping
аннотации , в которой мы установили
источник как degreeName
в Education
класса, и target
, как
degree
поле DoctorDto
класса.
Если Education
и Doctor
содержат поля с одинаковыми именами - мы
должны сообщить картографу, какое из них использовать, иначе он выдаст
исключение. Если обе модели содержат id
, нам нужно будет выбрать,
какой id
будет сопоставлен со свойством DTO.
Сопоставление дочерних сущностей
В большинстве случаев POJO содержат не только примитивные типы данных.
В большинстве случаев они будут содержать другие классы. Например, у
Doctor
будет 1..n
пациентов:
public class Patient {
private int id;
private String name;
}
И составим их List
для Doctor
:
public class Doctor {
private int id;
private String name;
private String specialty;
private List<Patient> patientList;
}
Поскольку Patient
будут перенесены, мы также создадим для него DTO:
public class PatientDto {
private int id;
private String name;
}
И, наконец, давайте обновим DoctorDto
List
только что созданных
PatientDto
:
public class DoctorDto {
private int id;
private String name;
private String degree;
private String specialization;
private List<PatientDto> patientDtoList;
}
Прежде чем мы изменим что-либо в DoctorMapper
, нам нужно будет
создать картограф, который преобразует классы Patient
и PatientDto
@Mapper
public interface PatientMapper {
PatientMapper INSTANCE = Mappers.getMapper(PatientMapper.class);
PatientDto toDto(Patient patient);
}
Это базовый маппер, который просто отображает пару примитивных типов данных.
Теперь давайте обновим наш DoctorMapper
чтобы включить в него
пациентов доктора:
@Mapper(uses = {PatientMapper.class})
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization")
DoctorDto toDto(Doctor doctor);
}
Так как мы работаем с другим классом , который требует сопоставлений, мы
установили uses
флаг @Mapper
аннотации. Этот @Mapper
использует
другой @Mapper
. Вы можете разместить здесь столько классов /
картографов, сколько захотите - у нас только один.
Поскольку мы добавили этот флаг, при создании реализации сопоставителя
для DoctorMapper
интерфейса, MapStruct также преобразовать Patient
модель в PatientDto
- так как мы зарегистрировали PatientMapper
для
этой задачи.
Теперь компиляция приложения приведет к новой реализации:
public class DoctorMapperImpl implements DoctorMapper {
private final PatientMapper patientMapper = Mappers.getMapper( PatientMapper.class );
@Override
public DoctorDto toDto(Doctor doctor) {
if ( doctor == null ) {
return null;
}
DoctorDtoBuilder doctorDto = DoctorDto.builder();
doctorDto.patientDtoList( patientListToPatientDtoList(doctor.getPatientList()));
doctorDto.specialization( doctor.getSpecialty() );
doctorDto.id( doctor.getId() );
doctorDto.name( doctor.getName() );
return doctorDto.build();
}
protected List<PatientDto> patientListToPatientDtoList(List<Patient> list) {
if ( list == null ) {
return null;
}
List<PatientDto> list1 = new ArrayList<PatientDto>( list.size() );
for ( Patient patient : list ) {
list1.add( patientMapper.toDto( patient ) );
}
return list1;
}
}
toDto()
patientListToPatientDtoList()
был добавлен новый маппер -
PatientListToPatientDtoList (). Это делается без явного определения
просто потому, что мы добавили PatientMapper
к DoctorMapper
.
Метод выполняет итерацию по списку Patient
, преобразует их в
PatientDto
и добавляет их в список, содержащийся в объекте DoctorDto
Обновление существующих экземпляров
Иногда нам нужно обновить модель с использованием последних значений из
DTO. Используя @MappingTarget
к целевому объекту ( Doctor
), мы
можем обновить существующие экземпляры.
Давайте добавим новый @Mapping
в наш DoctorMapper
который принимает
экземпляры Doctor
и DoctorDto
DoctorDto
будет источником данных, а
Doctor
- целью:
@Mapper(uses = {PatientMapper.class})
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctorDto.patientDtoList", target = "patientList")
@Mapping(source = "doctorDto.specialization", target = "specialty")
void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor);
}
Теперь, после повторной генерации реализации, у нас есть метод
updateModel()
:
public class DoctorMapperImpl implements DoctorMapper {
@Override
public void updateModel(DoctorDto doctorDto, Doctor doctor) {
if (doctorDto == null) {
return;
}
if (doctor.getPatientList() != null) {
List<Patient> list = patientDtoListToPatientList(doctorDto.getPatientDtoList());
if (list != null) {
doctor.getPatientList().clear();
doctor.getPatientList().addAll(list);
}
else {
doctor.setPatientList(null);
}
}
else {
List<Patient> list = patientDtoListToPatientList(doctorDto.getPatientDtoList());
if (list != null) {
doctor.setPatientList(list);
}
}
doctor.setSpecialty(doctorDto.getSpecialization());
doctor.setId(doctorDto.getId());
doctor.setName(doctorDto.getName());
}
}
Стоит отметить, что список пациентов также обновляется, поскольку это дочерний объект модуля.
Внедрение зависимости
До сих пор мы получали доступ к сгенерированным картографам через метод
getMapper()
:
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
Однако, если вы используете Spring , вы можете обновить конфигурацию сопоставителя и внедрить ее как обычную зависимость.
Давайте обновим наш DoctorMapper
для работы со Spring:
@Mapper(componentModel = "spring")
public interface DoctorMapper {}
Добавление (componentModel = "spring")
в @Mapper
сообщает MapStruct,
что при создании класса реализации сопоставителя мы хотели бы, чтобы он
создавался с поддержкой внедрения зависимостей через Spring. Теперь нет
необходимости добавлять INSTANCE
в интерфейс.
Созданный DoctorMapperImpl
теперь будет иметь аннотацию @Component
@Component
public class DoctorMapperImpl implements DoctorMapper {}
После того, как он помечен как @Component
, Spring может забрать его
как bean-компонент, и вы можете использовать @Autowire
в другом
классе, таком как контроллер:
@Controller
public class DoctorController() {
@Autowired
private DoctorMapper doctorMapper;
}
Если вы не используете Spring, MapStruct также поддерживает Java CDI :
@Mapper(componentModel = "cdi")
public interface DoctorMapper {}
Отображение перечислений
Сопоставление перечислений работает так же, как сопоставление полей.
MapStruct без проблем отобразит те, которые имеют такие же имена. Хотя
для перечислений с разными именами мы будем использовать аннотацию
@ValueMapping
Опять же, это похоже на @Mapping
с обычными типами.
Создадим два перечисления, первое из которых - PaymentType
:
public enum PaymentType {
CASH,
CHEQUE,
CARD_VISA,
CARD_MASTER,
CARD_CREDIT
}
Это, скажем, доступные варианты оплаты в приложении. А теперь давайте взглянем на эти варианты в более общем и ограниченном виде:
public enum PaymentTypeView {
CASH,
CHEQUE,
CARD
}
Теперь давайте создадим интерфейс сопоставления между этими двумя enum
:
@Mapper
public interface PaymentTypeMapper {
PaymentTypeMapper INSTANCE = Mappers.getMapper(PaymentTypeMapper.class);
@ValueMappings({
@ValueMapping(source = "CARD_VISA", target = "CARD"),
@ValueMapping(source = "CARD_MASTER", target = "CARD"),
@ValueMapping(source = "CARD_CREDIT", target = "CARD")
})
PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);
}
Здесь у нас есть общее CARD
и более конкретные значения CARD_VISA
,
CARD_MASTER
и CARD_CREDIT
. Несовпадение количества значений - у
PaymentType
6 значений, а у PaymentTypeView
только 3.
@ValueMappings
их, мы можем использовать аннотацию @ValueMappings,
которая принимает несколько аннотаций @ValueMapping
Здесь мы можем
установить источник как любой из трех конкретных случаев, а цель - как
значение CARD
MapStruct обработает эти случаи:
public class PaymentTypeMapperImpl implements PaymentTypeMapper {
@Override
public PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType) {
if (paymentType == null) {
return null;
}
PaymentTypeView paymentTypeView;
switch (paymentType) {
case CARD_VISA: paymentTypeView = PaymentTypeView.CARD;
break;
case CARD_MASTER: paymentTypeView = PaymentTypeView.CARD;
break;
case CARD_CREDIT: paymentTypeView = PaymentTypeView.CARD;
break;
case CASH: paymentTypeView = PaymentTypeView.CASH;
break;
case CHEQUE: paymentTypeView = PaymentTypeView.CHEQUE;
break;
default: throw new IllegalArgumentException( "Unexpected enum constant: " + paymentType );
}
return paymentTypeView;
}
}
CASH
и CHEQUE
имеют соответствующие значения по умолчанию, тогда как
конкретное CARD
обрабатывается через цикл switch
Однако этот подход может стать непрактичным, если у вас есть много значений, которые вы хотите присвоить более общему. Вместо того, чтобы назначать каждое из них вручную, мы можем просто позволить MapStruct просмотреть все доступные оставшиеся значения и сопоставить их все с другим.
Это делается через MappingConstants
:
@ValueMapping(source = MappingConstants.ANY_REMAINING, target = "CARD")
PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);
Здесь, после того, как сопоставления по умолчанию выполнены, все
оставшиеся (не совпадающие) значения будут сопоставлены с CARD
.
@Override
public PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType) {
if ( paymentType == null ) {
return null;
}
PaymentTypeView paymentTypeView;
switch ( paymentType ) {
case CASH: paymentTypeView = PaymentTypeView.CASH;
break;
case CHEQUE: paymentTypeView = PaymentTypeView.CHEQUE;
break;
default: paymentTypeView = PaymentTypeView.CARD;
}
return paymentTypeView;
}
Другой вариант - использовать ANY_UNMAPPED
:
@ValueMapping(source = MappingConstants.ANY_UNMAPPED, target = "CARD")
PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);
В этом случае вместо того, чтобы сначала сопоставить значения по умолчанию, а затем сопоставить оставшиеся с одной целью, MapStruct просто сопоставит все несопоставленные значения с целью.
Сопоставление типов данных
MapStruct поддерживает преобразование типов данных между source
и
target
свойствами. Он также обеспечивает автоматическое преобразование
типов между примитивными типами и их соответствующими оболочками.
Автоматическое преобразование типов применяется к:
- Преобразование между примитивными типами и их соответствующими
типами-оболочками . Например, преобразование между
int
иInteger
,float
иFloat
,long
иLong
,boolean
иBoolean
и т. Д. - Преобразование между любыми примитивными типами и любыми
типами-оболочками . Например, между
int
иlong
,byte
иInteger
и т. Д. - Преобразование между всеми типами примитивов и оболочек и
String
. Например, преобразование междуboolean
иString
,Integer
иString
,float
иString
и т. Д.
Таким образом, во время генерации кода сопоставителя, если преобразование типа между исходным и целевым полем подпадает под любой из вышеперечисленных сценариев, MapStrcut самостоятельно обработает преобразование типа.
Давайте обновим наш PatientDto
чтобы включить поле для хранения
dateofBirth
:
public class PatientDto {
private int id;
private String name;
private LocalDate dateOfBirth;
}
С другой стороны, предположим, что у нашего объекта Patient
dateOfBirth
типа String
:
public class Patient {
private int id;
private String name;
private String dateOfBirth;
}
Теперь давайте продолжим и создадим сопоставление между этими двумя:
@Mapper
public interface PatientMapper {
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
Patient toModel(PatientDto patientDto);
}
При преобразовании между датами мы также можем использовать dateFormat
чтобы установить спецификатор формата. Сгенерированная реализация будет
выглядеть так:
public class PatientMapperImpl implements PatientMapper {
@Override
public Patient toModel(PatientDto patientDto) {
if (patientDto == null) {
return null;
}
PatientBuilder patient = Patient.builder();
if (patientDto.getDateOfBirth() != null) {
patient.dateOfBirth(DateTimeFormatter.ofPattern("dd/MMM/yyyy")
.format(patientDto.getDateOfBirth()));
}
patient.id(patientDto.getId());
patient.name(patientDto.getName());
return patient.build();
}
}
Обратите внимание, что MapStruct использовал шаблон, предоставленный
флагом dateFormat
Если бы мы не указали формат, для него был бы
установлен формат по умолчанию для LocalDate
:
if (patientDto.getDateOfBirth() != null) {
patient.dateOfBirth(DateTimeFormatter.ISO_LOCAL_DATE
.format(patientDto.getDateOfBirth()));
}
Добавление собственных методов
До сих пор мы добавляли метод заполнителя, который мы хотим, чтобы
MapStruct реализовал за нас. Что мы также можем сделать, так это
добавить в интерфейс default
Добавляя default
, мы также можем
напрямую добавить реализацию. Мы сможем получить к нему доступ через
экземпляр без проблем.
Для этого давайте DoctorPatientSummary
, который содержит сводку между
Doctor
и списком их Patient
:
public class DoctorPatientSummary {
private int doctorId;
private int patientCount;
private String doctorName;
private String specialization;
private String institute;
private List<Integer> patientIds;
}
Теперь в нашем DoctorMapper
мы добавим default
который вместо
сопоставления Doctor
с DoctorDto
преобразует объекты Doctor
и
Education
DoctorPatientSummary
:
@Mapper
public interface DoctorMapper {
default DoctorPatientSummary toDoctorPatientSummary(Doctor doctor, Education education) {
return DoctorPatientSummary.builder()
.doctorId(doctor.getId())
.doctorName(doctor.getName())
.patientCount(doctor.getPatientList().size())
.patientIds(doctor.getPatientList()
.stream()
.map(Patient::getId)
.collect(Collectors.toList()))
.institute(education.getInstitute())
.specialization(education.getDegreeName())
.build();
}
}
Этот объект создается из Doctor
и Education
с использованием шаблона
Builder Design.
Эта реализация будет доступна для использования после того, как MapStruct сгенерирует класс реализации mapper. Вы можете получить к нему доступ так же, как и любой другой метод сопоставления:
DoctorPatientSummary summary = doctorMapper.toDoctorPatientSummary(dotor, education);
Создание настраиваемых картографов
До сих пор мы использовали интерфейсы для создания чертежей для
картографов. Мы также можем создавать чертежи с abstract
классами,
@Mapper
аннотацией @Mapper. MapStruct создаст реализацию для этого
класса, аналогичную созданию реализации интерфейса.
Давайте перепишем предыдущий пример, но на этот раз сделаем его
abstract
классом:
@Mapper
public abstract class DoctorCustomMapper {
public DoctorPatientSummary toDoctorPatientSummary(Doctor doctor, Education education) {
return DoctorPatientSummary.builder()
.doctorId(doctor.getId())
.doctorName(doctor.getName())
.patientCount(doctor.getPatientList().size())
.patientIds(doctor.getPatientList()
.stream()
.map(Patient::getId)
.collect(Collectors.toList()))
.institute(education.getInstitute())
.specialization(education.getDegreeName())
.build();
}
}
Вы можете использовать эту реализацию так же, как и реализацию
интерфейса. Использование abstract
классов дает нам больше контроля и
возможностей при создании пользовательских реализаций из-за меньших
ограничений. Еще одно преимущество - возможность добавлять
@BeforeMapping
и @AfterMapping
.
@BeforeMapping и @AfterMapping
Для дополнительного контроля и настройки мы можем определить
@BeforeMapping
и @AfterMapping
. Очевидно, они выполняются до и
после каждого сопоставления. То есть эти методы будут добавлены и
выполнены до и после фактического сопоставления двух объектов в
реализации.
Добавим эти методы в наш DoctorCustomMapper
:
@Mapper(uses = {PatientMapper.class}, componentModel = "spring")
public abstract class DoctorCustomMapper {
@BeforeMapping
protected void validate(Doctor doctor) {
if(doctor.getPatientList() == null){
doctor.setPatientList(new ArrayList<>());
}
}
@AfterMapping
protected void updateResult(@MappingTarget DoctorDto doctorDto) {
doctorDto.setName(doctorDto.getName().toUpperCase());
doctorDto.setDegree(doctorDto.getDegree().toUpperCase());
doctorDto.setSpecialization(doctorDto.getSpecialization().toUpperCase());
}
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization")
public abstract DoctorDto toDoctorDto(Doctor doctor);
}
Теперь давайте сгенерируем маппер на основе этого класса:
@Component
public class DoctorCustomMapperImpl extends DoctorCustomMapper {
@Autowired
private PatientMapper patientMapper;
@Override
public DoctorDto toDoctorDto(Doctor doctor) {
validate(doctor);
if (doctor == null) {
return null;
}
DoctorDto doctorDto = new DoctorDto();
doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor
.getPatientList()));
doctorDto.setSpecialization(doctor.getSpecialty());
doctorDto.setId(doctor.getId());
doctorDto.setName(doctor.getName());
updateResult(doctorDto);
return doctorDto;
}
}
Метод validate()
DoctorDto
до создания экземпляра объекта
updateResult()
после завершения сопоставления.
Добавление значений по умолчанию
Пара полезных флагов, которые вы можете использовать с @Mapping
- это
константы и значения по умолчанию. constant
значение, независимо от
значения source
. source
значение равно null
будет использоваться
значение по default
.
Давайте обновим наш DoctorMapper
с constant
и default
:
@Mapper(uses = {PatientMapper.class}, componentModel = "spring")
public interface DoctorMapper {
@Mapping(target = "id", constant = "-1")
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization", defaultValue = "Information Not Available")
DoctorDto toDto(Doctor doctor);
}
Если специальность недоступна, вместо Information Not Available
Кроме
того, мы жестко запрограммировали id
на -1
.
Сгенерируем маппер:
@Component
public class DoctorMapperImpl implements DoctorMapper {
@Autowired
private PatientMapper patientMapper;
@Override
public DoctorDto toDto(Doctor doctor) {
if (doctor == null) {
return null;
}
DoctorDto doctorDto = new DoctorDto();
if (doctor.getSpecialty() != null) {
doctorDto.setSpecialization(doctor.getSpecialty());
}
else {
doctorDto.setSpecialization("Information Not Available");
}
doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor.getPatientList()));
doctorDto.setName(doctor.getName());
doctorDto.setId(-1);
return doctorDto;
}
}
Если doctor.getSpecialty()
возвращает null
, мы устанавливаем
специализацию для нашего сообщения по умолчанию. id
устанавливается
независимо, поскольку он constant
.
Добавление выражений Java
MapStruct позволяет вам полностью вводить выражения Java в качестве
флагов аннотации @Mapping
Вы можете установить defaultExpression
по
умолчанию (если source
значение равно null
) или expression
которое является константой.
Давайте добавим externalId
который будет String
и appointment
которая будет иметь LocalDateTime
для наших Doctor
и DoctorDto
.
Наша Doctor
будет выглядеть так:
public class Doctor {
private int id;
private String name;
private String externalId;
private String specialty;
private LocalDateTime availability;
private List<Patient> patientList;
}
И DoctorDto
будет выглядеть так:
public class DoctorDto {
private int id;
private String name;
private String externalId;
private String specialization;
private LocalDateTime availability;
private List<PatientDto> patientDtoList;
}
А теперь давайте обновим наш DoctorMapper
:
@Mapper(uses = {PatientMapper.class}, componentModel = "spring", imports = {LocalDateTime.class, UUID.class})
public interface DoctorMapper {
@Mapping(target = "externalId", expression = "java(UUID.randomUUID().toString())")
@Mapping(source = "doctor.availability", target = "availability", defaultExpression = "java(LocalDateTime.now())")
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization")
DoctorDto toDtoWithExpression(Doctor doctor);
}
Здесь мы присвоили значение java(UUID.randomUUID().toString())
externalId
, в то время как мы условно установили доступность на новый
LocalDateTime
, если availability
отсутствует.
Поскольку выражения представляют собой просто String
s, мы должны
указать классы, используемые в выражениях. Это не код, который
оценивается, это буквальное текстовое значение. Таким образом, мы
добавили imports = {LocalDateTime.class, UUID.class}
в аннотацию
@Mapper
Сгенерированный маппер будет выглядеть так:
@Component
public class DoctorMapperImpl implements DoctorMapper {
@Autowired
private PatientMapper patientMapper;
@Override
public DoctorDto toDtoWithExpression(Doctor doctor) {
if (doctor == null) {
return null;
}
DoctorDto doctorDto = new DoctorDto();
doctorDto.setSpecialization(doctor.getSpecialty());
if (doctor.getAvailability() != null) {
doctorDto.setAvailability(doctor.getAvailability());
}
else {
doctorDto.setAvailability(LocalDateTime.now());
}
doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor
.getPatientList()));
doctorDto.setId(doctor.getId());
doctorDto.setName(doctor.getName());
doctorDto.setExternalId(UUID.randomUUID().toString());
return doctorDto;
}
}
Для externalId
установлено значение:
doctorDto.setExternalId(UUID.randomUUID().toString());
В то время как, если availability
равна null
, устанавливается
значение:
doctorDto.setAvailability(LocalDateTime.now());
Обработка исключений при отображении
Обработка исключений неизбежна. Приложения постоянно находятся в исключительных состояниях. MapStruct предоставляет поддержку, позволяющую легко включать обработку исключений, что значительно упрощает вашу работу как разработчика.
Давайте рассмотрим сценарий, в котором мы хотим проверить нашу Doctor
при сопоставлении ее с DoctorDto
. Сделаем для этого Validator
public class Validator {
public int validateId(int id) throws ValidationException {
if(id == -1){
throw new ValidationException("Invalid value in ID");
}
return id;
}
}
Теперь мы хотим обновить наш DoctorMapper
чтобы использовать
Validator
, без необходимости указывать реализацию. Как обычно, мы
добавим классы в список классов, используемых @Mapper
, и все, что нам
нужно сделать, это сообщить MapStruct, что наш toDto()
throws ValidationException
:
@Mapper(uses = {PatientMapper.class, Validator.class}, componentModel = "spring")
public interface DoctorMapper {
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization")
DoctorDto toDto(Doctor doctor) throws ValidationException;
}
Теперь давайте сгенерируем реализацию для этого картографа:
@Component
public class DoctorMapperImpl implements DoctorMapper {
@Autowired
private PatientMapper patientMapper;
@Autowired
private Validator validator;
@Override
public DoctorDto toDto(Doctor doctor) throws ValidationException {
if (doctor == null) {
return null;
}
DoctorDto doctorDto = new DoctorDto();
doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor
.getPatientList()));
doctorDto.setSpecialization(doctor.getSpecialty());
doctorDto.setId(validator.validateId(doctor.getId()));
doctorDto.setName(doctor.getName());
doctorDto.setExternalId(doctor.getExternalId());
doctorDto.setAvailability(doctor.getAvailability());
return doctorDto;
}
}
MapStruct автоматически установил идентификатор doctorDto
с
результатом экземпляра Validator
Он также добавил предложение throws
для метода.
Отображение конфигураций
MapStruct предоставляет очень полезную конфигурацию для написания методов картографирования. В большинстве случаев конфигурации сопоставления, которые мы указываем для метода сопоставления, реплицируются при добавлении другого метода сопоставления для аналогичных типов.
Вместо того, чтобы настраивать их вручную, мы можем настроить аналогичные типы, чтобы иметь одинаковые / похожие методы сопоставления.
Наследовать конфигурацию
Давайте вернемся к сценарию в разделе « Обновление существующих
экземпляров» , где мы создали средство
сопоставления для обновления значений существующей модели Doctor
из
объекта DoctorDto
@Mapper(uses = {PatientMapper.class})
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctorDto.patientDtoList", target = "patientList")
@Mapping(source = "doctorDto.specialization", target = "specialty")
void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor);
}
Скажем, у нас есть другой картограф, который генерирует Doctor
из
DoctorDto
:
@Mapper(uses = {PatientMapper.class, Validator.class})
public interface DoctorMapper {
@Mapping(source = "doctorDto.patientDtoList", target = "patientList")
@Mapping(source = "doctorDto.specialization", target = "specialty")
Doctor toModel(DoctorDto doctorDto);
}
Оба этих метода сопоставления используют одну и ту же конфигурацию.
source
и target
одинаковы. Вместо того, чтобы повторять конфигурации
для обоих методов сопоставления, мы можем использовать аннотацию
@InheritConfiguration
@InheritConfiguration
аннотацией @InheritConfiguration, MapStruct
будет искать другой, уже настроенный метод, конфигурация которого может
быть применена и к этому. Обычно эта аннотация используется для методов
обновления после метода сопоставления, точно так же, как мы ее
используем:
@Mapper(uses = {PatientMapper.class, Validator.class}, componentModel = "spring")
public interface DoctorMapper {
@Mapping(source = "doctorDto.specialization", target = "specialty")
@Mapping(source = "doctorDto.patientDtoList", target = "patientList")
Doctor toModel(DoctorDto doctorDto);
@InheritConfiguration
void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor);
}
Наследовать инверсную конфигурацию
Другой подобный сценарий - написание функций сопоставления для сопоставления модели с DTO и DTO с моделью , как в приведенном ниже коде, где мы должны указать одно и то же сопоставление исходной цели для обеих функций:
Ваши конфигурации не всегда будут одинаковыми . Например, они могут быть обратными. Сопоставление модели с DTO и DTO с моделью - вы используете те же поля, но в обратном порядке. Вот как это обычно выглядит:
@Mapper(componentModel = "spring")
public interface PatientMapper {
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
Patient toModel(PatientDto patientDto);
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
PatientDto toDto(Patient patient);
}
Вместо того, чтобы писать это дважды, мы можем использовать
@InheritInverseConfiguration
во втором методе:
@Mapper(componentModel = "spring")
public interface PatientMapper {
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
Patient toModel(PatientDto patientDto);
@InheritInverseConfiguration
PatientDto toDto(Patient patient);
}
Сгенерированный код из обеих реализаций маппера будет одинаковым.
Заключение
В этой статье мы изучили MapStruct - библиотеку для создания классов сопоставления, начиная с сопоставлений базового уровня и заканчивая настраиваемыми методами и настраиваемыми сопоставителями. Мы также рассмотрели различные варианты, предоставляемые MapStruct, включая внедрение зависимостей, сопоставления типов данных, сопоставления перечислений и использование выражений.
MapStruct предоставляет мощный плагин интеграции для уменьшения объема кода, который должен писать пользователь, и делает процесс создания картографов простым и быстрым.
Исходный код примера кода можно найти здесь .