Java实体对象映射工具MapStruct

前言

今天我们来介绍一款非常优秀的实体对象映射工具,MapStruct

在开发的时候我们会看到业务代码之间有很多的JavaBean之间的相互转化,非常影响美观,但是又不得不存在。

通常的JavaBean处理方法是通过Spring或者Apache提供的BeanUtils工具,而对于一些不匹配的属性,通过get/set方法解决。

由于BeanUtils使用反射机制,故其在大量使用时可能会影响到性能,同时对于不匹配的属性,如果较多,get/set起来也非常麻烦和繁琐。

对于一些特殊类型不一致的字段,如DO里为Date类型,可能DTO里需要变为时间的String类型,我们需要调用指定方法进行处理,再进行get/set

正文

MapStruct可以让我们从这种复杂而繁琐的工作中解放出来。

MapSturct 是一个生成类型安全,高性能且无依赖的JavaBean映射代码的注解处理器(annotation processor)。

它可以让我们通过注解的方式,生成JavaBean之间的映射代码,相较于反射,更安全更高效。

让我们来看一下。

MapStruct引入

首先我们需要在Maven项目中引入MapSturct的相关Jar包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<properties>
<java.version>1.8</java.version>
<org.mapstruct.version>1.3.1.Final</org.mapstruct.version>
</properties>

<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct -->
<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>

有两个,mapstruct是主要包,mapstruct-processor用来辅助生成JavaBean之间的映射代码。

MapStruct使用

我们根据例子来看如何使用MapSturct

我们这儿有两个JavaBeanPersonDOPersonDTO,现在我们将PersonDO转为PersonDTO

1
2
3
4
5
6
7
8
9
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PersonDO {
private Integer id;
private String name;
private int age;
private String gender;
}
1
2
3
4
5
6
7
8
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PersonDTO {
private String userName;
private Integer age;
private Gender gender;
}
1
2
3
public enum Gender {
MAN,MALE
}

转化时需要编写Mapper,即对象映射器,是一个接口,如下。

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

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

@Mapping(source = "name", target = "userName")
PersonDTO do2dto(PersonDO person);
}

使用注解@Mapper定义一个Converter接口,在其中定义一个do2dto方法,方法的入参类型是PersonDO,出参类型是PersonDTO,这个方法就用于将PersonDO转成PersonDTO

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
public static void main(String[] args) {
PersonDO personDO = new PersonDO();
personDO.setName("Sakura");
personDO.setAge(27);
personDO.setId(1);
personDO.setGender(Gender.MAN.name());

PersonDTO personDTO = PersonConverter.INSTANCE.do2dto(personDO);
System.out.println(personDTO);
}
}

输出结果

1
PersonDTO(userName=Sakura, age=27, gender=MAN)

由上面的例子可以看到MapSturct成功将PersonDO转成PersonDTO

MapStruct字段映射处理

两个JavaBean对象进行映射,属性名称一致的字段可以相互映射,不一致的如何处理呢?

两个字段名字不一致

从上面的例子可以看出,我们只需要使用@Mapping注解。在转换方法上设置

1
@Mapping(source = "name", target = "userName")

即可。其含义是将name字段的值映射给userName

MapStruct可以映射的类型

在上面例子,我们发现age字段完成映射,其类型一个为int,另一个为包装类。

gender字段完成映射,其类型一个为枚举,一个为String

一般情况下,MapSturct会对部分类型做自动映射,而不需要我们额外配置。

  • 基本类型及其他们对应的包装类型。
  • 基本类型的包装类型和String类型之间
  • String类型和枚举类型之间

自定义常量和默认值

如果我们想在转换中,给某个属性一个固定值,可以使用constant

比如PersonDTO里新增身高字段。而PersonDO里没有,那么我们可以在转换时赋予默认值。

1
2
3
4
5
6
7
8
9
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PersonDTO {
private String userName;
private Integer age;
private Gender gender;
private Integer height;
}
1
@Mapping(target = "height",constant = "175")

而对于某一个属性,如果映射后为空,我们可以给予其默认值,使用defaultValue,如下:

1
@Mapping(target = "gender",defaultValue = "MAN")

类型不一致的映射

在实际转化中,有可能字段类型是不一致的,如何进行映射呢?

我们使用上面的例子,在PersonDO里增加生日属性,为Date类型,PersonDTO里为String类型。

1
2
3
4
5
6
7
8
9
10
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PersonDO {
private Integer id;
private String name;
private int age;
private String gender;
private Date birthday;
}
1
2
3
4
5
6
7
8
9
10
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PersonDTO {
private String userName;
private Integer age;
private Gender gender;
private Integer height;
private String birthday;
}

这时候我们可以使用如下方法转化。

1
2
3
4
@Mapping(source = "name", target = "userName")
@Mapping(target = "height",constant = "175")
@Mapping(target = "birthday",dateFormat = "yyyy-MM-dd HH:mm:ss")
PersonDTO do2dto(PersonDO person);

