Java数据对象映射库MapStruct介绍(2)

随着微服务和分布式应用的广泛采用,出于服务的独立性和数据的安全性方面的考虑,每个服务都会按照自己的需要定义业务数据对象,这样当服务相互调用时就要经常进行数据对象之间的映射。目前,有很多实现数据对象映射的库,本文介绍一种高性能的映射库MapStruct

https://mapstruct.org/documentation/stable/reference/html/

Java数据对象映射库MapStruct介绍(1)

MapStruct简介

MapStruct是在编译时根据定义(接口)生成映射类(实现),自动生成需要手工编写数据映射代码,通过直接调用复制数据,不需要通过反射,因此速度非常快。

本文将介绍一些MapStruct的高级功能,包括:

  • 更新已经存在的实例
  • 添加自定义映射方法
  • 创建自定义映射器
  • 异常处理
  • 复用映射关系(继承,反向继承)

更新已经存在的实例

有时候,我们想用源数据对象更新已经存在的数据对象,而不是新建对象,这时需要用@MappingTarget指定目标对象。

DoctorMapper中添加新的映射方法。

@Mapper(componentModel = "spring", imports = { LocalDateTime.class, UUID.class })
public interface DoctorMapper {
  /*添加了新的映射方法,用@MappingTarget更新目标对象*/
  @Mapping(source = "doctorDto.patientDtoList", target = "patientList")
  @Mapping(source = "doctorDto.specialization", target = "specialty")
  void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor); // @MappingTarget指定了要更新的数据对象
}

生成的代码将目标对象作为参数传入映射方法。

  public void updateModel(DoctorDto doctorDto, Doctor doctor) {
    if (doctorDto == null)
      return; 
    if (doctor.getPatientList() != null) {
      List list = patientDtoListToPatientList(doctorDto.getPatientDtoList());
      if (list != null) {
        doctor.getPatientList().clear();
        doctor.getPatientList().addAll(list);
      } else {
        doctor.setPatientList(null);
      } 
    } else {
      List list = patientDtoListToPatientList(doctorDto.getPatientDtoList());
      if (list != null)
        doctor.setPatientList(list); 
    } 
    doctor.setSpecialty(doctorDto.getSpecialization());
    doctor.setAvailability(doctorDto.getAvailability());
    doctor.setExternalId(doctorDto.getExternalId());
    doctor.setId(doctorDto.getId());
    doctor.setName(doctorDto.getName());
  }

注意:patientList也进行了更新。

添加自定义映射方法

之前我们是添加接口方法让MapStruct自动生成实现代码,也可以用default关键字在接口中添加手工编写的方法。

@Data
public class DoctorPatientSummary {
  private int doctorId;
  private int patientCount;
  private String doctorName;
  private String specialization;
  private String institute;
  private List patientIds;
}

DoctorMapper中添加自定义的映射方法。

@Mapper(componentModel = "spring", imports = { LocalDateTime.class, UUID.class })
public interface DoctorMapper {
  /*省略了之前定义的映射函数*/

  /*手工指定映射关系*/
  default DoctorPatientSummary toDoctorPatientSummary(Doctor doctor, Education education) {
    DoctorPatientSummary summary = new DoctorPatientSummary();
    summary.setDoctorId(doctor.getId());
    summary.setDoctorName(doctor.getName());
    summary.setPatientCount(doctor.getPatientList().size());
    summary.setPatientIds(doctor.getPatientList().stream().map(Patient::getId).collect(Collectors.toList()));
    summary.setInstitute(education.getInstitute());
    summary.setSpecialization(education.getDegreeName());

    return summary;
  }
}

MapStruct自动生成的实现类实现了映射接口,因此可以在实现类的实例中直接调用该方法。

创建自定义映射器

除了通过接口定义映射,还可以用抽象类(abstract)实现,MapStruct也会自动生成实现类。

@Mapper(componentModel = "spring")
public abstract class DoctorCustomMapper {

  public DoctorPatientSummary toDoctorPatientSummary(Doctor doctor, Education education) {
    DoctorPatientSummary summary = new DoctorPatientSummary();
    summary.setDoctorId(doctor.getId());
    summary.setDoctorName(doctor.getName());
    summary.setPatientCount(doctor.getPatientList().size());
    summary.setPatientIds(doctor.getPatientList().stream().map(Patient::getId).collect(Collectors.toList()));
    summary.setInstitute(education.getInstitute());
    summary.setSpecialization(education.getDegreeName());

    return summary;
  }
}

自动生成的实现类。

@Component
public class DoctorCustomMapperImpl extends DoctorCustomMapper {}

通过抽象类实现的映射类还可以用@BeforeMapping@AfterMapping添加映射前和映射后执行的方法。

@Mapper(componentModel = "spring")
public abstract class DoctorCustomMapper {

  @BeforeMapping
  protected void validate(Doctor doctor) {
    if (doctor.getPatientList() == null) {
      doctor.setPatientList(new ArrayList<>());
    }
  }

  @AfterMapping
  protected void updateResult(@MappingTarget DoctorDto doctorDto) {
    doctorDto.setName(doctorDto.getName().toUpperCase());
    doctorDto.setDegree(doctorDto.getDegree().toUpperCase());
    doctorDto.setSpecialization(doctorDto.getSpecialization().toUpperCase());
  }

  /*抽象方法,由MapStruct生成实现代码*/
  @Mapping(source = "doctor.patientList", target = "patientDtoList")
  @Mapping(source = "doctor.specialty", target = "specialization")
  public abstract DoctorDto toDoctorDto(Doctor doctor);

  /*省略其它定义*/
}
@Component
public class DoctorCustomMapperImpl extends DoctorCustomMapper {
  public DoctorDto toDoctorDto(Doctor doctor) {
    validate(doctor); // 调用了@BeforeMapping注解的方法
    if (doctor == null)
      return null; 
    DoctorDto doctorDto = new DoctorDto();
    doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor.getPatientList()));
    doctorDto.setSpecialization(doctor.getSpecialty());
    doctorDto.setAvailability(doctor.getAvailability());
    doctorDto.setExternalId(doctor.getExternalId());
    doctorDto.setId(doctor.getId());
    doctorDto.setName(doctor.getName());
    updateResult(doctorDto);
    return doctorDto; // 调用了@AfterMapping注解的方法
  }
 /*省略了生成的其它代码*/
}

异常处理

数据对象转换过程中难免会发生异常,MapStruct也支持进行异常处理。

假设我们有一个进行数据检查的类Validator,检查失败会抛出自定义异常ValidationException

public class Validator {
  public int validateId(int id) throws ValidationException {
    if (id == -1) {
      throw new ValidationException("Invalid value in ID");
    }
    return id;
  }
}

在定义的映射方法上添加抛出异常。

@Mapper(componentModel = "spring", uses = { PatientMapper.class, Validator.class }, imports = { LocalDateTime.class, UUID.class })
public interface DoctorMapper {

  /*省略自动生成的内容*/
  DoctorDto toDto(Doctor doctor, Education education) throws ValidationException; // 方法抛出异常

  /*省略自动生成的内容*/
}
@Component
public class DoctorMapperImpl implements DoctorMapper {
  @Autowired
  private PatientMapper patientMapper;
  
  @Autowired
  private Validator validator;
  
  public DoctorDto toDto(Doctor doctor, Education education) throws ValidationException {
    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.setId(this.validator.validateId(doctor.getId())); // 这里会检查id的合法性,不合法抛异常
      doctorDto.setName(doctor.getName());
    } 
    if (education != null)
      doctorDto.setDegree(education.getDegreeName()); 
    doctorDto.setExternalId(UUID.randomUUID().toString());
    return doctorDto;
  }
  

复用映射关系

有时候多个映射方法使用相同的映射关系,例如:生成新数据对象或更新现有数据对象的方法。

@Mapper(uses = { PatientMapper.class }, componentModel = "spring")
public interface DoctorInheritMapper {
  @Mapping(source = "doctorDto.specialization", target = "specialty")
  @Mapping(source = "doctorDto.patientDtoList", target = "patientList")
  Doctor toModel(DoctorDto doctorDto);

  @InheritConfiguration
  void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor);
}
@Component
public class DoctorInheritMapperImpl implements DoctorInheritMapper {
  public Doctor toModel(DoctorDto doctorDto) {
    if (doctorDto == null)
      return null; 
    Doctor doctor = new Doctor();
    doctor.setSpecialty(doctorDto.getSpecialization());
    doctor.setPatientList(patientDtoListToPatientList(doctorDto.getPatientDtoList()));
    doctor.setAvailability(doctorDto.getAvailability());
    doctor.setExternalId(doctorDto.getExternalId());
    doctor.setId(doctorDto.getId());
    doctor.setName(doctorDto.getName());
    return doctor;
  }
  /*这里按照继承的映射关系生成了代码*/
  public void updateModel(DoctorDto doctorDto, Doctor doctor) {
    if (doctorDto == null)
      return; 
    doctor.setSpecialty(doctorDto.getSpecialization());
    if (doctor.getPatientList() != null) {
      List list = patientDtoListToPatientList(doctorDto.getPatientDtoList());
      if (list != null) {
        doctor.getPatientList().clear();
        doctor.getPatientList().addAll(list);
      } else {
        doctor.setPatientList(null);
      } 
    } else {
      List list = patientDtoListToPatientList(doctorDto.getPatientDtoList());
      if (list != null)
        doctor.setPatientList(list); 
    } 
    doctor.setAvailability(doctorDto.getAvailability());
    doctor.setExternalId(doctorDto.getExternalId());
    doctor.setId(doctorDto.getId());
    doctor.setName(doctorDto.getName());
  }
  /*省略生成的其他代码*/
}

还有一种用到继承的情况是需要按照一套规则进行双向的数据对象转换。

@Mapper(componentModel = "spring")
public interface PatientMapper {
  @Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
  PatientDto toDto(Patient patient);

  @InheritInverseConfiguration
  Patient toModel(PatientDto patientDto);
}
@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;
  }
  
  /*根据继承的反向规则生成的代码*/
  public Patient toModel(PatientDto patientDto) {
    if (patientDto == null)
      return null; 
    Patient patient = new Patient();
    if (patientDto.getDateOfBirth() != null)
      patient.setDateOfBirth(DateTimeFormatter.ofPattern("dd/MMM/yyyy").format(patientDto.getDateOfBirth())); 
    patient.setId(patientDto.getId());
    patient.setName(patientDto.getName());
    return patient;
  }
}

总结

MapStruct是一个非常强大的数据对象映射库,除了减少手工代码的编写量,还为我们提供了一个进行数据对象映射的基础框架。MapStruct还有很多用法,需要不断研究和实践。

你可能感兴趣的:(Java数据对象映射库MapStruct介绍(2))