Рубрики
Без рубрики

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

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

Автор оригинала: Taimoor Choudhary.

Вступление

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

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

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

Структура карты

MapStruct-это генератор кода на основе Java с открытым исходным кодом, который создает код для реализаций сопоставления.

Он использует обработку аннотаций для создания реализаций классов картографов во время компиляции и значительно сокращает объем шаблонного кода, который регулярно писался бы вручную.

Зависимости от структуры карты

Если вы используете Maven, установите MapStruct, добавив зависимость:


    
        org.mapstruct
        mapstruct
        ${org.mapstruct.version}
    

Эта зависимость будет импортировать аннотации основной структуры карты. Поскольку MapStruct работает во время компиляции и прикреплен к таким конструкторам, как Maven и Gradle, нам также придется добавить плагин в <сборка> :


    
        
            org.apache.maven.plugins
            maven-compiler-plugin
            3.5.1
            
                1.8
                1.8
                
                    
                        org.mapstruct
                        mapstruct-processor
                        ${org.mapstruct.version}
                    
                
            
        
    

Если вы используете 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 и opt-eclipse в зависимости от вашей СТОРОНЫ.

Вы можете ознакомиться с последней версией по адресу Maven Central .

Основные Сопоставления

Давайте начнем с некоторых базовых карт. У нас будет Доктор модель и Докторская степень . Их поля будут иметь одинаковые имена для вашего удобства:

public class Doctor {
    private int id;
    private String name;
}

И:

public class DoctorDto {
    private int id;
    private String name;
}

Теперь, чтобы создать сопоставление между этими двумя, мы создадим интерфейс Doctor Mapper . Аннотируя его с помощью @Mapper , MapStruct знает, что это сопоставитель между нашими двумя классами:

@Mapper
public interface DoctorMapper {
    DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
    DoctorDto toDto(Doctor doctor);
}

У нас есть ЭКЗЕМПЛЯР типа Doctor Mapper|/. Это будет нашей "точкой входа" в экземпляр, как только мы создадим реализацию.

Мы определили метод toDto() в интерфейсе, который принимает экземпляр Doctor и возвращает экземпляр Doctorate . Этого достаточно, чтобы MapStruct знал, что мы хотели бы сопоставить экземпляр Doctor с экземпляром Doctor D в .

Когда мы создадим/скомпилируем приложение, плагин обработчика аннотаций структуры карты подберет интерфейс Doctor Mapper и сгенерирует для него реализацию:

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 с полями Doctor D .

Теперь, чтобы сопоставить экземпляр Doctor с экземпляром Doctor D с экземпляром , мы бы сделали:

DoctorDto doctorDto = DoctorMapper.INSTANCE.toDto(doctor);

Примечание: Возможно, вы заметили DoctorDtoBuilder в приведенной выше реализации. Мы опустили реализацию для краткости, так как строители, как правило, длинные. MapStruct попытается использовать ваш конструктор, если он присутствует в классе. Если нет, он просто создаст его с помощью ключевого слова new .

Если вы хотите узнать больше о шаблоне проектирования Builder на Java , мы вас раскроем!

Сопоставление различных полей источника и цели

Часто модель и технический директор не будут иметь одинаковые имена полей. Могут быть небольшие различия из-за того, что члены команды назначают свои собственные представления, а также из-за того, как вы хотели бы упаковать информацию для службы, которая вызвала DTO.

MapStruct обеспечивает поддержку для обработки этих ситуаций с помощью аннотации @Mapping .

Разные Имена Свойств

Давайте обновим класс Доктор , чтобы включить специальность :

public class Doctor {
    private int id;
    private String name;
    private String specialty;
}

А для Доктора D в давайте добавим специализацию поле:

public class DoctorDto {
    private int id;
    private String name;
    private String specialization;
}

Теперь нам придется сообщить вашему Доктору Картографу об этом несоответствии. Мы сделаем это, установив флаги источник и цель аннотации @Mapping с обоими этими вариантами:

@Mapper
public interface DoctorMapper {
    DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);

    @Mapping(source = "doctor.specialty", target = "specialization")
    DoctorDto toDto(Doctor doctor);
}

Специальность область Врача класса соответствует специализации области Врача D к классу.

После компиляции кода обработчик аннотаций сгенерировал эту реализацию:

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 :

Давайте создадим другую модель Образование :

public class Education {
    private String degreeName;
    private String institute;
    private Integer yearOfPassing;
}

