美文网首页编程实践
Java数据对象映射库MapStruct介绍(1)

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

作者: 全栈顾问 | 来源:发表于2021-04-06 13:52 被阅读0次

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

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

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

    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>
    

    定义字段映射关系

    基本定义

    假设我们有两个数据对象DoctorDoctorDto,它们有相同的字段。注意:两个数据对象都添加了@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支持基本类型的自动类型转换,包括:

    • 原始类型和相应的包裹类型间的转换,例如:intIntegerfloatFloatlongLongbooleanBoolean
    • 原始类型和包裹类型间的相互转换,例如:intlongbyteInteger等。
    • 原始类型和包裹类型与String类型之间的转换,booleanStringIntegerStringfloatString等。
    @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提供两种方式设置目标数据对象字段默认值,constantdefaultconstant是不论源数据对象字段是什么值都将目标数据对象字段设置为对应的值,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表达式

    除了使用constantdefault设置目标值,还可以用expressiondefaultExpressionexpression是忽略源数据对象的值进行设置,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的基础功能,下一篇介绍它的一些高级功能。

    相关文章

      网友评论

        本文标题:Java数据对象映射库MapStruct介绍(1)

        本文链接:https://www.haomeiwen.com/subject/iwvdkltx.html