вводить
Поскольку микросервисы и распределенные приложения быстро захватывают ландшафт разработки, целостность и безопасность данных важны как никогда. Между этими слабо связанными системами первостепенное значение имеют защищенные каналы связи и ограниченная передача данных. В большинстве случаев конечным пользователям или службам требуется доступ не ко всем данным в модели, а только к некоторым определенным частям.
В этих приложениях часто используются объекты передачи данных (DTO). DTO — это просто объект, который содержит запрошенную информацию в другом объекте. Часто эта информация является ограниченной частью. Например, преобразования часто происходят между сущностями, определенными уровнем сохраняемости, и объектами DTO, отправляемыми клиенту. Поскольку DTO являются отражением исходных объектов, преобразователи между этими классами играют ключевую роль в процессе преобразования.
Это проблема, которую решает MapStruct: ручное создание модулей сопоставления компонентов занимает много времени. Но библиотека может автоматически генерировать классы сопоставления компонентов.
В этой статье мы подробно изучимMapStruct.
MapStruct
MapStruct — это генератор кода на основе Java с открытым исходным кодом для создания преобразователей расширений, реализующих преобразования между Java-бинами. С MapStruct нам нужно только создать интерфейс, а библиотека автоматически создает конкретную реализацию отображения с помощью аннотаций в процессе компиляции, что значительно сокращает объем шаблонного кода, который обычно приходится писать вручную.
Зависимости MapStruct
Если вы используете Maven, вы можете установить MapStruct, импортировав зависимости:
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
Эта зависимость импортирует основные аннотации MapStruct. Поскольку MapStruct работает во время компиляции и интегрируется с инструментами сборки, такими как Maven и Gradle, нам также нужно добавить плагин в тег maven-compiler-plugin
, а в его конфигурации добавитьannotationProcessorPaths
Плагин генерирует соответствующий код при строительстве.
<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>
Установить MapStruct проще, если вы используете Gradle:
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
Плагин позаботится об обработке аннотаций. Вы можете включить плагины в зависимости от используемой IDE.apt-idea
илиapt-eclipse
плагин.
MapStructипроцессорПоследняя стабильная версия может быть отЦентральный склад Mavenполучено в.
карта
Базовая карта
Начнем с некоторых основных отображений. Мы создадим объект Doctor и DoctorDto. Для удобства все они имеют одинаковые имена для своих полей свойств:
public class Doctor {
private int id;
private String name;
// getters and setters or builder
}
public class DoctorDto {
private int id;
private String name;
// getters and setters or builder
}
Теперь, чтобы сопоставить их, мы собираемся создатьDoctorMapper
интерфейс. использовать этот интерфейс@Mapper
Аннотация, MapStruct будет знать, что это сопоставление между двумя классами.
@Mapper
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
DoctorDto toDto(Doctor doctor);
}
Этот код создаетDoctorMapper
экземпляр типаINSTANCE
, после генерации соответствующего кода реализации, это и есть "запись", которую мы вызываем.
Определяем в интерфейсеtoDto()
метод, который получаетDoctor
instance в качестве параметра и возвращаетDoctorDto
пример. Этого достаточно, чтобы MapStruct знал, что мы хотим поместитьDoctor
экземпляр сопоставляется сDoctorDto
пример.
Когда мы собираем/компилируем приложение, плагин обработчика аннотаций MapStruct распознает интерфейс 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
. Поскольку код построителя часто бывает длинным, для краткости код реализации шаблона построителя здесь опущен. Если ваш класс содержит Builder, MapStruct попытается использовать его для создания экземпляров; если нет, MapStruct пройдетnew
ключевое слово для создания экземпляра.
Сопоставление между различными полями
Обычно имена полей моделей и DTO не совпадают. Поскольку члены группы указывают свои имена и для разных служб вызовов, разработчики выбирают разные методы упаковки для возвращаемой информации, и имена могут незначительно меняться.
MapStruct через@Mapping
Аннотации обеспечивают поддержку для таких случаев.
разные имена свойств
Давайте обновимся первымDoctor
класс, добавить свойствоspecialty
:
public class Doctor {
private int id;
private String name;
private String specialty;
// getters and setters or builder
}
существуетDoctorDto
добавить классspecialization
Атрибуты:
public class DoctorDto {
private int id;
private String name;
private String specialization;
// getters and setters or builder
}
Теперь нам нужно позволитьDoctorMapper
Знайте несоответствие здесь. мы можем использовать@Mapping
аннотацию и установить его внутреннююsource
иtarget
Теги соответственно указывают на два несовместимых поля.
@Mapper
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctor.specialty", target = "specialization")
DoctorDto toDto(Doctor doctor);
}
Значение этого кода аннотации:Doctor
серединаspecialty
поле соответствуетDoctorDto
Категорияspecialization
.
После компиляции будет сгенерирован следующий код реализации:
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;
// getters and setters or builder
}
затем кDoctorDto
Добавьте новое поле в:
public class DoctorDto {
private int id;
private String name;
private String degree;
private String specialization;
// getters and setters or builder
}
Далее будет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
аннотировать и разместить егоsource
Установить какEducation
КатегорияdegreeName
,будетtarget
Установить какDoctorDto
Категорияdegree
поле.
еслиEducation
класс иDoctor
Класс содержит поля с одинаковыми именами, и мы должны сообщить мапперу, какой из них использовать, иначе он выдаст исключение. Например, если обе модели содержатid
Поле, мы должны выбрать какой классid
Сопоставляется со свойствами DTO.
сопоставление дочерних объектов
В большинстве случаев POJO не будутТолькоСодержит базовые типы данных, которые часто содержат другие классы. Например,Doctor
В классе будет несколько классов пациентов:
public class Patient {
private int id;
private String name;
// getters and setters or builder
}
Добавить список пациентов в ДоктореList
:
public class Doctor {
private int id;
private String name;
private String specialty;
private List<Patient> patientList;
// getters and setters or builder
}
так какPatient
Чтобы преобразовать, создайте для него соответствующий DTO:
public class PatientDto {
private int id;
private String name;
// getters and setters or builder
}
Наконец, вDoctorDto
добавить хранилищеPatientDto
Список:
public class DoctorDto {
private int id;
private String name;
private String degree;
private String specialization;
private List<PatientDto> patientDtoList;
// getters and setters or builder
}
изменение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);
}
Поскольку мы имеем дело с другим классом, который необходимо отобразить, вот настройка@Mapper
аннотированныйuses
логотип, такой, что настоящий@Mapper
вы можете использовать другой@Mapper
картограф. Здесь мы добавили только один, но вы можете добавить сюда столько классов/сопоставителей, сколько захотите.
мы добавилиuses
флаг, так что в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()
. Этот метод был добавлен без явного определения просто потому, что мы поместилиPatientMapper
добавленDoctorMapper
середина.
Этот метод повторяетPatient
list, преобразуя каждый элемент вPatientDto
, и добавьте преобразованный объект вDoctorDto
в списке внутри объекта.
Обновить существующий экземпляр
Иногда мы хотим обновить свойство в модели с последним значением DTO на целевом объекте (в нашем случаеDoctorDto
)использовать@MappingTarget
аннотацию, можно обновить существующий экземпляр.
@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());
}
}
Стоит отметить, что, поскольку список пациентов является дочерним объектом в этой модели, список пациентов также обновляется.
преобразование типа данных
сопоставление типов данных
Поддержка MapStructsource
иtarget
Преобразование типа данных между свойствами. Он также обеспечивает автоматическое преобразование между примитивными типами и соответствующими им классами-оболочками.
Автоматическое преобразование типов применяется к:
- Между примитивными типами и соответствующими им классами-оболочками. Например,
int
иInteger
,float
иFloat
,long
иLong
,boolean
иBoolean
Ждать. - Между любым базовым типом и любым классом-оболочкой. как
int
иlong
,byte
иInteger
Ждать. - Все примитивные типы и классы-оболочки с
String
между. какboolean
иString
,Integer
иString
,float
иString
Ждать. - перечислить и
String
между. - Тип большого числа Java (
java.math.BigInteger
,java.math.BigDecimal
) и примитивные типы Java (включая их классы-оболочки) сString
между. - Для других ситуаций см.Официальная документация по электронной почте.
Таким образом, в процессе генерации кода преобразователя, если какой-либо из вышеперечисленных случаев возникает между исходным полем и целевым полем, MapStrcut выполнит преобразование типов самостоятельно.
мы модифицируемPatientDto
, добавить новыйdateofBirth
Поле:
public class PatientDto {
private int id;
private String name;
private LocalDate dateOfBirth;
// getters and setters or builder
}
С другой стороны, добавивPatient
Есть один в объектеString
ТипdateOfBirth
:
public class Patient {
private int id;
private String name;
private String dateOfBirth;
// getters and setters or builder
}
Создайте сопоставление между двумя:
@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();
}
}
Как видите, здесь используетсяdateFormat
Заявленный формат даты. Если бы мы не объявили формат, MapStruct использовал быLocalDate
Формат по умолчанию примерно такой:
if (patientDto.getDateOfBirth() != null) {
patient.dateOfBirth(DateTimeFormatter.ISO_LOCAL_DATE
.format(patientDto.getDateOfBirth()));
}
преобразование цифрового формата
Как вы можете видеть в приведенном выше примере, при преобразовании дат вы можете передатьdateFormat
Флаг определяет формат даты.
Кроме того, для преобразования чисел также можно использоватьnumberFormat
Укажите формат отображения:
// 数字格式转换示例
@Mapping(source = "price", target = "price", numberFormat = "$#.00")
карта перечисления
Сопоставление перечисления работает так же, как сопоставление полей. 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
Есть 5 значений иPaymentTypeView
Есть только 3.
Чтобы установить мост между этими перечислениями, мы можем использовать@ValueMappings
Аннотация, аннотация может содержать множество@ValueMapping
аннотация. Здесь мы будемsource
устанавливается в один из трех конкретных элементов перечисления, иtarget
Установить как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 не обрабатывает сопоставление по умолчанию, как раньше, а затем сопоставляет оставшиеся элементы перечисления сtarget
ценность. Вместо этого напрямуювсене прошел@ValueMapping
Значение аннотации для явного сопоставления преобразуется вtarget
ценность.
карта коллекции
Короче говоря, использование MapStruct обрабатывает сопоставления коллекций так же, как вы обрабатываете простые типы.
Мы создаем простой интерфейс или абстрактный класс и объявляем метод сопоставления. MapStruct автоматически сгенерирует код отображения на основе нашего объявления. Как правило, сгенерированный код проходит через исходную коллекцию, преобразует каждый элемент в целевой тип и добавляет каждый преобразованный элемент в целевую коллекцию.
Сопоставление списка
Сначала мы определяем новый метод отображения:
@Mapper
public interface DoctorMapper {
List<DoctorDto> map(List<Doctor> doctor);
}
Сгенерированный код примерно такой:
public class DoctorMapperImpl implements DoctorMapper {
@Override
public List<DoctorDto> map(List<Doctor> doctor) {
if ( doctor == null ) {
return null;
}
List<DoctorDto> list = new ArrayList<DoctorDto>( doctor.size() );
for ( Doctor doctor1 : doctor ) {
list.add( doctorToDoctorDto( doctor1 ) );
}
return list;
}
protected DoctorDto doctorToDoctorDto(Doctor doctor) {
if ( doctor == null ) {
return null;
}
DoctorDto doctorDto = new DoctorDto();
doctorDto.setId( doctor.getId() );
doctorDto.setName( doctor.getName() );
doctorDto.setSpecialization( doctor.getSpecialization() );
return doctorDto;
}
}
Как видите, MapStruct автоматически генерируется изDoctor
прибытьDoctorDto
картографический метод.
Но следует отметить, что если мы добавим новое поле в DTOfullName
, при генерации кода возникает ошибка:
警告: Unmapped target property: "fullName".
По сути, это означает, что MapStruct не может автоматически генерировать методы сопоставления для нас в текущей ситуации. Поэтому нам нужно вручную определитьDoctor
иDoctorDto
метод сопоставления между. Обратитесь к предыдущему разделу за подробностями.
Отображение наборов и карт
Обработка данных Set и Map аналогична обработке List. Изменить следующим образомDoctorMapper
:
@Mapper
public interface DoctorMapper {
Set<DoctorDto> setConvert(Set<Doctor> doctor);
Map<String, DoctorDto> mapConvert(Map<String, Doctor> doctor);
}
Окончательный сгенерированный код реализации выглядит следующим образом:
public class DoctorMapperImpl implements DoctorMapper {
@Override
public Set<DoctorDto> setConvert(Set<Doctor> doctor) {
if ( doctor == null ) {
return null;
}
Set<DoctorDto> set = new HashSet<DoctorDto>( Math.max( (int) ( doctor.size() / .75f ) + 1, 16 ) );
for ( Doctor doctor1 : doctor ) {
set.add( doctorToDoctorDto( doctor1 ) );
}
return set;
}
@Override
public Map<String, DoctorDto> mapConvert(Map<String, Doctor> doctor) {
if ( doctor == null ) {
return null;
}
Map<String, DoctorDto> map = new HashMap<String, DoctorDto>( Math.max( (int) ( doctor.size() / .75f ) + 1, 16 ) );
for ( java.util.Map.Entry<String, Doctor> entry : doctor.entrySet() ) {
String key = entry.getKey();
DoctorDto value = doctorToDoctorDto( entry.getValue() );
map.put( key, value );
}
return map;
}
protected DoctorDto doctorToDoctorDto(Doctor doctor) {
if ( doctor == null ) {
return null;
}
DoctorDto doctorDto = new DoctorDto();
doctorDto.setId( doctor.getId() );
doctorDto.setName( doctor.getName() );
doctorDto.setSpecialization( doctor.getSpecialization() );
return doctorDto;
}
}
Подобно отображению списка, MapStruct автоматически генерируетDoctor
преобразовать вDoctorDto
картографический метод.
Стратегия сопоставления коллекций
Во многих сценариях нам нужно преобразовать типы данных с отношениями родитель-потомок. Как правило, будет один тип данных (родительский), поля которого являются коллекцией другого типа данных (дочернего).
В этом случае MapStruct предоставляет способ выбрать способ установки или добавления подтипа к супертипу. В частности, это@Mapper
в примечанияхcollectionMappingStrategy
атрибут, который может принимать значениеACCESSOR_ONLY
,SETTER_PREFERRED
,ADDER_PREFERRED
илиTARGET_IMMUTABLE
.
Эти значения представляют разные способы присвоения значений коллекциям подтипов. Значение по умолчаниюACCESSOR_ONLY
, что означает, что подколлекции можно задавать только с помощью средств доступа.
когда родительский типCollectionполеsetter
метод недоступен, но у нас есть подтипadd
метод, эта опция пригодится; еще одна полезная ситуация в родительском типеCollectionПоля неизменяемые.
Создаем новый класс:
public class Hospital {
private List<Doctor> doctors;
// getters and setters or builder
}
В то же время определите целевой класс DTO сопоставления и определите геттер, сеттер и сумматор поля коллекции подтипа:
public class HospitalDto {
private List<DoctorDto> doctors;
// 子类型集合字段getter
public List<DoctorDto> getDoctors() {
return doctors;
}
// 子类型集合字段setter
public void setDoctors(List<DoctorDto> doctors) {
this.doctors = doctors;
}
// 子类型数据adder
public void addDoctor(DoctorDto doctorDTO) {
if (doctors == null) {
doctors = new ArrayList<>();
}
doctors.add(doctorDTO);
}
}
Создайте соответствующий маппер:
@Mapper(uses = DoctorMapper.class)
public interface HospitalMapper {
HospitalMapper INSTANCE = Mappers.getMapper(HospitalMapper.class);
HospitalDto toDto(Hospital hospital);
}
Окончательный сгенерированный код реализации:
public class HospitalMapperImpl implements HospitalMapper {
@Override
public HospitalDto toDto(Hospital hospital) {
if ( hospital == null ) {
return null;
}
HospitalDto hospitalDto = new HospitalDto();
hospitalDto.setDoctors( doctorListToDoctorDtoList( hospital.getDoctors() ) );
return hospitalDto;
}
}
Как видите, стратегия, принятая по умолчанию,ACCESSOR_ONLY
, используя метод установкиsetDoctors()
В направленииHospitalDto
Запишите данные списка в объект.
И наоборот, если вы используетеADDER_PREFERRED
В качестве картографической стратегии:
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
uses = DoctorMapper.class)
public interface HospitalMapper {
HospitalMapper INSTANCE = Mappers.getMapper(HospitalMapper.class);
HospitalDto toDto(Hospital hospital);
}
В это время преобразованные объекты DTO подтипа будут добавлены в поле коллекции супертипа один за другим с использованием метода сумматора.
public class CompanyMapperAdderPreferredImpl implements CompanyMapperAdderPreferred {
private final EmployeeMapper employeeMapper = Mappers.getMapper( EmployeeMapper.class );
@Override
public CompanyDTO map(Company company) {
if ( company == null ) {
return null;
}
CompanyDTO companyDTO = new CompanyDTO();
if ( company.getEmployees() != null ) {
for ( Employee employee : company.getEmployees() ) {
companyDTO.addEmployee( employeeMapper.map( employee ) );
}
}
return companyDTO;
}
}
Если ни в целевом DTOsetter
нет методаadder
метод, сначала пройдетgetter
Метод получает коллекцию подтипов, а затем вызывает соответствующий интерфейс коллекции для добавления объекта подтипа.
допустимыйСправочная документацияСм. определения DTO различных типов (независимо от того, содержат ли они методы установки или методы добавления) и методы добавления подтипов в коллекцию при использовании различных стратегий сопоставления.
тип реализации целевой коллекции
MapStruct поддерживает интерфейсы коллекций в качестве целевых типов для методов карты.
В этом случае в сгенерированном коде используется некоторая реализация интерфейса коллекции по умолчанию. Например, в приведенном выше примереList
Реализация по умолчаниюArrayList
.
Общие интерфейсы и соответствующие им реализации по умолчанию следующие:
Interface type | Implementation type |
---|---|
Collection |
ArrayList |
List |
ArrayList |
Map |
HashMap |
SortedMap |
TreeMap |
ConcurrentMap |
ConcurrentHashMap |
ты сможешьСправочная документацияНайдите список всех интерфейсов, поддерживаемых MapStruct, и тип реализации по умолчанию, соответствующий каждому интерфейсу.
Расширенная операция
внедрение зависимости
До сих пор мы прошли черезgetMapper()
Способ доступа к сгенерированному мапперу:
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
Однако, если вы используете Spring, просто измените конфигурацию сопоставления, вам может понравиться обычное внедрение зависимостей в качестве картографов.
ИсправлятьDoctorMapper
Для поддержки среды Spring:
@Mapper(componentModel = "spring")
public interface DoctorMapper {}
существует@Mapper
добавлено в аннотации(componentModel = "spring")
, чтобы сообщить 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 {}
Добавить значение по умолчанию
@Mapping
Аннотации имеют два очень полезных флага, которые являются константами.constant
и значение по умолчаниюdefaultValue
. несмотря ни на чтоsource
Независимо от того, как берется значение, всегда будет использоваться постоянное значение; еслиsource
ценностьnull
, будет использоваться значение по умолчанию.
немного отредактироватьDoctorMapper
,добавить однуconstant
иdefaultValue
:
@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);
}
еслиspecialty
нет в наличии, мы заменим на"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
, тоspecialization
Установить как наше сообщение по умолчанию. В любом случае будетid
задание, так как этоconstant
.
добавить выражение
MapStruct даже позволяет@Mapping
Введите выражение Java в аннотацию. вы можете установитьdefaultExpression
(source
ценностьnull
вступает в силу) илиexpression
(Похоже на постоянный, постоянный).
существуетDoctor
иDoctorDto
Два новых свойства были добавлены в оба класса, одно из нихString
ТипexternalId
, другойLocalDateTime
Типappointment
, два класса примерно следующие:
public class Doctor {
private int id;
private String name;
private String externalId;
private String specialty;
private LocalDateTime availability;
private List<Patient> patientList;
// getters and setters or builder
}
public class DoctorDto {
private int id;
private String name;
private String externalId;
private String specialization;
private LocalDateTime availability;
private List<PatientDto> patientDtoList;
// getters and setters or builder
}
Исправлять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);
}
Видно, что здесьexternalId
Значение установлено наjava(UUID.randomUUID().toString())
, если исходный объект не имеетavailability
атрибут, целевой объект будетavailability
установить на новыйLocalDateTime
объект.
Поскольку только строковое выражение, мы должны указать тип, используемый в выражении. Но выражение здесь — это не окончательное выполнение кода, а только одна буква текстового значения. Поэтому мы хотим@Mapper
добавлено вimports = {LocalDateTime.class, UUID.class}
.
Добавить пользовательский метод
Стратегия, которую мы использовали до сих пор, состоит в том, чтобы добавить метод-заполнитель и ожидать, что 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;
// getters and setters or builder
}
Далее мыDoctorMapper
добавитьdefault
метод, который будет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();
}
}
Шаблон Builder используется здесь для созданияDoctorPatientSummary
объект.
После того как MapStruct сгенерирует класс реализации преобразователя, вы можете использовать этот метод реализации точно так же, как и любой другой метод преобразователя:
DoctorPatientSummary summary = doctorMapper.toDoctorPatientSummary(dotor, education);
Создать собственный картограф
Мы всегда разрабатывали функцию сопоставления через интерфейс, на самом деле мы также можем использовать интерфейс с@Mapper
изabstract
Класс Mapper для реализации. MAPSTRUCT создаст реализацию для класса, создать аналогичный интерфейс.
Давайте перепишем предыдущий пример, на этот раз изменив его на абстрактный класс:
@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();
}
}
Вы можете использовать этот маппер таким же образом. С меньшими ограничениями использование абстрактных классов может дать нам больше контроля и выбора при создании пользовательских реализаций. Еще одним преимуществом является то, что вы можете добавить@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()
Метод будет выполнен после завершения сопоставления.
Обработка исключений сопоставления
Обработка исключений неизбежна, и приложение может в любой момент перейти в ненормальное состояние. MapStruct поддерживает обработку исключений, что упрощает работу разработчика.
Рассмотрим сценарий, в котором мы хотимDoctor
карты наDoctorDto
проверить передDoctor
Данные. Мы создаем новую независимую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
изid
Установить какValidator
Метод возвращает значение экземпляра. Он также добавляет предложение throws в сигнатуру метода.
Обратите внимание, что если тип пары свойств до и после сопоставления совпадает сValidator
Способ и из типа параметра одинаковы, то поле будет называться, когда поле отображаетсяValidator
Метод, таким образом, пожалуйста, будьте осторожны.
Конфигурация сопоставления
MapStruct предоставляет очень полезную конфигурацию для написания методов сопоставления. В большинстве случаев, если мы определили метод сопоставления между двумя типами, когда мы хотим добавить еще один метод сопоставления между тем же типом, мы склонны напрямую копировать конфигурацию сопоставления существующего метода.
На самом деле нам не нужно вручную копировать эти аннотации, просто простая конфигурация может создать идентичный/похожий метод сопоставления.
унаследованная конфигурация
Давайте рассмотрим"Обновить существующий экземпляр«В этом сценарии мы создали Mapper, обновляя свойства существующих объектов доктора, основанные на собственности объекта доктора:
@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);
}
Предположим, у нас есть другой преобразователь, который будетDoctorDto
преобразовать вDoctor
:
@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
Annotation, MapStruct извлечет другие настроенные методы, чтобы найти конфигурацию аннотации, доступную для текущего метода. Как правило, эта аннотация используется дляmapping
после методаupdate
метод следующим образом:
@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);
}
Наследовать обратную конфигурацию
Существует еще один похожий сценарий, когда написание функции отображения будетModelПреобразовать вDTO, И воляDTOПреобразовать вModel. Как показано в коде ниже, мы должны добавить одну и ту же аннотацию к обеим функциям.
@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);
}
Конфигурация двух методов не будет точно такой же, на самом деле их следует поменять местами. будетModelПреобразовать вDTO, и воляDTOПреобразовать вModel- то же самое до и после поля сопоставления, исходное и целевое атрибутивное поле атрибутивного поля меняются местами.
Мы можем использовать на втором методе@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 предоставляет мощный подключаемый модуль интеграции, который сокращает усилия разработчика по написанию кода шаблона, делая процесс создания картографов быстрым и легким.
Если вы хотите изучить более подробное использование, вы можете обратиться к официально предоставленному MapStruct.Справочное руководство.
Более качественные статьи можно переместить в личные блоги:
или
Обратите внимание на публичный аккаунт