隨著微服務(wù)和分布式應(yīng)用的廣泛采用,出于服務(wù)的獨(dú)立性和數(shù)據(jù)的安全性方面的考慮,每個(gè)服務(wù)都會(huì)按照自己的需要定義業(yè)務(wù)數(shù)據(jù)對(duì)象,這樣當(dāng)服務(wù)相互調(diào)用時(shí)就要經(jīng)常進(jìn)行數(shù)據(jù)對(duì)象之間的映射。目前,有很多實(shí)現(xiàn)數(shù)據(jù)對(duì)象映射的庫(kù),本文介紹一種高性能的映射庫(kù)
MapStruct。
MapStruct簡(jiǎn)介
MapStruct是在編譯時(shí)根據(jù)定義(接口)生成映射類(lèi)(實(shí)現(xiàn)),自動(dòng)生成需要手工編寫(xiě)數(shù)據(jù)映射代碼,通過(guò)直接調(diào)用復(fù)制數(shù)據(jù),不需要通過(guò)反射,因此速度非常快。
本文將介紹一些MapStruct的基礎(chǔ)功能,包括:
- Maven安裝
- 字段映射:自動(dòng)映射,指定映射關(guān)系,從多個(gè)源對(duì)象映射,映射子對(duì)象;
- 類(lèi)型轉(zhuǎn)換:基本類(lèi)型轉(zhuǎn)換,枚舉類(lèi)型轉(zhuǎn)換;
- 設(shè)置對(duì)象值:使用默認(rèn)值,使用Java表達(dá)式
通過(guò)Maven安裝
在pom.xml中安裝MapStruct。
指定MapStruct的版本。
<properties>
<org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
MapStruct作用于編譯階段,需要在build中添加插件。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
定義字段映射關(guān)系
基本定義
假設(shè)我們有兩個(gè)數(shù)據(jù)對(duì)象Doctor和DoctorDto,它們有相同的字段。注意:兩個(gè)數(shù)據(jù)對(duì)象都添加了@Data注解(lombok),用于自動(dòng)生成getter/setter方法,否則無(wú)法實(shí)現(xiàn)映射。
@Data
public class Doctor {
private int id;
private String name;
}
@Data
public class DoctorDto {
private int id;
private String name;
}
現(xiàn)在我們用MapStruct的注解@Mapper定義兩個(gè)數(shù)據(jù)類(lèi)的映射關(guān)系。
@Mapper(componentModel = "spring")
public interface DoctorMapper {
DoctorDto toDto(Doctor doctor);
}
接口中定義了數(shù)據(jù)映射方法toDto(),接收一個(gè)Doctor實(shí)例,返回一個(gè)DoctorDto實(shí)例。@Mapper(componentModel = "spring")將會(huì)給生成的實(shí)現(xiàn)添加@Component注解,用于支持Spring的依賴(lài)注入。注意:這里只指定了類(lèi)型,并不需要指定字段的映射關(guān)系。
執(zhí)行mvn compile生成DoctorMapperImpl.class。
@Component
public class DoctorMapperImpl implements DoctorMapper {
public DoctorDto toDto(Doctor doctor) {
if (doctor == null)
return null;
DoctorDto doctorDto = new DoctorDto();
doctorDto.setId(doctor.getId());
doctorDto.setName(doctor.getName());
return doctorDto;
}
}
從生成的實(shí)現(xiàn)類(lèi)可以看到自動(dòng)生成的代碼和我們手工賦值的代碼沒(méi)有什么區(qū)別。代碼中已經(jīng)添加@Component,在Spring中可以通過(guò)依賴(lài)注入的方式使用映射類(lèi)實(shí)例。
映射字段名
如果映射的數(shù)據(jù)對(duì)象字段名不一致,用@Mapping指定映射關(guān)系。
@Data
public class Doctor {
private int id;
private String name;
private String specialty; // 不一致的字段名
}
@Data
public class DoctorDto {
private int id;
private String name;
private String specialization; // 不一致的字段名
}
@Mapper(componentModel = "spring")
public interface DoctorMapper {
@Mapping(source = "doctor.specialty", target = "specialization") // 指定映射關(guān)系
DoctorDto toDto(Doctor doctor);
}
@Component
public class DoctorMapperImpl implements DoctorMapper {
public DoctorDto toDto(Doctor doctor) {
if (doctor == null)
return null;
DoctorDto doctorDto = new DoctorDto();
doctorDto.setSpecialization(doctor.getSpecialty()); // 按照指定的關(guān)系映射
doctorDto.setId(doctor.getId());
doctorDto.setName(doctor.getName());
return doctorDto;
}
}
從多個(gè)源對(duì)象映射
有時(shí)候需要從多個(gè)數(shù)據(jù)對(duì)象映射到一個(gè)數(shù)據(jù)對(duì)象,這時(shí)需要在定義的映射函數(shù)toDto()中指定所有源數(shù)據(jù)對(duì)象。如果多個(gè)源數(shù)據(jù)對(duì)象有同樣的字段,例如:id,那么必須通過(guò)@Mapping指定用哪個(gè)源中的字段。
@Data
public class Education {
private String degreeName;
private String institute;
private Integer yearOfPassing;
}
@Data
public class DoctorDto {
private int id;
private String name;
private String degree;
private String specialization;
}
@Mapper(componentModel = "spring")
public interface DoctorMapper {
@Mapping(source = "doctor.specialty", target = "specialization")
@Mapping(source = "education.degreeName", target = "degree")
DoctorDto toDto(Doctor doctor, Education education); // 從多個(gè)對(duì)象映射
}
@Component
public class DoctorMapperImpl implements DoctorMapper {
public DoctorDto toDto(Doctor doctor, Education education) {
if (doctor == null && education == null)
return null;
DoctorDto doctorDto = new DoctorDto();
if (doctor != null) {
doctorDto.setSpecialization(doctor.getSpecialty());
doctorDto.setId(doctor.getId());
doctorDto.setName(doctor.getName());
}
if (education != null)
doctorDto.setDegree(education.getDegreeName());
return doctorDto;
}
}
映射子對(duì)象
@Data
public class Patient {
private int id;
private String name;
}
@Data
public class Doctor {
private int id;
private String name;
private String specialty;
private List<Patient> patientList; // 增加了子對(duì)象的列表
}
@Data
public class PatientDto {
private int id;
private String name;
}
@Data
public class DoctorDto {
private int id;
private String name;
private String degree;
private String specialization;
private List<PatientDto> patientDtoList; // 子對(duì)象的映射對(duì)象列表
}
@Mapper(componentModel = "spring")
public interface DoctorMapper {
@Mapping(source = "doctor.specialty", target = "specialization")
@Mapping(source = "doctor.patientList", target = "patientDtoList") // 子對(duì)象列表的映射關(guān)系
@Mapping(source = "education.degreeName", target = "degree")
DoctorDto toDto(Doctor doctor, Education education);
}
@Component
public class DoctorMapperImpl implements DoctorMapper {
public DoctorDto toDto(Doctor doctor, Education education) {
if (doctor == null && education == null)
return null;
DoctorDto doctorDto = new DoctorDto();
if (doctor != null) {
doctorDto.setSpecialization(doctor.getSpecialty());
doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor.getPatientList()));
doctorDto.setId(doctor.getId());
doctorDto.setName(doctor.getName());
}
if (education != null)
doctorDto.setDegree(education.getDegreeName());
return doctorDto;
}
protected PatientDto patientToPatientDto(Patient patient) {
if (patient == null)
return null;
PatientDto patientDto = new PatientDto();
patientDto.setId(patient.getId());
patientDto.setName(patient.getName());
return patientDto;
}
protected List<PatientDto> patientListToPatientDtoList(List<Patient> list) {
if (list == null)
return null;
List<PatientDto> list1 = new ArrayList<>(list.size());
for (Patient patient : list)
list1.add(patientToPatientDto(patient));
return list1;
}
}
類(lèi)型轉(zhuǎn)換
基本類(lèi)型轉(zhuǎn)換
MapStruct支持基本類(lèi)型的自動(dòng)類(lèi)型轉(zhuǎn)換,包括:
- 原始類(lèi)型和相應(yīng)的包裹類(lèi)型間的轉(zhuǎn)換,例如:
int和Integer,float和Float,long和Long,boolean和Boolean。 - 原始類(lèi)型和包裹類(lèi)型間的相互轉(zhuǎn)換,例如:
int和long,byte和Integer等。 - 原始類(lèi)型和包裹類(lèi)型與
String類(lèi)型之間的轉(zhuǎn)換,boolean和String,Integer和String,float和String等。
@Data
public class PatientDto {
private int id;
private String name;
private LocalDate dateOfBirth;
}
@Data
public class Patient {
private int id;
private String name;
private String dateOfBirth;
}
@Mapper(componentModel = "spring")
public interface PatientMapper {
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
PatientDto toDto(Patient patient);
}
@Component
public class PatientMapperImpl implements PatientMapper {
public PatientDto toDto(Patient patient) {
if (patient == null)
return null;
PatientDto patientDto = new PatientDto();
if (patient.getDateOfBirth() != null)
patientDto.setDateOfBirth(LocalDate.parse(patient.getDateOfBirth(), DateTimeFormatter.ofPattern("dd/MMM/yyyy"))); // 自動(dòng)生成了時(shí)間類(lèi)型轉(zhuǎn)換代碼
patientDto.setId(patient.getId());
patientDto.setName(patient.getName());
return patientDto;
}
}
枚舉類(lèi)型轉(zhuǎn)換
MapStruct支持枚舉類(lèi)型之間的轉(zhuǎn)換,如果枚舉名是相同的自動(dòng)完成映射,如果名稱(chēng)不一致,通過(guò)@ValueMapping進(jìn)行映射。
public enum PaymentType {
CASH, CHEQUE, CARD_VISA, CARD_MASTER, CARD_CREDIT
}
public enum PaymentTypeView {
CASH, CHEQUE, CARD
}
@Mapper(componentModel = "spring")
public interface PaymentTypeMapper {
@ValueMappings({ @ValueMapping(source = "CARD_VISA", target = "CARD"),
@ValueMapping(source = "CARD_MASTER", target = "CARD"), @ValueMapping(source = "CARD_CREDIT", target = "CARD") })
PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);
}
@Component
public class PaymentTypeMapperImpl implements PaymentTypeMapper {
public PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType) {
PaymentTypeView paymentTypeView;
if (paymentType == null)
return null;
switch (paymentType) {
case CARD_VISA:
paymentTypeView = PaymentTypeView.CARD;
return paymentTypeView;
case CARD_MASTER:
paymentTypeView = PaymentTypeView.CARD;
return paymentTypeView;
case null:
paymentTypeView = PaymentTypeView.CARD;
return paymentTypeView;
case CASH:
paymentTypeView = PaymentTypeView.CASH;
return paymentTypeView;
case CHEQUE:
paymentTypeView = PaymentTypeView.CHEQUE;
return paymentTypeView;
}
throw new IllegalArgumentException("Unexpected enum constant: " + paymentType);
}
}
設(shè)置字段值
設(shè)置默認(rèn)值
MapStruct提供兩種方式設(shè)置目標(biāo)數(shù)據(jù)對(duì)象字段默認(rèn)值,constant和default,constant是不論源數(shù)據(jù)對(duì)象字段是什么值都將目標(biāo)數(shù)據(jù)對(duì)象字段設(shè)置為對(duì)應(yīng)的值,default是源數(shù)據(jù)對(duì)象字段的值如果為null就使用指定的值。
@Mapper(componentModel = "spring")
public interface DoctorMapper {
@Mapping(target = "id", constant = "-1") // 使用固定的值
@Mapping(source = "doctor.specialty", target = "specialization", defaultValue = "沒(méi)有指定") // 如果源數(shù)據(jù)對(duì)象的值為null,使用指定的值
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "education.degreeName", target = "degree")
DoctorDto toDto(Doctor doctor, Education education);
}
@Component
public class DoctorMapperImpl implements DoctorMapper {
public DoctorDto toDto(Doctor doctor, Education education) {
if (doctor == null && education == null)
return null;
DoctorDto doctorDto = new DoctorDto();
if (doctor != null) {
if (doctor.getSpecialty() != null) {
doctorDto.setSpecialization(doctor.getSpecialty());
} else {
doctorDto.setSpecialization("沒(méi)有指定"); // 用default指定的值
}
doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor.getPatientList()));
doctorDto.setName(doctor.getName());
}
if (education != null)
doctorDto.setDegree(education.getDegreeName());
doctorDto.setId(-1); // 用constant指定的值
return doctorDto;
}
}
使用Java表達(dá)式
除了使用constant和default設(shè)置目標(biāo)值,還可以用expression和defaultExpression,expression是忽略源數(shù)據(jù)對(duì)象的值進(jìn)行設(shè)置,defaultExpression是源對(duì)象的值如果為null時(shí)進(jìn)行設(shè)置。
@Data
public class Doctor {
private int id;
private String name;
private String externalId; // 新添字段
private String specialty;
private LocalDateTime availability; // 新添字段
private List<Patient> patientList;
}
@Data
public class DoctorDto {
private int id;
private String name;
private String externalId; // 新添字段
private String degree;
private String specialization;
private LocalDateTime availability; // 新添字段
private List<PatientDto> patientDtoList;
}
@Mapper(componentModel = "spring")
public interface DoctorMapper {
@Mapping(target = "id", constant = "-1")
@Mapping(target = "externalId", expression = "java(UUID.randomUUID().toString())") // 添加了表達(dá)式
@Mapping(source = "doctor.availability", target = "availability", defaultExpression = "java(LocalDateTime.now())") // 添加了表達(dá)式
@Mapping(source = "doctor.specialty", target = "specialization", defaultValue = "沒(méi)有指定")
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "education.degreeName", target = "degree")
DoctorDto toDto(Doctor doctor, Education education);
}
@Component
public class DoctorMapperImpl implements DoctorMapper {
public DoctorDto toDto(Doctor doctor, Education education) {
if (doctor == null && education == null)
return null;
DoctorDto doctorDto = new DoctorDto();
if (doctor != null) {
if (doctor.getAvailability() != null) {
doctorDto.setAvailability(doctor.getAvailability());
} else {
doctorDto.setAvailability(LocalDateTime.now()); // 使用了指定的表達(dá)式
}
if (doctor.getSpecialty() != null) {
doctorDto.setSpecialization(doctor.getSpecialty());
} else {
doctorDto.setSpecialization("沒(méi)有指定");
}
doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor.getPatientList()));
doctorDto.setName(doctor.getName());
}
if (education != null)
doctorDto.setDegree(education.getDegreeName());
doctorDto.setId(-1);
doctorDto.setExternalId(UUID.randomUUID().toString()); // 使用了指定的表達(dá)式
return doctorDto;
}
}
總結(jié)
從上面的示例中可以看出MapStruct是一個(gè)功能強(qiáng)大的數(shù)據(jù)對(duì)象映射工具,可以在很大程度上減少手工代碼量,同時(shí)為我們提供了一個(gè)考慮數(shù)據(jù)對(duì)象映射問(wèn)題的基本框架,可以提高代碼質(zhì)量。
本篇文章主要介紹MapStruct的基礎(chǔ)功能,下一篇介紹它的一些高級(jí)功能。