抱歉,您的瀏覽器無法訪問本站
本頁面需要瀏覽器支持(啟用)JavaScript
了解詳情 >

banner

前言

也不知道说啥,那就直入主题吧~

什么是MapStruct?

介绍

相信大家,已经在各种地方都听过MapStruct这个框架了,知道它是一个类似于BeanUtils的拷贝框架,那么下面让我们来看下官方的介绍

MapStruct is a Java annotation processor for the generation of type-safe and performant mappers for Java bean classes. It saves you from writing mapping code by hand, which is a tedious and error-prone task. The generator comes with sensible defaults and many built-in type conversions, but it steps out of your way when it comes to configuring or implementing special behavior.

翻译一下就是:MapStruct是一个基于Java编译期注解处理器来实现,用来高性能、类型安全的生成Java bean的映射器。可以避免我们手动写Getter/Getter等映射代码。

在上面的介绍中可以发现一个词Java annotation processor,翻译成中文就是Java编译期注解处理器,那么什么是Java编译期注解处理器呢?

Java编译处理器(APT)

Java编译时期处理器可以简称为APT,它是Javac的工具,最早出现在JDK5的版本中,随后在之后的版本不断新增相关API,开始流行起来。

通过APT可以拿到注解和被注解对象的相关信息,随后通过自身需求来自动生成一些代码,避免手动编写。例如我们常用的Lombok就有采用APT来生成相关代码。

同时,获取注解和自定义生成代码等操作都是在编译期完成的,不会影响运行时期的程序性能。不过有一点需要注意,APT只能用于生成新文件,不能更改现有的文件。

img

基本原理

通过官方介绍已经知道MapStruct是通过Java编译期注解处理器也就是APT来实现的。那么具体是怎么样实现的呢?

  1. 实现一个APT首先需要继承AbstractProcessor或者实现Processor接口,基本都是继承抽象类来实现的。
  2. 随后在创建META-INF/services/javax.annotation.processing.Processor文件,并在里面注册自定义的Annotation Processor

通过查看[MapStruct](mapstruct/mapstruct: An annotation processor for generating type-safe bean mappers (github.com))的源码,就可以发现它定义了一个MappingProcessor的类同时继承了AbstractProcessor来实现相应功能。并在javax.annotation.processing.Processor文件中注册了MappingProcessor类。

image-20220615111712621

image-20220615111724109

MapStruct的具体怎么生成Mapper实现类先不鸟他,下面就来看看怎么使用吧~

优缺点

在使用前,先来看一波MapStruct的优缺点,在实践中就能切身体会到优缺点拉。

先吹优点:

  1. 高性能映射对象,相关于反射来实现的性能快将近20倍,差不多很接近原生的Getter/Setter方法。如果想要看具体性能比较可以参考这篇文章Performance of Java Mapping Frameworks | Baeldung
  2. 编译时期安全,不会出现错误对象的映射,例如不会出现将商品映射成用户DTO。
  3. 仅在编译时期工作,不会有运行期依赖

那么代价是啥捏?就我个人而言认为缺点只有一个:繁琐,相关于常用Spring中的BeanUtils编写较为繁琐,会增加工作量,不能够准时下班拉。

那么下面就来通过实例来了解MapStruct吧

如何简单使用

环境配置

  • Jdk11
  • MapStruct 1.5.0.RC1

需要注意一点,MapStruct只支持1.8或者更高的版本。

Maven配置:

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
...
<properties>
<org.mapstruct.version>1.5.0.RC1</org.mapstruct.version>
</properties>
...
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.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>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
...

入门例子

先定两个平平无奇的模型类GoodsGoodsDto

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
//Goods.java
public class Goods {

private Integer id;

private String name;

private Long price;

//...省略构造方法和Getter/Setter方法
}

//GoodsDto.java
public class GoodsDto {

private Integer id;

private String goodsName;

private Long price;

private LocalDate createTime;

//...省略构造方法和Getter/Setter方法
}

随后定义Mapper接口

1
2
3
4
5
6
7
8
@Mapper
public interface GoodsMapper {

GoodsMapper INSTANCE = Mappers.getMapper(GoodsMapper.class);

@Mapping(source = "name",target = "goodsName")
GoodsDto goodsToGoodsDto(Goods goods);
}

然后编译代码,跑一下测试方法,输出GoodsDto属性结果,然后你就可以说掌握MapStruct(^_^)拉。

1
2
3
4
5
6
7
8
    @Test