И добавьте новое поле в Doctor в :

public class DoctorDto {
    private int id;
    private String name;
    private String degree;
    private String specialization;
}

Теперь давайте обновим интерфейс Doctor Mapper :

@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 , в которой мы установили источник в качестве имени степени класса Образования , а цель в качестве поля степени класса DoctorDto .

Если классы Образование и Врач содержат поля с одинаковыми именами – мы должны сообщить картографу, какое из них использовать, иначе он выдаст исключение. Если обе модели содержат идентификатор , нам придется выбрать, какой идентификатор будет сопоставлен свойству TO.

Сопоставление Дочерних Сущностей

В большинстве случаев POJO не содержат только примитивные типы данных. В большинстве случаев они будут содержать другие классы. Например, у Врача будет 1..n пациентов:

public class Patient {
    private int id;
    private String name;
}

И давайте составим Список из них для Врача :

public class Doctor {
    private int id;
    private String name;
    private String specialty;
    private List patientList;
}

Поскольку Данные пациента будут переданы, мы также создадим для этого DTO:

public class PatientDto {
    private int id;
    private String name;
}

И, наконец, давайте обновим Докторскую степень Списком вновь созданного Пациента D до :

public class DoctorDto {
    private int id;
    private String name;
    private String degree;
    private String specialization;
    private List patientDtoList;
}

Прежде чем мы что-либо изменим в DoctorMapper , нам нужно будет создать сопоставитель, который преобразует Пациента и Пациента в классы:

@Mapper
public interface PatientMapper {
    PatientMapper INSTANCE = Mappers.getMapper(PatientMapper.class);
    PatientDto toDto(Patient patient);
}

Это базовый картограф, который просто отображает пару примитивных типов данных.

Теперь давайте обновим наш Doctor Mapper , чтобы включить пациентов доктора:

@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 . Этот @Mapper использует другой @Mapper . Вы можете разместить здесь столько классов/картографов, сколько захотите – у нас есть только один.

Поскольку мы добавили этот флаг, при создании реализации картографа для интерфейса Doctor Mapper 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 patientListToPatientDtoList(List list) {
        if ( list == null ) {
            return null;
        }

        List list1 = new ArrayList( list.size() );
        for ( Patient patient : list ) {
            list1.add( patientMapper.toDto( patient ) );
        }

        return list1;
    }
}

Очевидно, был добавлен новый сопоставитель – Список пациентов Пациенту D в список () , помимо точки в() сопоставитель. Это делается без явного определения, просто потому, что мы добавили Сопоставитель пациентов в Сопоставитель врачей .

Метод перебирает список моделей Patient , преобразует их в PatientDto s и добавляет их в список, содержащийся в объекте DoctorDto .

Обновление Существующих Экземпляров

Иногда мы хотели бы обновить модель с последними значениями от а ДО. Используя аннотацию @MappingTarget на целевом объекте ( Доктор в нашем случае), мы можем обновить существующие экземпляры.

Давайте добавим новое @Отображение в наш Doctor Mapper , который принимает Doctor и Doctor D в экземпляры. Экземпляр Doctor D to будет источником данных, в то время как 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 list = patientDtoListToPatientList(doctorDto.getPatientDtoList());
            if (list != null) {
                doctor.getPatientList().clear();
                doctor.getPatientList().addAll(list);
            }
            else {
                doctor.setPatientList(null);
            }
        }
        else {
            List 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 , вы можете обновить конфигурацию картографа и внедрить ее как обычную зависимость.

Давайте обновим наш Doctor Mapper для работы с Spring:

@Mapper(componentModel = "spring")
public interface DoctorMapper {}

Добавление (ComponentModel) в аннотацию @Mapper сообщает MapStruct, что при создании класса реализации mapper мы хотели бы, чтобы он был создан с поддержкой внедрения зависимостей через Spring. Теперь нет необходимости добавлять поле ЭКЗЕМПЛЯР в интерфейс.

Сгенерированный DoctorMapperImpl теперь будет иметь @Компонент аннотацию:

@Component
public class DoctorMapperImpl implements DoctorMapper {}

После того , как он помечен как @компонент , Spring может использовать его в качестве компонента, и вы можете @автоматически подключать его в другом классе, например в контроллере:

@Controller
public class DoctorController() {
    @Autowired
    private DoctorMapper doctorMapper;
}

Если вы не используете Spring, MapStruct также поддерживает Java CDI :

@Mapper(componentModel = "cdi")
public interface DoctorMapper {}

Перечисления Отображений

Сопоставление перечислений работает так же, как и сопоставление полей. MapStruct без проблем сопоставит объекты с одинаковыми именами. Хотя для перечислений с разными именами мы будем использовать аннотацию @ValueMapping . Опять же, это похоже на аннотацию @Mapping с обычными типами.

Давайте создадим два перечисления, первое из которых Тип платежа :

public enum PaymentType {
    CASH,
    CHEQUE,
    CARD_VISA,
    CARD_MASTER,
    CARD_CREDIT
}

Это, скажем, доступные варианты оплаты в приложении. А теперь давайте рассмотрим эти варианты в более общем и ограниченном виде:

public enum PaymentTypeView {
    CASH,
    CHEQUE,
    CARD
}

Теперь давайте создадим интерфейс сопоставления между этими двумя перечислениями s:

@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);
}

