随着微服务和分布式应用的广泛采用,出于服务的独立性和数据的安全性方面的考虑,每个服务都会按照自己的需要定义业务数据对象,这样当服务相互调用时就要经常进行数据对象之间的映射。目前,有很多实现数据对象映射的库,本文介绍一种高性能的映射库
MapStruct
。
MapStruct简介
MapStruct
是在编译时根据定义(接口)生成映射类(实现),自动生成需要手工编写数据映射代码,通过直接调用复制数据,不需要通过反射,因此速度非常快。
本文将介绍一些MapStruct
的基础功能,包括:
- Maven安装
- 字段映射:自动映射,指定映射关系,从多个源对象映射,映射子对象;
- 类型转换:基本类型转换,枚举类型转换;
- 设置对象值:使用默认值,使用Java表达式
通过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>
定义字段映射关系
基本定义
假设我们有两个数据对象Doctor
和DoctorDto
,它们有相同的字段。注意:两个数据对象都添加了@Data
注解(lombok),用于自动生成getter/setter
方法,否则无法实现映射。
@Data
public class Doctor {
private int id;
private String name;
}
@Data
public class DoctorDto {
private int id;
private String name;
}
现在我们用MapStruct
的注解@Mapper
定义两个数据类的映射关系。
@Mapper(componentModel = "spring")
public interface DoctorMapper {
DoctorDto toDto(Doctor doctor);
}
接口中定义了数据映射方法toDto()
,接收一个Doctor
实例,返回一个DoctorDto
实例。@Mapper(componentModel = "spring")
将会给生成的实现添加@Component
注解,用于支持Spring
的依赖注入。注意:这里只指定了类型,并不需要指定字段的映射关系。
执行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;
}
}
从生成的实现类可以看到自动生成的代码和我们手工赋值的代码没有什么区别。代码中已经添加@Component
,在Spring
中可以通过依赖注入的方式使用映射类实例。
映射字段名
如果映射的数据对象字段名不一致,用@Mapping
指定映射关系。
@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") // 指定映射关系
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()); // 按照指定的关系映射
doctorDto.setId(doctor.getId());
doctorDto.setName(doctor.getName());
return doctorDto;
}
}
从多个源对象映射
有时候需要从多个数据对象映射到一个数据对象,这时需要在定义的映射函数toDto()
中指定所有源数据对象。如果多个源数据对象有同样的字段,例如:id,那么必须通过@Mapping
指定用哪个源中的字段。
@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); // 从多个对象映射
}
@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;
}
}
映射子对象
@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; // 增加了子对象的列表
}
@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; // 子对象的映射对象列表
}
@Mapper(componentModel = "spring")
public interface DoctorMapper {
@Mapping(source = "doctor.specialty", target = "specialization")
@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) {
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;
}
}
类型转换
基本类型转换
MapStruct
支持基本类型的自动类型转换,包括:
- 原始类型和相应的包裹类型间的转换,例如:
int
和Integer
,float
和Float
,long
和Long
,boolean
和Boolean
。 - 原始类型和包裹类型间的相互转换,例如:
int
和long
,byte
和Integer
等。 - 原始类型和包裹类型与
String
类型之间的转换,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"))); // 自动生成了时间类型转换代码
patientDto.setId(patient.getId());
patientDto.setName(patient.getName());
return patientDto;
}
}
枚举类型转换
MapStruct
支持枚举类型之间的转换,如果枚举名是相同的自动完成映射,如果名称不一致,通过@ValueMapping
进行映射。
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);
}
}
设置字段值
设置默认值
MapStruct
提供两种方式设置目标数据对象字段默认值,constant
和default
,constant
是不论源数据对象字段是什么值都将目标数据对象字段设置为对应的值,default
是源数据对象字段的值如果为null
就使用指定的值。
@Mapper(componentModel = "spring")
public interface DoctorMapper {
@Mapping(target = "id", constant = "-1") // 使用固定的值
@Mapping(source = "doctor.specialty", target = "specialization", defaultValue = "没有指定") // 如果源数据对象的值为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("没有指定"); // 用default指定的值
}
doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor.getPatientList()));
doctorDto.setName(doctor.getName());
}
if (education != null)
doctorDto.setDegree(education.getDegreeName());
doctorDto.setId(-1); // 用constant指定的值
return doctorDto;
}
}
使用Java表达式
除了使用constant
和default
设置目标值,还可以用expression
和defaultExpression
,expression
是忽略源数据对象的值进行设置,defaultExpression
是源对象的值如果为null
时进行设置。
@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())") // 添加了表达式
@Mapping(source = "doctor.availability", target = "availability", defaultExpression = "java(LocalDateTime.now())") // 添加了表达式
@Mapping(source = "doctor.specialty", target = "specialization", defaultValue = "没有指定")
@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()); // 使用了指定的表达式
}
if (doctor.getSpecialty() != null) {
doctorDto.setSpecialization(doctor.getSpecialty());
} else {
doctorDto.setSpecialization("没有指定");
}
doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor.getPatientList()));
doctorDto.setName(doctor.getName());
}
if (education != null)
doctorDto.setDegree(education.getDegreeName());
doctorDto.setId(-1);
doctorDto.setExternalId(UUID.randomUUID().toString()); // 使用了指定的表达式
return doctorDto;
}
}
总结
从上面的示例中可以看出MapStruct
是一个功能强大的数据对象映射工具,可以在很大程度上减少手工代码量,同时为我们提供了一个考虑数据对象映射问题的基本框架,可以提高代码质量。
本篇文章主要介绍MapStruct
的基础功能,下一篇介绍它的一些高级功能。