运行后可以看到正确输出结果:

1
PersonDTO(userName=Sakura, age=27, gender=MAN, height=175, birthday=2020-08-17 15:20:16)

dateFormatMapStruct自带的属性,如果我们的属性映射方法比较复杂呢?

当然,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
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PersonDO {
private Integer id;
private String name;
private int age;
private String gender;
private Date birthday;
private List<Integer> hobbies;
}
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PersonDTO {
private String userName;
private Integer age;
private Gender gender;
private Integer height;
private String birthday;
private List<String> strHobbies;
}
public enum HobbiesEnum {
FOOTBALL(1,"足球"),
READ_BOOK(2,"读书"),
PLAY_GAME(3,"玩游戏"),
SINGING(4,"唱歌"),
TRAVEL(5,"旅行");
private Integer value;
private String desc;

//get/set 构造器略

/**
* 获取一个枚举
* @param value
* @return
*/
public static HobbiesEnum getEnum(Integer value){
return Arrays.stream(HobbiesEnum.values()).filter(e->e.value.equals(value)).findFirst().orElse(null);
}
}

需要完成ListhobbiesListstrHobbies的转化,我们可以使用如下代码:

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
@Mapper
public interface PersonConverter {
PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);

@Mapping(source = "name", target = "userName")
@Mapping(target = "height",constant = "175")
@Mapping(target = "gender",defaultValue = "MAN")
@Mapping(target = "birthday",dateFormat = "yyyy-MM-dd HH:mm:ss")
@Mapping(target = "strHobbies",expression = "java(getHobbiesStr(person.getHobbies()))")
PersonDTO do2dto(PersonDO person);
/**
* 爱好映射
* @param integers
* @return
*/
default List<String> getHobbiesStr(List<Integer> integers){
List<String> list = new ArrayList<>();
for (Integer integer : integers) {
HobbiesEnum hobbiesEnum = HobbiesEnum.getEnum(integer);
if(hobbiesEnum!=null){
list.add(hobbiesEnum.getDesc());
}
}
return list;
}
}

PS:java1.8接口支持自定义default方法,expression = "java(getHobbiesStr(person.getHobbies()))"这段代码也可以指向自己写的某个方法,需要具体路径包名。

上面代码还是非常好理解的,这儿就不过多叙述。

多个属性映射

MapStruct还支持将多个实体数据组装到一个实体中,如下:

比如上面的例子,我们的身高属性来自另一个Bean,则代码如下:

1
2
3
4
5
6
7
@NoArgsConstructor
@AllArgsConstructor
@Data
public class HeightDO {
private String id;
private Integer height;
}

则组装数据的方法如下:

1
2
3
4
5
6
@Mapping(source = "person.name", target = "userName")
@Mapping(source = "height.height",target = "height")
@Mapping(target = "gender",defaultValue = "MAN")
@Mapping(target = "birthday",dateFormat = "yyyy-MM-dd HH:mm:ss")
@Mapping(target = "strHobbies",expression = "java(getHobbiesStr(person.getHobbies()))")
PersonDTO do2dto(PersonDO person,HeightDO height);

对于source属性,需要指定来自哪个DO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test {
public static void main(String[] args) {
PersonDO personDO = new PersonDO();
personDO.setName("Sakura");
personDO.setAge(27);
personDO.setId(1);
personDO.setBirthday(new Date());
personDO.setGender(Gender.MAN.name());
List<Integer> list =new ArrayList<>();
list.add(1);
list.add(3);
personDO.setHobbies(list);

HeightDO heightDO = new HeightDO();
heightDO.setHeight(178);

PersonDTO personDTO = PersonConverter.INSTANCE.do2dto(personDO,heightDO);
System.out.println(personDTO);
}
}

输出结果:

1
PersonDTO(userName=Sakura, age=27, gender=MAN, height=178, birthday=2020-08-17 15:52:14, strHobbies=[足球, 玩游戏])

更新现有Bean实例

MapStruct还支持将一个Bean的属性更新到另一个Bean的同名属性里。

1
void updateDTOfromDO(PersonDO person, @MappingTarget PersonDTO personDTO);

需要注意这个对于同名属性,如果DO上有值,那么DTO上的值将被覆盖,如下:

1
2
3
4
5
6
7
8
9
10
11
12
PersonDTO dto = new PersonDTO();
dto.setAge(15);

PersonDO aDo = new PersonDO();
aDo.setName("Sakura");
aDo.setAge(27);
aDo.setId(1);
aDo.setBirthday(new Date());
aDo.setGender(Gender.MAN.name());

PersonConverter.INSTANCE.updateDTOfromDO(aDo,dto);
System.out.println(dto);

输出结果:

1
PersonDTO(userName=null, age=27, gender=MAN, height=null, birthday=2020/8/17 下午4:29, strHobbies=null)