void shouldMapGoodsToDto(){
Goods goods = new Goods(1,"3060TI显卡",299900L);
GoodsDto goodsDto = GoodsMapper.INSTANCE.goodsToGoodsDto(goods);
log.info("goodsDto:{}",goodsDto);
}

// goodsDto:GoodsDto{id=1, goodsName='3060TI显卡', price=299900, createTime=null}
分析一下

通过上面的例子,可以知道对于类型和名字一致的属性对自动映射,而对于属性名不一致的时候就需要手动声明一下。同时对于未匹配的属性字段在编译时期会有一个警告⚠信息,例如下面的编译告警信息。

1
2
D:\java-workspace\mapstruct-example\src\test\java\pers\czj\mapstruct\example01\GoodsMapper.java:13:14
java: Unmapped target property: "createTime".

我们可以在target的目录下看到MapStruct生成的Mapper实现类,下面的代码就是GoodsMapper接口的生成。可以看到当我们使用goodsToGoodsDto方法时本质上就是调用对象的Getter/Setter方法,这就是为什么MapStruct是高性能的原因。

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
package pers.czj.mapstruct.example01;

import javax.annotation.processing.Generated;

@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2022-06-02T15:36:45+0800",
comments = "version: 1.5.0.RC1, compiler: javac, environment: Java 11.0.13 (Oracle Corporation)"
)
public class GoodsMapperImpl implements GoodsMapper {

@Override
public GoodsDto goodsToGoodsDto(Goods goods) {
if ( goods == null ) {
return null;
}

GoodsDto goodsDto = new GoodsDto();

goodsDto.setGoodsName( goods.getName() );
goodsDto.setId( goods.getId() );
goodsDto.setPrice( goods.getPrice() );

return goodsDto;
}
}

对象映射

不同名字段映射

使用上面例子的@Mapping(source = "name",target = "goodsName"),如果有多个映射属性名不一致就定义多个@Mapping注解。

自动映射类型
  • String类和包装类型的互相自动转换,例如StringInteger等。
  • 基本类型和其包装类型的互相转换
  • String和枚举类型之间的互相转换
结合lombok

在同时使用MapStructLombok的时候,需要注意编译顺序问题,当LombokSource Code 未生成时,生成的Mapper实现,将会缺少很多属性字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>11</source>
<target>11</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
多个数据源同时映射

MapStruct也支持多个数据源同时映射一个对象。

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
//Goods.java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Goods {

private Integer id;

private String name;

private Long price;

}

//GoodsInfo.java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GoodsInfo {

private Integer id;

private String sourceAddress;

private LocalDate createTime;

private String description;
}
//GoodsDto.java
@Data
public class GoodsDto {

private Integer id;

private Long price;

private String goodsName;

private String sourceAddress;

private String description;

private LocalDate createTime;

}
1
2
3
4
5
6
7
8
9
10
11
@Mapper
public interface GoodsMapper {

GoodsMapper INSTANCE = Mappers.getMapper(GoodsMapper.class);


@Mapping(source = "goods.name",target = "goodsName")
@Mapping(source = "goods.id",target = "id")
@Mapping(source = "info",target = ".")
GoodsDto goodsAndInfoToGoodsDto(Goods goods,GoodsInfo info);
}

这里使用了.将告诉MapStruct将源Bean的每个属性都映射到目标对象中。

更新已有实例
1
2
3
4
@Mapping(source = "goods.name",target = "goodsName")
@Mapping(source = "goods.id",target = "id")
@Mapping(source = "info",target = ".")
void updateGoodsDto(Goods goods, GoodsInfo info, @MappingTarget GoodsDto goodsDto);

使用@MappingTarget注解标明要更新的实例

更多操作

MapStruct还支持很多的操作,其他操作下篇在介绍拉~

观点

通过本文的一些例子,相信大家对于MapStruct有一丢丢的了解,它的优势就是高性能,通过APT来自动生成映射对象之间的Getter/Setter方法。当然他的劣势也比较明显,即繁琐。对于每个对象之间的映射都需要定义对应的Mapper接口,还需要处理一些类型转换,字段名不匹配等问题,会添加许多工作量。不过这并不妨碍MapStruct是一个优秀的对象映射框架。

如果你的项目追求高性能,那么就可以使用MapStruct。如果你的项目是普通的CRUD,并且产品还天天催那就还是选择BeanUtils一把梭,毕竟早点下班才是真谛~。

img