0%

Middleware DTO映射 MapStruct

MapStruct 是一个代码生成器,它基于约定优于配置方法极大地简化了 Java bean 类型之间映射的实现。自动生成的映射转换代码只使用简单的方法调用,因此速度快、类型安全而且易于理解阅读,源码仓库 Github 地址 MapStruct。总的来说,有如下三个特点:

  1. 基于注解
  2. 在编译期自动生成映射转换代码
  3. 类型安全、高性能、无依赖性

MapStruct 用法

S/T 字段及类型一致

在 Source 和 Target 需要映射字段的字段名和类型一致的场景下,只需写如下的 interface 方法 / abstract 方法即可。

import org.mapstruct.Mapper;  
import org.mapstruct.factory.Mappers;

@Mapper  
public abstract class Converter {  
  
    public static Converter INSTANT = Mappers.getMapper(Converter.class);  
  
    public abstract Target convert(Source source);  
}

S/T 字段类型不一致

在 Source 和 Target 需要映射的字段名称或类型不同时,可使用 @Mapping 注解指定映射的 Target 名称及类型。如下,Source 的 Integer 类型的 id,被映射到 Target 中 String 类型的 no。

import org.mapstruct.Mapper;  
import org.mapstruct.factory.Mappers;

@Mapper  
public abstract class Converter {  
  
    public static Converter INSTANT = Mappers.getMapper(Converter.class);  
    
    @Mapping(source = "id", target = "no", resultType = String.class)
    public abstract Target convert(Source source);  
}

S/T 一对多字段转换

在一些特殊场景下,一个字段中可能包含多个待映射字段的信息。如下,extra 字段包含 id 和 name 两个字段的信息。可以通过定义解析函数的方式读取完成转换。

import org.mapstruct.Mapper;  
import org.mapstruct.Mapping;  
import org.mapstruct.Named;  
import org.mapstruct.factory.Mappers;  
  
@Mapper  
public abstract class Converter {  
  
    public static Converter INSTANT = Mappers.getMapper(Converter.class);  

    @Mapping(source = "extra", target = "id", qualifiedByName = "extraId")
    @Mapping(source = "extra", target = "name", qualifiedByName = "extraName")  
    public abstract Source convert(Target target);  

    @Named("extraId")  
    public String extractId(String extra) {  
        return extra.split(",")[0];  
    }
    
    @Named("extraName")  
    public String extractName(String extra) {  
        return extra.split(",")[1];  
    }
}

S/T 多对一字段转换

Target 增加 String 类型的 extra 字段,可以使用自定义转换方法转换,将多个字段内容融合成一个字段。

import org.mapstruct.Mapper;  
import org.mapstruct.Mapping;  
import org.mapstruct.Named;  
import org.mapstruct.factory.Mappers;

@Mapper  
public abstract class Converter {  
  
    public static Converter INSTANT = Mappers.getMapper(Converter.class);  
  
    @Mapping(source = "source", target = "extra", qualifiedByName = "convertToExtra")  
    public abstract Target convert(Source source);  
  
    @Named("convertToExtra")  
    public String convertToExtra(Source source) {  
        return String.format("%s,%s", source.getId(), source.getName());  
    }
}

表达式转换

根据内嵌代码执行转换

@Mapping(target = "name", expression = "java(programer.getName().toUpperCase())")

日期转换

将日期类型转换为指定格式字符串。

@Mapping(target = "beDate", dateFormat = "yyyy-MM-dd HH:mm:ss")

数值转换

将数值类型转换为指定格式字符串。

@Mapping(target = "height", source = "height" ,numberFormat = "#.00")

映射切面

MapStruct 还提供了两个注解 @BeforeMapping@AfterMapping 用来实现在 mapping 前后的统一操作,常用于操作多态。

实例:Human 父类,有 Man 和 Woman 两个子类,然后我们要将这两个子类型 mapping 成 HumanDto。HumanDto 中有个性别的属性,需要根据具体的类型决定。在 mapping 完成后,我们还要将名称修饰一下。

public class Human {
    private String name;
}
public class Man extends Human{
}
public class Woman extends Human{
}

public class HumanDto {
    private String name;
    private GenderType genderType;
}
public enum GenderType {
    MAN, WOMAN
}

当然我们可以写两个转换方法,一个 Man 到 HumanDto,一个 Woman 到 HumanDto,但是在使用的时候就比较麻烦了,需要传入具体的类型,代码也有重复。这种场景下我们就可以使用这两个注解完美的解决问题。

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public abstract class HumanConvertor {

    @BeforeMapping
    protected void humanDtoWithGender(Human human, @MappingTarget HumanDto humanDto) {
        if (human instanceof Man) {
            humanDto.setGenderType(GenderType.MAN);
        } else if (human instanceof Woman) {
            humanDto.setGenderType(GenderType.WOMAN);
        }
    }

    @AfterMapping
    protected void decorateName(@MappingTarget HumanDto humanDto) {
        humanDto.setName(String.format("【%s】", humanDto.getName()));
    }

    public abstract HumanDto toHumanDto(Human human);
}

更新对象属性

MapStruct 提供了另外一种方式来更新一个对象中的属性。

@Mappings({                 
	@Mapping(source = "id", target = "userId"), 
	@Mapping(source = "username", target = "name"),  
	@Mapping(source = "role.roleName", target = "roleName")         
})         
void update(User user, @MappingTarget UserRoleDto userRoleDto);

通过 @MappingTarget 来指定目标类(谁的属性需要被更新)。@Mapping 还是用来定义属性对应规则。

自定义类型转换

定义转换器

public class BoolStrFormat {  
    public String toStr(Boolean isDisable) {  
        if (isDisable) {  
            return "Y";  
        } else {  
            return "N";  
        }    }  
    public Boolean toBool(String str) {  
        if ("Y".equals(str)) {  
            return true;  
        } else {  
            return false;  
        }    
	}
}

定义映射器

@Mapper(uses = {BoolStrFormat.class})  
public abstract class Converter {  
  
    public static Converter INSTANT = Mappers.getMapper(Converter.class);  
  
    public abstract Target convert(Source source);  
}

这样转换时,Boolean 类型会通过 BoolStrFormat 被转换成特定字符串。

为转换加缓存

使用 ThreadLocal 缓存 extra 解析结果。

其他特性

多数据源

多数据源场景下,需显示指定参数名称

@Mapping(target = "boyName", source = "boy.name")
@Mapping(target = "girlName", source = "girl.name")
Couple toCouple(Boy boy, Girl girl);

注入外部依赖

@Mapper 注解有 use 和 import 两种注入方式。import 指通过生成 @Autowire 注解注入指定的类去使用。use 指通过 new 指定对象的实例进行使用。

忽略映射

@Mapping(target = "name", ignore = true)

设置默认值

source 值为 null 时给一个默认值。

@Mapping(target = "name", defaultValue = "Amy")

设置常量

直接给 target 值设置一个常量,而不是从 source 进行转换。

@Mapping(target = "name", constant = "Sam")