MapStruct插件
前言
MapStruct提供了一个功能强大的集成插件,可减少开发人员编写模板代码的工作量,使创建映射器的过程变得简单快捷。MapStruct是一个用于创建映射器类的库。从基本映射到自定义方法和自定义映射器,此外,关于MapStruct提供的一些高级操作选项,包括依赖注入,数据类型映射、枚举映射和表达式使用,在本文中也有讲述案例。
一、基本映射
1.基本使用
我们先从一些基本的映射开始。我们会创建一个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()方法,该方法接收一个Doctor实例为参数,并返回一个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代码往往比较长,为了简洁起见,这里省略了builder模式的实现代码。如果你的类中包含Builder,MapStruct会尝试使用它来创建实例;如果没有的话,MapStruct将通过new关键字进行实例化。
2.字段间映射
通常,模型和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);
}
3.多源类映射
有时,单个类不足以构建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属性中。
4.子对象映射
public class Patient {
private int id;
private String name;
// getters and setters or builder
}
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
}
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);
}
@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);
}
二、高级映射
1.集合映射策略
很多场景中,我们需要对具有父子关系的数据类型进行转换。通常来说,会有一个数据类型(父),其字段是另一个数据类型(子)的集合。对于这种情况,MapStruct提供了一种方法来选择如何将子类型设置或添加到父类型中。具体来说,就是@Mapper注解中的collectionMappingStrategy属性,该属性可以取值为ACCESSOR_ONLY, SETTER_PREFERRED, ADDER_PREFERRED 或TARGET_IMMUTABLE。
这些值分别表示不同的为子类型集合赋值的方式。默认值是ACCESSOR_ONLY,这意味着只能使用访问器来设置子集合。当父类型中的Collection字段setter方法不可用,但我们有一个子类型add方法时,这个选项就派上用场了;另一种有用的情况是父类型中的Collection字段是不可变的。
@Data
public class Hospital {
private List<Doctor> doctors;
}
同时定义一个映射目标DTO类,同时定义子类型集合字段的getter、setter
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);
}
可以看到,在默认情况下采用的策略是ACCESSOR_ONLY,使用setter方法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);
}
2.添加表达式
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
}
@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);
}
总结
我们对于mapstruct这种技术的出现是非常高兴的,解决了复杂数据层不同数据结构转化的难题。