Mapstruct笔记

Mapstruct笔记

官方文档:文档

参考博客:博客一博客二

简介

背景:业务场景下会涉及很多的实体模型,如 DO、DTO、PO、VO (理解) 等,不同的模型用于不同的场景,相同领域下的模型在业务代码编写中经常需要进行转换 ( Convert 转换类),由于属性很多,导致转换过程的编写通常很冗长,人工书写容易出错。

上述实体 POJO 介绍参见 文章,贴一张各个模型的场景转换图

image.png

mapstruct 就是为了解决此问题而产生,它是一个 Java 注解处理器,用于生成类型安全的模型转换。

使用时所需要做的就是定义一个模型转换器接口 (Mapper),该接口声明任何必需的转换方法,在编译期间,mapstruct 将生成此接口的实现,属性的转换只涉及 getter/setter 方法,通常与 Lombok 一起使用

编译生成类

简单使用

  1. 依赖引入,自己操作时遇到一些坑,注意插件的引入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    <properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>

    <mapstruct.version>1.4.2.Final</mapstruct.version>
    <lombok.version>1.18.20</lombok.version>
    </properties>

    <dependencies>
    <dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>${mapstruct.version}</version>
    </dependency>
    <dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>${mapstruct.version}</version>
    </dependency>
    <dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-jdk8</artifactId>
    <version>${mapstruct.version}</version>
    </dependency>

    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>${lombok.version}</version>
    </dependency>
    </dependencies>

    <build>
    <plugins>
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.1</version>
    <configuration>
    <source>1.8</source>
    <target>1.8</target>
    <annotationProcessorPaths>
    <path>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>${mapstruct.version}</version>
    </path>
    <path>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>${lombok.version}</version>
    </path>
    </annotationProcessorPaths>
    </configuration>
    </plugin>
    </plugins>
    </build>
  2. 定义同一领域的的各个模型,注意保证 无参构造方法 的存在

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    // DO
    @Data
    @Builder
    public class CoolBoy {
    private String name;
    private Integer age;
    private BigDecimal salary;

    /**
    * 类型
    */
    private Long boyType;
    /**
    * 存储格式:Mary,May,....
    */
    private String girlFriends;
    }

    // DTO
    @Data
    @Builder
    public class CoolBoyDTO {
    private String name;
    private Integer age;
    private BigDecimal salary;

    /**
    * 类型,
    */
    private BoyTypeEnum boyType;
    /**
    * 存储格式:Mary, May, ....
    */
    private List<String> girlFriends;
    }

    // VO
    @Data
    @Builder
    public class CoolBoyVO {
    private String name;
    private Integer age;
    private String salary;

    private String boyType;
    }
  3. 编写转换接口,(当然也可以说抽象类)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    @Mapper(
    // 选择注入方式 Spring
    // componentModel = "spring",
    // 选择空置转换策略,三种:1. 忽略, 2. 设置为空, 3. 设置为默认值
    // nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE
    )
    public interface CoolBoyConvert {
    CoolBoyConvert INSTANCE = Mappers.getMapper(CoolBoyConvert.class);

    @Mappings({
    // 使用方法做映射
    @Mapping(target = "boyType", expression = "java(BoyTypeEnum.byCode(coolBoy.getBoyType()))"),
    @Mapping(target = "girlFriends", expression = "java(string2ListString(coolBoy.getGirlFriends()))")

    })
    CoolBoyDTO coolBoy2CoolBoyDTO(CoolBoy coolBoy);

    @Mappings({
    // 指定转换数字格式
    @Mapping(source = "salary", target = "salary", numberFormat = "$#.00", defaultValue = "$0.00"),
    @Mapping(target = "boyType", expression = "java(coolBoyDTO.getBoyType().getLabel())")
    })
    CoolBoyVO coolBoyDTO2CoolBoyVO(CoolBoyDTO coolBoyDTO);

    // 指定映射关系
    @Mapping(target = "name", source = "name")
    // 忽略目标项
    @Mapping(target = "girlFriends", ignore = true)
    // 反转
    @InheritConfiguration(name = "coolBoyDTO2CoolBoyVO")
    CoolBoyDTO coolBoyVO2CoolBoyDTO(CoolBoyVO coolBoyVO);

    default List<String> string2ListString(String girlFriends) {
    return new ArrayList<>(Arrays.asList(girlFriends.split(",")));
    }
    }
  4. 测试类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    public class CoolBoyConvertTest {

    private final CoolBoyConvert convert = CoolBoyConvert.INSTANCE;

    @Test
    public void coolBoy2CoolBoyDTOTest() {
    CoolBoy coolBoy = CoolBoy.builder()
    .name("张三")
    .age(55)
    .salary(new BigDecimal(2000))
    .boyType(1L)
    .girlFriends("李四,王五")
    .build();

    CoolBoyDTO coolBoyDTO = convert.coolBoy2CoolBoyDTO(coolBoy);
    System.out.println(coolBoy);
    System.out.println(coolBoyDTO);
    }

    @Test
    public void coolBoyDTO2CoolBoyVOTest() {
    CoolBoyDTO coolBoyDTO = CoolBoyDTO.builder()
    .name("张三")
    .age(55)
    .salary(new BigDecimal(2000))
    .boyType(BoyTypeEnum.COOL)
    .girlFriends(Arrays.asList("李四", "王五"))
    .build();

    CoolBoyVO coolBoyVO = convert.coolBoyDTO2CoolBoyVO(coolBoyDTO);
    System.out.println(coolBoyDTO);
    System.out.println(coolBoyVO);
    }
    }

遇到一个很神奇的问题:

  • 前面使用普通 Maven 项目搭建不知道为什么爆 java: Unknown property "xxx" in result type xxx "null"?

  • 然后注释掉转换接口中自定映射注解 @Mapping ,转换出来的对象的所有属性均为 null

    1
    2
    CoolBoy(name=张三, age=55, salary=2000, boyType=1, girlFriends=李四, 王五)
    CoolBoyDTO(name=null, age=null, salary=null, boyType=null, girlFriends=null)
  • 把注释去掉,重新跑一下,又正常了

    1
    2
    CoolBoy(name=张三, age=55, salary=2000, boyType=1, girlFriends=李四, 王五)
    CoolBoyDTO(name=张三, age=55, salary=2000, boyType=COOL, girlFriends=null)
  • 很迷幻,不知道是不是自身配置问题。。。。

后续直接跑在 SpringBoot 项目时直接正常运行了。。。

后续的高级用法直接参考官方文档,真香了。。。。

近期使用场景的一些补充

要是用 Spring 容器中一些 Bean 的话,有两种方式

前提需将 componentModel 模式设置为 spring

1
2
3
4
5
@Mapper(
// 选择注入方式 Spring
componentModel = "spring",
// ......
)
  • 在保持原有为接口的状态下,使用工具类,并在 @Mapper 中使用 uses 属性引入类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // 工具类,注入Spring容器
    @Component
    public class TypeConversionWorker {
    @Resource
    protected XxxService xxxService;

    @Named("xxxMethod")
    protected Xxx xxxMethod(Xxx from) {
    return xxxService.xxxMethod(source);
    }
    }

    @Mapper(
    componentModel = "spring",
    // 引入工具类
    uses = TypeConversionWorker.class
    )
    public abstract class PermissionConvert {
    @Mappings({
    @Mapping(target = "fileds", source = "code"),
    // 使用指定方法进行属性转换
    @Mapping(target = "target", source = "source", qualifiedByName = "xxxMethod")
    })
    XxxDO PO2DO(XxxPO xxxPO);
    }
  • 升级为抽象类,将转换方法实现在抽象类中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Mapper(
    componentModel = "spring"
    )
    public abstract class PermissionConvert {
    @Mappings({
    @Mapping(target = "fileds", source = "code"),
    // 使用指定方法进行属性转换
    @Mapping(target = "target", source = "source", qualifiedByName = "xxxMethod")
    })
    protected abstract XxxDO PO2DO(XxxPO xxxPO);

    @Resource
    protected XxxService xxxService;

    @Named("xxxMethod")
    protected Xxx xxxMethod(Xxx from) {
    return xxxService.xxxMethod(source);
    }
    }