Руководство пользователя MapStruct

Java

вводить

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

В этих приложениях часто используются объекты передачи данных (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()метод, который получаетDoctorinstance в качестве параметра и возвращает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середина.

Этот метод повторяетPatientlist, преобразуя каждый элемент в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аннотацию, чтобы избежать дублирования конфигурации этих двух методов сопоставления.

Если добавить к методу@InheritConfigurationAnnotation, 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.Справочное руководство.


Более качественные статьи можно переместить в личные блоги:

код целостности дороги

или

Обратите внимание на публичный аккаунт

码道诚公