Руководство по MapStruct на Java - Расширенная библиотека картографии

Введение Поскольку микросервисы и распределенные приложения быстро захватывают мир разработки, целостность и безопасность данных становятся важнее, чем когда-либо. Безопасный канал связи и ограниченная передача данных между этими слабосвязанными системами имеют первостепенное значение. В большинстве случаев конечному пользователю или службе требуется доступ не ко всем данным модели, а только к некоторым конкретным частям. Объекты передачи данных (DTO) регулярно применяются в этих приложениях. DTO - это просто объекты

Вступление

Поскольку микросервисы и распределенные приложения быстро захватывают мир разработки, целостность и безопасность данных важны как никогда. Безопасный канал связи и ограниченная передача данных между этими слабосвязанными системами имеют первостепенное значение. В большинстве случаев конечному пользователю или службе требуется доступ не ко всем данным модели, а только к некоторым конкретным частям.

Объекты передачи данных (DTO) регулярно применяются в этих приложениях. DTO - это просто объекты, которые содержат запрошенную информацию о другом объекте. Обычно информация ограничена по объему. Поскольку DTO являются отражением исходных объектов, сопоставители между этими классами играют ключевую роль в процессе преобразования.

В этой статье мы погрузимся в MapStruct - обширный картограф для Java Beans.

Оглавление:

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

Исходный код примера кода можно найти здесь .

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus