mapstruct学习及使用详解

  • Post author:
  • Post category:其他


注:

  • 没有提供对应的对象,自己实现,提高认识
  • 学习方式:最好是对class进行一个反编译,看看他生成的代码。如果发现一些类型没设置成功也可以通过反编译查看。
  • 反编译工具:

    java-decompiler
<!--导入的版本,参考官网-->
<dependency>
  <groupId>org.mapstruct</groupId>
  <artifactId>mapstruct</artifactId>
  <version>${org.mapstruct.version}</version>
</dependency>
<dependency>
  <groupId>org.mapstruct</groupId>
  <artifactId>mapstruct-processor</artifactId>
  <version>${org.mapstruct.version}</version>
</dependency>



映射器定义

// 测试方法,后续都是这样测试
@Test
void test() {
  BasicMapper instance = BasicMapper.INSTANCE;
	BasicUserDTO convert = instance.convert(user);
}



基本映射

  • 如果两个字段名称相同会自动映射。
  • 如果两个字段名称不相同则需要使用

    @Mapping

    进行映射,参考 – 从多个源对象映射
@Mapper
public interface BasicMapper {
  // 使用入口
  BasicMapper INSTANCE = Mappers.getMapper(BasicMapper.class);
  BasicUserDTO convert(BasicUser user);
}



自定义映射方法

// 接口方式
@Mapper
public interface BasicMapper {
  BasicMapper INSTANCE = Mappers.getMapper(BasicMapper.class);
  BasicUserDTO convert(BasicUser user);
  default PersonDTO convertCustom(BasicUser user) {
    return PersonDTO
             .builder()
             .id(String.valueOf(user.getId()))
             .firstName(user.getName().substring(0, user.getName().indexOf(" ")))
             .lastName(user.getName().substring(user.getName().indexOf(" ") + 1))
             .build();
  }
}

// 抽象类方式,好处:可以直接在映射器类中声明附加字段
@Mapper
public abstract class BasicMapper {

  public abstract BasicUserDTO convert(BasicUser user);

  public PersonDTO convertCustom(BasicUser user) {
    return PersonDTO
             .builder()
             .id(String.valueOf(user.getId()))
             .firstName(user.getName().substring(0, user.getName().indexOf(" ")))
             .lastName(user.getName().substring(user.getName().indexOf(" ") + 1))
             .build();
  }
}



从多个源对象映射

  • 当入参和返回的参数不匹配时,或者 有多个入参对象时,可以通过该方式指定需要映射到哪个字段
  • source:传入进来的参数
  • target:返回的参数
@Mapping(source = "user.id", target = "id")
@Mapping(source = "user.name", target = "firstName")
@Mapping(source = "education.degreeName", target = "educationalQualification")
@Mapping(source = "address.city", target = "residentialCity")
@Mapping(source = "address.country", target = "residentialCountry")
PersonDTO convert(BasicUser user, Education education, Address address);



映射嵌套对象

  • 当有嵌套对象时,可以采用@Mapper的uses指定一个嵌套对象对应的映射类

    @Data
    @Builder
    @ToString
    public class BasicUser {
      private int id;
      private String name;
      // 嵌套对象
      private List<Manager> managerList;
    }
    
    // 指定一个嵌套对象的映射类
    @Mapper(uses = {ManagerMapper.class})
    public interface UserMapper {
      UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
      
      @Mapping(source = "user.id", target = "id")
      @Mapping(source = "user.name", target = "firstName")
      @Mapping(source = "education.degreeName", target = "educationalQualification")
      @Mapping(source = "address.city", target = "residentialCity")
      @Mapping(source = "address.country", target = "residentialCountry")
      PersonDTO convert(BasicUser user, Education education, Address address);
    }
    
  • 在生成方法时,会通过指定的嵌套类对这个嵌套对象进行映射,如下所示

    // 生成了一个managerListToManagerDTOList对嵌套类进行解析
    



更新现有实例

  • 使用映射更新现有的 DTO
  • 对需要更新的映射添加一个

    @MappingTarget

    注解,就会对其进行更新
@Mapping(source = "address.city", target = "residentialCity")
@Mapping(source = "address.country", target = "residentialCountry")
void updateExisting(Address address, @MappingTarget PersonDTO personDTO);
// 生成了一个DTO
PersonDTO personDTO = UserMapper.INSTANCE.convert(address);
// 对这个personDTO进行了更新
UserMapper.INSTANCE.updateExisting(address,personDTO);



继承配置

  • 对于重复的配置,使用

    @InheritConfiguration

    ,MapStruct 会查找已配置的方法,并且进行应用
@Mapper
public interface ManagerMapper {
  ManagerMapper INSTANCE = Mappers.getMapper(ManagerMapper.class);
  // 故意吧这两个顺序弄反,在测试看是否生效
  @Mapping(source = "address.city", target = "residentialCountry")
	@Mapping(source = "address.country", target = "residentialCity")
  ManagerDTO convert(Manager manager);

  @InheritConfiguration
  void updateExisting(Manager manager, @MappingTarget ManagerDTO managerDTO);
}



逆映射

  • 想定义一个双向映射

    如:

    • Entity 映射 DTO

    • DTO 映射 Entity

  • 使用

    @InheritInverseConfiguration

    会自动的反转配置

    @Mapper
    public interface UserMapper {
      UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
      // mapping的配置效果,也会被下面的反转映射所使用
      BasicUserDTO convert(BasicUser user);
    
      @InheritInverseConfiguration // 反转映射
      BasicUser convert(BasicUserDTO userDTO);
    }
    



映射期间的异常处理

  • 自定义校检规则,映射期间如果发现跟校检的要求不匹配,则抛出异常(自定义)

  • 步骤:

    • 自定义异常

      public class ValidationException extends RuntimeException {
      
        public ValidationException(String message, Throwable cause) {
          super(message, cause);
        }
      
        public ValidationException(String message) {
          super(message);
        }
      }
      
    • 自定义校检规则

      • 方法名要求:validate字段名(字段类型)

      • 注意事项:校检的字段类型要和形参以及异常进行匹配,否则的话匹配不到则不会生效

      public class Validator {
        public int validateId(int id) throws ValidationException {
          if(id < 0){
            throw new ValidationException("Invalid ID value");
          }
          return id;
        }
      }
      
    • 使用

      // 导入校检规则
      @Mapper(uses = { Validator.class})
      public interface UserMapper {
        UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
        // 抛出对应的异常
        BasicUserDTO convert(BasicUser user) throws ValidationException;
      
      }
      



数据类型转换



隐式类型转换

  • 数值

    @Mapping(source = "employment.salary",
             target = "salary",
             numberFormat = "$#.00") 
    PersonDTO convert(BasicUser user,
                      Education education,
                      Address address,
                      Employment employment);
    // 会自动转换,如下:
    personDTO.setSalary( new DecimalFormat( "$#.00" ).format(
                    employment.getSalary() ) );
    
  • 日期

    @Mapping(source = "dateOfBirth",
             target = "dateOfBirth",
             dateFormat = "dd/MMM/yyyy") 
    ManagerDTO convert(Manager manager);
    
    // 会自动转换,如下:
    managerDTO.setDateOfBirth(
        new SimpleDateFormat( "dd/MMM/yyyy" )
        .parse( manager.getDateOfBirth() ) );
    
    // 如果没自定义转换, 则生成如下:
    managerDTO.setDateOfBirth( new SimpleDateFormat().parse(
        manager.getDateOfBirth() ) );
    



映射集合

  • 通过循环遍历,进行映射。

  • 如果使用了@Mapping的uses则会自动调用此对应的映射方法来执行元素转换。

  • 简单使用

    @Mapper
    public interface CollectionMapper {
      CollectionMapper INSTANCE = Mappers.getMapper(CollectionMapper.class);
    
      Set<String> convert(Set<Long> ids);
      
      Set<EmploymentDTO> convertEmployment(Set<Employment> employmentSet);
    }
    
  • 需要对实体进行自定义映射,则需要先定义实体之间的转换方法。

    @Mapper
    public interface CollectionMapper {
      CollectionMapper INSTANCE = Mappers.getMapper(CollectionMapper.class);
    	
      // 自定义映射
      @Mapping(source = "degreeName", target = "degree")
      @Mapping(source = "institute", target = "college")
      @Mapping(source = "yearOfPassing", target = "passingYear")
      EducationDTO convert(Education education);
      // 会去匹配自定义映射进行转换
      List<EducationDTO> convert(List<Education> educationList);
    }
    
    // 会生成如下代码:
    educationDTO.degree( education.getDegreeName() );
    educationDTO.college( education.getInstitute() );
    educationDTO.passingYear( education.getYearOfPassing() );
    
  • 对map进行映射

    • 可以通过 keyNumberFormat 和 valueDateFormat 对转入的键值做一个转换
    @Mapper
    public interface CollectionMapper {
      CollectionMapper INSTANCE = Mappers.getMapper(CollectionMapper.class);
    
      @MapMapping(keyNumberFormat = "#L", valueDateFormat = "dd.MM.yyyy")
      Map<String, String> map(Map<Long, Date> dateMap);
    }
    
    // 生成如下代码:
    String key = new DecimalFormat( "#L" ).format( entry.getKey() );
    String value = new SimpleDateFormat( "dd.MM.yyyy" ).format( entry.getValue() );
    



映射策略

  • 默认值为

    ACCESSOR_ONLY

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s4eI45Pl-1660918092350)(images/mapstruct – 集合映射策略.png)]

// 使用ADDER_PREFERRED策略
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
public interface PersonMapperAdderPreferred {
  PersonDTO map(Person person);
}



映射流

  • 和映射集合相同,只是

    Stream

    会从提供的返回

    Iterable

@Mapper
public interface CollectionMapper {
  CollectionMapper INSTANCE = Mappers.getMapper(CollectionMapper.class);

  Set<String> convertStream(Stream<Long> ids);

  @Mapping(source = "degreeName", target = "degree")
  @Mapping(source = "institute", target = "college")
  @Mapping(source = "yearOfPassing", target = "passingYear")
  EducationDTO convert(Education education);
  List<EducationDTO> convert(Stream<Education> educationStream);
}

// 生成如下:
return ids.map( long1 -> String.valueOf( long1 ) )
  .collect( Collectors.toCollection( HashSet<String>::new ) );



映射枚举

  • 名字相同,则直接映射即可

  • 名字不相同,使用

    @ValueMapping

    进行映射

    无法识别源值,抛出 IllegalStateException。

    public enum DesignationCode {CEO}
    public enum DesignationConstant {CHIEF_EXECUTIVE_OFFICER}
    
    @Mapper
    public interface UserMapper {    
      UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
      
      @ValueMappings({
              @ValueMapping(source = "CEO", target = "CHIEF_EXECUTIVE_OFFICER"),
      })
      DesignationConstant convertDesignation(DesignationCode code);
    }    
    
  • 如果有前缀,则使用如下4个属性进行映射


    • suffix

      – 在源枚举上应用后缀

    • stripSuffix

      – 从源枚举中去除后缀

    • prefix

      – 在源枚举上应用前缀

    • stripPrefix

      – 从源枚举中去除前缀
    public enum DegreeStream {MATHS}
    public enum DegreeStreamPrefix {MSC_MATHS}
    
    
    @Mapper
    public interface UserMapper {
      UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
            
      @EnumMapping(nameTransformationStrategy = "prefix", configuration = "MSC_")
      DegreeStreamPrefix convert(DegreeStream degreeStream);
    
      @EnumMapping(nameTransformationStrategy = "stripPrefix", configuration = "MSC_")
      DegreeStream convert(DegreeStreamPrefix degreeStreamPrefix);
    }
    



定义默认值或常量

  • defaultValue:当值不存在时,则使用默认值
  • constant:映射到目标枚举类型中具有相同名称的常量
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
        uses = {CollectionMapper.class, ManagerMapper.class, Validator.class},
        imports = UUID.class )
public interface UserMapper {
  UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);

  @Mapping(source = "education.yearOfPassing", target = "education.passingYear",
           defaultValue = "2001")
  @Mapping(source = "employment", target = ".")
  @Mapping(target = "residentialCountry", constant = "US")
  PersonDTO convert(BasicUser user,
                    Education education,
                    Address address,
                    Employment employment);
}    



定义默认表达式

  • 用于使用java表达式
  • 在源属性为

    null

    时使用,才会触发。
  • 还需要导入对应的类
@Mapper( imports = UUID.class )
public interface UserMapper {
  UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);

  @Mapping(source = "user.id", target = "id",
           defaultExpression = "java( UUID.randomUUID().toString() )")
  PersonDTO convert(BasicUser user,
                    Education education,
                    Address address,
                    Employment employment);
}



映射器检索策略

  • 不使用依赖注入框架,使用

    Mappers

    获取映射器实例

    @Mapper
    public interface UserMapper {
      UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
    }
    // 使用
    PersonDTO personDTO = UserMapper.INSTANCE.convert(user);
    
  • 使用

    @componentModel

    注解导入依赖注入

    • 支持

      CDI

      和Spring框架
    @Mapper(componentModel = "spring")
    public interface UserMapper {}
    
    // 生成如下:
    @Component
    public class UserMapperImpl implements UserMapper {}
    
    // 使用
    @Controller
    public class UserController() {
      @Autowired
      private UserMapper userMapper;
    }
    



映射定制

  • 使用装饰器模式,进行自定义
  • 使用

    @BeforeMapping

    /

    @AfterMapping

    ,进行通用的设置



装饰器

  • 定义一个Decorator类,在使用

    @DecoratedWith

    使其生效
  • 对需要自定义映射的方法进行实现,其他的方法用默认实现生成对原始映射器的委托。
public abstract class UserMapperDecorator implements UserMapper {

  private final UserMapper delegate;

  protected UserMapperDecorator (UserMapper delegate) {
      this.delegate = delegate;
  }

  @Override
  public PersonDTO convert(BasicUser user,
                           Education education,
                           Address address,
                           Employment employment) {
    // 委托
    PersonDTO dto = delegate.convert(user, education, address, employment);
    if (user.getName().split("\\w+").length > 1) {
       dto.setFirstName(user.getName().substring(0, user.getName().lastIndexOf(' ')));
       dto.setLastName(user.getName().substring(user.getName().lastIndexOf(" ") + 1));
     }
     else {
        dto.setFirstName(user.getName());
     }
     return dto;
  }
}

// 使用
@Mapper
@DecoratedWith(UserMapperDecorator.class)
public interface UserMapper {
  UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
    
  PersonDTO convert(BasicUser user, Education education, Address address, Employment employment);
}



@BeforeMapping和@AfterMapping


  • @BeforeMapping

    用于执行前,运行指定的逻辑

  • @AfterMapping

    用于执行后,运行指定的逻辑
@Mapper
@DecoratedWith(UserMapperDecorator.class)
public interface UserMapper {
  UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);

  // 执行前, 如果manager为null则设置一个空集合
  @BeforeMapping
  default void validateMangers(BasicUser user) {
    if (Objects.isNull(user.getManagerList())) {
       user.setManagerList(new ArrayList<>());
    }
  }

  @Mapping(target = "residentialCountry", constant = "US")
  void updateExisting( Address address );

  // 执行后,对firstName和LastName进行一个字符转换
  @AfterMapping
  default void updateResult(BasicUser user, 
                            @MappingTarget PersonDTO personDTO) {
      personDTO.setFirstName(personDTO.getFirstName().toUpperCase());
      personDTO.setLastName(personDTO.getLastName().toUpperCase());
  }
}



参考文献



版权声明:本文为qq_42191033原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。