2021-11-08

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这种技术的出现是非常高兴的,解决了复杂数据层不同数据结构转化的难题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值