Git Essentials

Ознакомьтесь с этим практическим руководством по изучению Git, содержащим лучшие практики и принятые в отрасли стандарты. Прекратите гуглить команды Git и на самом деле изучите это!

Здесь у нас есть общее значение КАРТЫ и более конкретные CARD_VISA , CARD_MASTER и CARD_CREDIT значения. Существует несоответствие с количеством значений – Тип платежа имеет 6 значений, в то время как Вид типа платежа имеет только 3.

Для связи между ними мы можем использовать аннотацию @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;
    }
}

НАЛИЧНЫЕ и ЧЕК по умолчанию имеют соответствующие значения, в то время как конкретное значение КАРТЫ обрабатывается через переключатель цикл.

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

Это делается с помощью Констант отображения :

@ValueMapping(source = MappingConstants.ANY_REMAINING, target = "CARD")
PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);

Здесь, после выполнения сопоставлений по умолчанию, все оставшиеся (не совпадающие) значения будут сопоставлены с КАРТОЙ .

@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 поддерживает преобразование типов данных между исходным и целевым свойствами. Он также обеспечивает автоматическое преобразование типов между примитивными типами и соответствующими им оболочками.

Автоматическое преобразование типов применяется к:

  • Преобразование между примитивными типами и их соответствующими типами оболочек . Например, преобразование между int и Целое число , поплавок и Поплавок , длинный и Длинный , логическое и Логическое и т.д.
  • Преобразование между любыми примитивными типами и любыми типами оболочек . Например, между int и long , байт и Целое число и т.д.
  • Преобразование между всеми типами примитивов и оболочек и Строкой . Например, преобразование между логическим и Строкой , Целым числом и Строкой , плавающей точкой и Строкой и т.д.

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

Позвольте обновить наши Пациенты , чтобы включить поле для хранения даты рождения :

public class PatientDto {
    private int id;
    private String name;
    private LocalDate dateOfBirth;
}

С другой стороны, скажем, наш Пациент объект имеет дату Рождения типа Строка :

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);
}

При преобразовании между датами мы также можем использовать флаг формат данных для установки спецификатора формата. Сгенерированная реализация будет выглядеть следующим образом:

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 использовал шаблон, указанный флагом формат даты . Если бы мы не указали формат, он был бы отправлен в формат по умолчанию Локальной даты :

if (patientDto.getDateOfBirth() != null) {
    patient.dateOfBirth(DateTimeFormatter.ISO_LOCAL_DATE
                        .format(patientDto.getDateOfBirth()));
}

Добавление Пользовательских Методов

До сих пор мы добавляли метод заполнителей, который мы хотим, чтобы MapStruct реализовал для нас. Что мы также можем сделать, так это добавить пользовательский по умолчанию метод в интерфейс. Добавив метод по умолчанию , мы также можем напрямую добавить реализацию. Мы сможем получить к нему доступ через экземпляр без проблем.

Для этого давайте составим Резюме пациента врача , которое содержит резюме между Врачом и списком их Пациентов :

public class DoctorPatientSummary {
    private int doctorId;
    private int patientCount;
    private String doctorName;
    private String specialization;
    private String institute;
    private List patientIds;
}

Теперь в нашем DoctorMapper мы добавим метод по умолчанию , который вместо сопоставления Doctor с Доктором D в преобразует Doctor и Образование объекты в 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.

Эта реализация будет доступна для использования после того, как класс реализации mapper будет создан MapStruct. Вы можете получить к нему доступ так же, как и к любому другому методу сопоставления:

DoctorPatientSummary summary = doctorMapper.toDoctorPatientSummary(dotor, education);

Создание Пользовательских Картографов

До сих пор мы использовали интерфейсы для создания чертежей для картографов. Мы также можем создавать чертежи с абстрактными классами, аннотированными аннотацией @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();
    }
}

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

@До сопоставления и @после сопоставления

Для дополнительного контроля и настройки мы можем определить методы @перед сопоставлением и @После сопоставления . Очевидно, что они выполняются до и после каждого сопоставления. Другими словами, эти методы будут добавлены и выполнены до и после фактического сопоставления между двумя объектами в реализации.

Давайте добавим эти методы в наш Doctor CustomMapper :

@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() выполняется до создания экземпляра объекта Doctor D to , а метод update Result() выполняется после завершения сопоставления.

Добавление Значений По Умолчанию

Полезной парой флагов, которые вы можете использовать с аннотацией @Mapping , являются константы и значения по умолчанию. Значение константа будет использоваться всегда, независимо от значения источника . Значение по умолчанию будет использоваться, если значение источник равно null .

Давайте обновим наш Doctor Mapper с константой и по умолчанию :

@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);
}

Если специальность недоступна, мы назначим вместо нее строку Информация недоступна . Кроме того, мы жестко закодировали идентификатор , чтобы быть -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.get Specialty() возвращает null , мы устанавливаем специализацию в наше сообщение по умолчанию. Идентификатор устанавливается независимо, так как это константа .

Добавление выражений Java

MapStruct позволяет вам полностью вводить выражения Java в качестве флагов в аннотацию @Mapping . Вы можете либо задать Выражение по умолчанию (если исходное значение равно null ), либо выражение//, которое является постоянным.

Давайте добавим Внешний идентификатор , который будет строкой и назначением , которое будет LocalDateTime типа для вашего Врача и Врача .

Наша Доктор модель будет выглядеть так:

public class Doctor {

    private int id;
    private String name;
    private String externalId;
    private String specialty;
    private LocalDateTime availability;
    private List patientList;
}

И Врачи будут выглядеть так:

public class DoctorDto {

    private int id;
    private String name;
    private String externalId;
    private String specialization;
    private LocalDateTime availability;
    private List patientDtoList;
}

А теперь давайте обновим наш Доктор Картограф :

@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 ()) | внешнему идентификатору , в то время как мы условно установили доступность на новое LocalDateTime , если доступность отсутствует.

Поскольку выражения являются просто строками s, мы должны указать классы, используемые в выражениях. Это не код, который оценивается, это буквальное текстовое значение. Таким образом, мы добавили импорт = {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;
    }
}

Внешний идентификатор имеет значение:

doctorDto.setExternalId(UUID.randomUUID().toString());

В то время как, если доступность равна null , она установлена в:

doctorDto.setAvailability(LocalDateTime.now());

Обработка исключений при сопоставлении

Обработка исключений неизбежна. Приложения постоянно подвергаются исключительным состояниям. MapStruct обеспечивает поддержку, позволяющую довольно легко включать обработку исключений, что значительно упрощает вашу работу в качестве разработчика.

Давайте рассмотрим сценарий, в котором мы хотим проверить нашу модель Doctor , сопоставляя ее с Doctor D в . Давайте создадим для этого отдельный Валидатор класс:

public class Validator {
    public int validateId(int id) throws ValidationException {
        if(id == -1){
            throw new ValidationException("Invalid value in ID");
        }
        return id;
    }
}

Теперь мы захотим обновить ваш Doctor Mapper , чтобы использовать класс Валидатор , без необходимости указывать реализацию. Как обычно, мы добавим классы в список классов , используемых @Mapper , и все, что нам нужно сделать, это сообщить MapStruct, что наш toDto() метод вызывает исключение 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 автоматически установил идентификатор врачей с результатом Валидатора экземпляра. Он также добавил предложение throws для метода.

Конфигурации Отображения

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

Вместо того, чтобы настраивать их вручную, мы можем настроить аналогичные типы так, чтобы они имели одинаковые/похожие методы сопоставления.

Наследование Конфигурации

Давайте вернемся к сценарию в Обновление существующих экземпляров , где мы создали сопоставитель для обновления значений существующей Модели от 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);
}

Допустим, у нас есть другой картограф, который генерирует Doctor из Doctor D в :

@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);
}

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

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