大多数时候,终端用户或服务不需要访问模型中的全部数据,而只需要访问某些特定的部分。数据传输对象(Data Transfer Objects, DTO)经常被用于这些应用中。DTO只是持有另一个对象中被请求的信息。通常情况下,这些信息是有限的一部分。例如,在持久化层定义的实体和发往客户端的DTO之间经常会出现相互之间的转换。由于DTO是原始对象的反映,因此这些类之间的映射器在转换过程中扮演着关键角色。这就是MapStruct解决的问题:手动创建bean映射器非常耗时。 但是该库可以自动生成Bean映射器类。
MapStruct是一个开源的基于Java的代码生成器,用于创建实现Java Bean之间转换的扩展映射器。使用MapStruct,我们只需要创建接口,而该库会通过注解在编译过程中自动创建具体的映射实现,大大减少了通常需要手工编写的样板代码的数量。
1.1 MapStruct 依赖
<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.3.1.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct-processor -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.3.1.Final</version>
</dependency>
-Djps.track.ap.dependencies=false
1.2 基本映射
我们先从一些基本的映射开始,我们会创建一个Doctor对象和一个DoctorDTO。为了方便起见,它们的属性字段都使用相同的名称 :
package com.zs.entity.mapstruct;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Doctor {
private int id;
private String name;
}
package com.zs.entity.mapstruct;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DoctorDTO {
private int id;
private String name;
}
为了在这两者之间进行映射,我们要创建一个DoctorMapper接口。对该接口使用@Mapper注解,MapStruct就会知道这是两个类之间的映射器。
package com.zs.entity.mapstruct.mapper;
import com.zs.entity.mapstruct.Doctor;
import com.zs.entity.mapstruct.DoctorDTO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
DoctorDTO toDTO(Doctor doctor);
}
这段代码中创建了一个DoctorMapper类型的实例INSTANCE,在生成对应的实现代码后,这就是我们调用的“入口”。我们在接口中定义了toDTO()方法,该方法接收一个Doctor实例为参数,并返回一个DoctorDTO实例。这足以让MapStruct知道我们想把一个Doctor实例映射到一个DoctorDTO实例。
当我们构建/编译应用程序时,MapStruct注解处理器插件会识别出DoctorMapper接口并为其生成一个实现类。
package com.zs.entity.mapstruct.mapper;
import com.zs.entity.mapstruct.Doctor;
import com.zs.entity.mapstruct.DoctorDTO;
import com.zs.entity.mapstruct.DoctorDTO.DoctorDTOBuilder;
public class DoctorMapperImpl implements DoctorMapper {
public DoctorMapperImpl() {
}
public DoctorDTO toDTO(Doctor doctor) {
if (doctor == null) {
return null;
} else {
DoctorDTOBuilder doctorDTO = DoctorDTO.builder();
doctorDTO.id(doctor.getId());
doctorDTO.name(doctor.getName());
return doctorDTO.build();
}
}
}
DoctorMapperImpl类中包含一个toDTO()方法,将我们的Doctor属性值映射到DoctorDTO的属性字段中。
如果要将Doctor实例映射到一个DoctorDTO实例,可以这样写:
@Test
public void test() {
Doctor doctor = new Doctor(1001, "Bethune");
DoctorDTO doctorDTO = DoctorMapper.INSTANCE.toDTO(doctor);
System.out.println(doctorDTO);
}
1.3 不同字段间映射
MapStruct通过@Mapping注解对这类情况提供了支持。
不同属性名称
package com.zs.entity.mapstruct;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Doctor {
private int id;
private String name;
private String specialty;
}
package com.zs.entity.mapstruct;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DoctorDTO {
private int id;
private String name;
private String specialization;
}
现在,我们需要让 DoctorMapper 知道这里的不一致。我们可以使用 @Mapping 注解,并设置其内部的 source 和 target 标记分别指向不一致的两个字段。
package com.zs.entity.mapstruct.mapper;
@Mapper
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctor.specialty", target = "specialization")
DoctorDTO toDTO(Doctor doctor);
}
这个注解代码的含义是:Doctor中的specialty字段对应于DoctorDTO类的 specialization 。
package com.zs.entity.mapstruct.mapper;
import com.zs.entity.mapstruct.Doctor;
import com.zs.entity.mapstruct.DoctorDTO;
import com.zs.entity.mapstruct.DoctorDTO.DoctorDTOBuilder;
public class DoctorMapperImpl implements DoctorMapper {
public DoctorMapperImpl() {
}
public DoctorDTO toDTO(Doctor doctor) {
if (doctor == null) {
return null;
} else {
DoctorDTOBuilder doctorDTO = DoctorDTO.builder();
doctorDTO.specialization(doctor.getSpecialty());
doctorDTO.id(doctor.getId());
doctorDTO.name(doctor.getName());
return doctorDTO.build();
}
}
}
测试:
@Test
public void test() {
Doctor doctor = new Doctor(1001, "Bethune", "pediatrics");
DoctorDTO doctorDTO = DoctorMapper.INSTANCE.toDTO(doctor);
System.out.println(doctorDTO);
}
1.4 多个源类
有时,单个类不足以构建DTO,我们可能希望将多个类中的值聚合为一个DTO,供终端用户使用。这也可以通过在@Mapping注解中设置适当的标志来完成。
我们先新建另一个对象 Education:
package com.zs.entity.mapstruct;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Education {
private String degreeName;
private String institute;
private Integer yearOfPassing;
}
然后向 DoctorDTO中添加一个新的字段:
package com.zs.entity.mapstruct;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DoctorDTO {
private int id;
private String name;
private String degree; // 新添加的字段
private String specialization;
}
接下来,将 DoctorMapper 接口更新为如下代码:
package com.zs.entity.mapstruct.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注解,并将其source设置为Education类的degreeName,将target设置为DoctorDTO类的degree字段。
如果 Education 类和 Doctor 类包含同名的字段,我们必须让映射器知道使用哪一个,否则它会抛出一个异常。举例来说,如果两个模型都包含一个id字段,我们就要选择将哪个类中的id映射到DTO属性中。
package com.zs.entity.mapstruct.mapper;
import com.zs.entity.mapstruct.Doctor;
import com.zs.entity.mapstruct.DoctorDTO;
import com.zs.entity.mapstruct.Education;
import com.zs.entity.mapstruct.DoctorDTO.DoctorDTOBuilder;
public class DoctorMapperImpl implements DoctorMapper {
public DoctorMapperImpl() {
}
public DoctorDTO toDTO(Doctor doctor, Education education) {
if (doctor == null && education == null) {
return null;
} else {
DoctorDTOBuilder doctorDTO = DoctorDTO.builder();
if (doctor != null) {
doctorDTO.specialization(doctor.getSpecialty());
doctorDTO.id(doctor.getId());
doctorDTO.name(doctor.getName());
}
if (education != null) {
doctorDTO.degree(education.getDegreeName());
}
return doctorDTO.build();
}
}
}
1.5 子对象映射
多数情况下,POJO中不会只包含基本数据类型,其中往往会包含其它类。比如说,一个Doctor类中会有多个患者类:
package com.zs.entity.mapstruct;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Patient {
private int id;
private String name;
}
在Doctor中添加一个患者列表List:
package com.zs.entity.mapstruct;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Doctor {
private int id;
private String name;
private String specialty;
private List<Patient> patientList;
}
因为Patient需要转换,为其创建一个对应的DTO:
package com.zs.entity.mapstruct;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PatientDTO {
private int id;
private String name;
}
最后,在 DoctorDTO 中新增一个存储 PatientDTO的列表:
package com.zs.entity.mapstruct;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DoctorDTO {
private int id;
private String name;
private String degree;
private String specialization;
private List<PatientDTO> patientDTOList;
}
在修改 DoctorMapper之前,我们先创建一个支持 Patient 和 PatientDTO 转换的映射器接口:
package com.zs.entity.mapstruct.mapper;
@Mapper
public interface PatientMapper {
PatientMapper INSTANCE = Mappers.getMapper(PatientMapper.class);
PatientDTO toDto(Patient patient);
}
这是一个基本映射器,只会处理几个基本数据类型。然后,我们再来修改 DoctorMapper 处理一下患者列表:
package com.zs.entity.mapstruct.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注解的uses标志,这样现在的 @Mapper 就可以使用另一个 @Mapper映射器。我们这里只加了一个,但你想在这里添加多少class/mapper都可以。
我们已经添加了uses标志,所以在为DoctorMapper接口生成映射器实现时,MapStruct 也会把 Patient 模型转换成 PatientDTO ——因为我们已经为这个任务注册了 PatientMapper。
package com.zs.entity.mapstruct.mapper;
public class DoctorMapperImpl implements DoctorMapper {
private final PatientMapper patientMapper = (PatientMapper)Mappers.getMapper(PatientMapper.class);
public DoctorMapperImpl() {
}
public DoctorDTO toDTO(Doctor doctor) {
if (doctor == null) {
return null;
} else {
DoctorDTOBuilder doctorDTO = DoctorDTO.builder();
doctorDTO.specialization(doctor.getSpecialty());
doctorDTO.patientDTOList(this.patientListToPatientDTOList(doctor.getPatientList()));
doctorDTO.id(doctor.getId());
doctorDTO.name(doctor.getName());
return doctorDTO.build();
}
}
protected List<PatientDTO> patientListToPatientDTOList(List<Patient> list) {
if (list == null) {
return null;
} else {
List<PatientDTO> list1 = new ArrayList(list.size());
Iterator var3 = list.iterator();
while(var3.hasNext()) {
Patient patient = (Patient)var3.next();
list1.add(this.patientMapper.toDto(patient));
}
return list1;
}
}
}
1.6 更新现有实例
有时,我们希望用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()方法:
package com.zs.entity.mapstruct.mapper;
public class DoctorMapperImpl implements DoctorMapper {
public DoctorMapperImpl() {
}
public void updateModel(DoctorDTO doctorDTO, Doctor doctor) {
if (doctorDTO != null) {
List list;
if (doctor.getPatientList() != null) {
list = this.patientDTOListToPatientList(doctorDTO.getPatientDTOList());
if (list != null) {
doctor.getPatientList().clear();
doctor.getPatientList().addAll(list);
} else {
doctor.setPatientList((List)null);
}
} else {
list = this.patientDTOListToPatientList(doctorDTO.getPatientDTOList());
if (list != null) {
doctor.setPatientList(list);
}
}
doctor.setSpecialty(doctorDTO.getSpecialization());
doctor.setId(doctorDTO.getId());
doctor.setName(doctorDTO.getName());
}
}
protected Patient patientDTOToPatient(PatientDTO patientDTO) {
if (patientDTO == null) {
return null;
} else {
PatientBuilder patient = Patient.builder();
patient.id(patientDTO.getId());
patient.name(patientDTO.getName());
return patient.build();
}
}
protected List<Patient> patientDTOListToPatientList(List<PatientDTO> list) {
if (list == null) {
return null;
} else {
List<Patient> list1 = new ArrayList(list.size());
Iterator var3 = list.iterator();
while(var3.hasNext()) {
PatientDTO patientDTO = (PatientDTO)var3.next();
list1.add(this.patientDTOToPatient(patientDTO));
}
return list1;
}
}
}
1.7 数据类型映射
自动类型转换适用于:
基本类型及其对应的包装类之间。比如, 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字段:
package com.zs.entity.mapstruct;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PatientDTO {
private int id;
private String name;
private LocalDate dateOfBirth;
}
另一方面,加入 Patient 对象中有一个String 类型的 dateOfBirth :
package com.zs.entity.mapstruct;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Patient {
private int id;
private String name;
private String dateOfBirth;
}
在两者之间创建一个映射器:
package com.zs.entity.mapstruct.mapper;
@Mapper
public interface PatientMapper {
PatientMapper INSTANCE = Mappers.getMapper(PatientMapper.class);
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "yyyy/MM/dd")
PatientDTO toDTO(Patient patient);
}
当对日期进行转换时,我们也可以使用 dateFormat 设置格式声明。生成的实现代码形式大致如下:
package com.zs.entity.mapstruct.mapper;
public class PatientMapperImpl implements PatientMapper {
public PatientMapperImpl() {
}
public PatientDTO toDTO(Patient patient) {
if (patient == null) {
return null;
} else {
PatientDTOBuilder patientDTO = PatientDTO.builder();
if (patient.getDateOfBirth() != null) {
patientDTO.dateOfBirth(LocalDate.parse(patient.getDateOfBirth(), DateTimeFormatter.ofPattern("yyyy/MM/dd")));
}
patientDTO.id(patient.getId());
patientDTO.name(patient.getName());
return patientDTO.build();
}
}
}
测试:
@Test
public void test() {
Patient patient = new Patient(1001, "duanyunfei", "2001/12/11");
PatientDTO patientDTO = PatientMapper.INSTANCE.toDTO(patient);
System.out.println(patientDTO);
}
1.8 枚举映射
枚举映射的工作方式与字段映射相同。MapStruct会对具有相同名称的枚举进行映射,这一点没有问题。但是,对于具有不同名称的枚举项,我们需要使用@ValueMapping注解。同样,这与普通类型的@Mapping注解也相似。
我们先创建两个枚举:
package com.zs.entity.mapstruct.enumE;
public enum PaymentType {
CASH,
CHEQUE,
CARD_VISA,
CARD_MASTER,
CARD_CREDIT;
}
package com.zs.entity.mapstruct.enumE;
public enum PaymentTypeView {
CASH,
CHEQUE,
CARD;
}
现在,我们创建这两个enum之间的映射器接口:
package com.zs.entity.mapstruct.mapper;
@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。
package com.zs.entity.mapstruct.mapper;
import com.zs.entity.mapstruct.enumE.PaymentType;
import com.zs.entity.mapstruct.enumE.PaymentTypeView;
public class PaymentTypeMapperImpl implements PaymentTypeMapper {
public PaymentTypeMapperImpl() {
}
public PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType) {
if (paymentType == null) {
return null;
} else {
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;
}
}
}
测试:
package com.zs.entity.mapstruct;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Patient {
private int id;
private String name;
private String dateOfBirth;
private PaymentType paymentType;
}
package com.zs.entity.mapstruct;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PatientDTO {
private int id;
private String name;
private LocalDate dateOfBirth;
private PaymentTypeView paymentTypeView;
}
package com.zs.entity.mapstruct.mapper;
@Mapper(uses = {PaymentTypeMapper.class})
public interface PatientMapper {
PatientMapper INSTANCE = Mappers.getMapper(PatientMapper.class);
@Mappings({
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "yyyy/MM/dd"),
@Mapping(source = "paymentType", target = "paymentTypeView")
})
PatientDTO toDTO(Patient patient);
}
@Test
public void test() {
Patient patient = new Patient(1001, "Wuqiqi", "2001/11/01", PaymentType.CARD_CREDIT);
PatientDTO patientDTO = PatientMapper.INSTANCE.toDTO(patient);
System.out.println(patientDTO);
}
1.9 集合映射
1.9.1 List
package com.zs.entity.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);
List<DoctorDTO> mapConvert(List<Doctor> doctor);
Set<DoctorDTO> setConvert(Set<Doctor> doctor);
Map<String, DoctorDTO> mapConvert(Map<String, Doctor> doctor);
}
package com.zs.entity.mapstruct.mapper;
public class DoctorMapperImpl implements DoctorMapper {
public DoctorMapperImpl() {
}
public void updateModel(DoctorDTO doctorDTO, Doctor doctor) {
if (doctorDTO != null) {
List list;
if (doctor.getPatientList() != null) {
list = this.patientDTOListToPatientList(doctorDTO.getPatientDTOList());
if (list != null) {
doctor.getPatientList().clear();
doctor.getPatientList().addAll(list);
} else {
doctor.setPatientList((List)null);
}
} else {
list = this.patientDTOListToPatientList(doctorDTO.getPatientDTOList());
if (list != null) {
doctor.setPatientList(list);
}
}
doctor.setSpecialty(doctorDTO.getSpecialization());
doctor.setId(doctorDTO.getId());
doctor.setName(doctorDTO.getName());
}
}
public List<DoctorDTO> mapConvert(List<Doctor> doctor) {
if (doctor == null) {
return null;
} else {
List<DoctorDTO> list = new ArrayList(doctor.size());
Iterator var3 = doctor.iterator();
while(var3.hasNext()) {
Doctor doctor1 = (Doctor)var3.next();
list.add(this.doctorToDoctorDTO(doctor1));
}
return list;
}
}
public Set<DoctorDTO> setConvert(Set<Doctor> doctor) {
if (doctor == null) {
return null;
} else {
Set<DoctorDTO> set = new HashSet(Math.max((int)((float)doctor.size() / 0.75F) + 1, 16));
Iterator var3 = doctor.iterator();
while(var3.hasNext()) {
Doctor doctor1 = (Doctor)var3.next();
set.add(this.doctorToDoctorDTO(doctor1));
}
return set;
}
}
public Map<String, DoctorDTO> mapConvert(Map<String, Doctor> doctor) {
if (doctor == null) {
return null;
} else {
Map<String, DoctorDTO> map = new HashMap(Math.max((int)((float)doctor.size() / 0.75F) + 1, 16));
Iterator var3 = doctor.entrySet().iterator();
while(var3.hasNext()) {
Entry<String, Doctor> entry = (Entry)var3.next();
String key = (String)entry.getKey();
DoctorDTO value = this.doctorToDoctorDTO((Doctor)entry.getValue());
map.put(key, value);
}
return map;
}
}
protected Patient patientDTOToPatient(PatientDTO patientDTO) {
if (patientDTO == null) {
return null;
} else {
PatientBuilder patient = Patient.builder();
patient.id(patientDTO.getId());
patient.name(patientDTO.getName());
if (patientDTO.getDateOfBirth() != null) {
patient.dateOfBirth(DateTimeFormatter.ISO_LOCAL_DATE.format(patientDTO.getDateOfBirth()));
}
return patient.build();
}
}
protected List<Patient> patientDTOListToPatientList(List<PatientDTO> list) {
if (list == null) {
return null;
} else {
List<Patient> list1 = new ArrayList(list.size());
Iterator var3 = list.iterator();
while(var3.hasNext()) {
PatientDTO patientDTO = (PatientDTO)var3.next();
list1.add(this.patientDTOToPatient(patientDTO));
}
return list1;
}
}
protected DoctorDTO doctorToDoctorDTO(Doctor doctor) {
if (doctor == null) {
return null;
} else {
DoctorDTOBuilder doctorDTO = DoctorDTO.builder();
doctorDTO.id(doctor.getId());
doctorDTO.name(doctor.getName());
return doctorDTO.build();
}
}
}