继承反转配置

MapStruct在存在DO转换为DTO的前提下,还支持将DTO转换为DO,而且不用我们复杂的配置。

方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Mapper
public interface PersonConverter {

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

@Mapping(source = "name", target = "userName")
@Mapping(target = "height",constant = "175")
@Mapping(target = "gender",defaultValue = "MAN")
@Mapping(target = "birthday",dateFormat = "yyyy-MM-dd HH:mm:ss")
@Mapping(target = "strHobbies",expression = "java(getHobbiesStr(person.getHobbies()))")
PersonDTO do2dto(PersonDO person);


@InheritInverseConfiguration(name = "do2dto")
PersonDO dto2do(PersonDTO personDTO);
}

我们只需要@InheritInverseConfiguration注解即可解决,name属性指向DO转换为DTO的方法。

测试:

1
2
3
4
5
6
7
8
9
10
11
12
PersonDTO dto1 = new PersonDTO();
dto1.setAge(27);
dto1.setBirthday("2018-11-11 12:12:12");
dto1.setGender(Gender.MAN);
dto1.setHeight(175);
dto1.setUserName("Sakura");
List<String> list1 = new ArrayList<>();
list1.add("足球");
list1.add("玩游戏");
dto1.setStrHobbies(list1);
PersonDO personDO1 = PersonConverter.INSTANCE.dto2do(dto1);
System.out.println(personDO1);

输出:

1
PersonDO(id=null, name=Sakura, age=27, gender=MAN, birthday=Sun Nov 11 12:12:12 CST 2018, hobbies=null)

这儿需要注意的是我们看到Hobbies属性并没有转化,这是正常的,这个需要我们配置@Mapping,写出并指定兴趣爱好转化的逆向方法。

MapStruct的性能

MapStruct的性能是非常优秀的,我们来测试一下:

为简化代码,我们是DODTO尽可能的简单。

1
2
3
4
5
6
@AllArgsConstructor
@NoArgsConstructor
@Data
public class TestDO {
private Integer a;
}
1
2
3
4
5
6
@NoArgsConstructor
@AllArgsConstructor
@Data
public class TestDTO {
private Integer a;
}

MapStruct相关方法:

1
2
3
4
5
6
@Mapper
public interface TestConverter {
TestConverter INSTANCE = Mappers.getMapper(TestConverter.class);

TestDTO do2dto(TestDO testDO);
}

我们使用Spring自带的BeanUtils工具类来与其进行比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
final static int NUM = 100000;
public static void main(String[] args) {
long start1 = System.currentTimeMillis();
for (int i =0;i<NUM;i++){
TestDO testDO = new TestDO();
testDO.setA(i);
TestDTO testDTO = new TestDTO();
BeanUtils.copyProperties(testDO,testDTO);
}
System.out.println("BeanUtils属性拷贝耗时:"+(System.currentTimeMillis()-start1)+"ms");

long start2 = System.currentTimeMillis();
for (int i =0;i<NUM;i++){
TestDO testDO = new TestDO();
testDO.setA(i);
TestDTO dto = TestConverter.INSTANCE.do2dto(testDO);
}
System.out.println("MapStruct处理耗时:"+(System.currentTimeMillis()-start2)+"ms");
}
}

我们以100000个对象为样本,得到如下输出结果:

1
2
BeanUtils属性拷贝耗时:1903ms
MapStruct处理耗时:5ms

可以看到差距是非常大的。

MapStruct为什么有如此优秀的性能呢?

其实核心点在于:MapStruct在编译期间,就生成了对象映射代码,确保了高性能,同时编译期间也可以发现可能存在的映射问题。

在上面的TestConverter中,经过代码编译后,MapStruct会生成一个TestConverterImpl的实现类,帮我们进行对象属性转换。

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2020-08-17T17:00:26+0800",
comments = "version: 1.3.1.Final, compiler: javac, environment: Java 9 (Oracle Corporation)"
)
public class TestConverterImpl implements TestConverter {

@Override
public TestDTO do2dto(TestDO testDO) {
if ( testDO == null ) {
return null;
}

TestDTO testDTO = new TestDTO();

testDTO.setA( testDO.getA() );

return testDTO;
}
}

这样在运行之前就相当于我们手写get/set方法,相比反射,速度更快。

其编译工作主要由mapstruct-processor包完成。

PS:PersonConverterImpl的相关实现这儿就不展示了,大家可以看下它是如何处理的,比如对于一些自定义方法等。

总结

本文介绍了一款Java对象映射工具,MapStruct。相比传统BeanUtils有着更高的性能。

映射代码在编译期间生成,相当于替我们手写了get/set,不仅相比反射具有很好的性能,同时还可以在编译期间发现可能存在的映射问题。




-------------文章结束啦 ~\(≧▽≦)/~ 感谢您的阅读-------------

您的支持就是我创作的动力!

欢迎关注我的其它发布渠道