对象映射神器Mapstruct,高效开发
你是不是还在为写一些实体转换的原始代码感到头疼,尤其是实体字段特别多的时候 既费时又费力,代码还不好维护。* mapstruct * (https://mapstruct.org/) 这款工具类的诞生就是为了解决上述问题,可以轻松优雅的进行转换,简化你的代码。当然工具类的选择没有最好只有更适合,大家在使用的时候一定要分析在项目环境下的利弊。
废话不多说了,上代码
痛点
``Animal`动物信息类
@Data
@ToString
public class Animal {
private String name;
private Integer type;
private Integer age;
}
``Dog`狗狗信息类
@Data
@ToString
public class Animal {
private String name;
private Integer type;
private Integer age;
}
明明上面的类字段属性都是相似的,但有个方法偏偏只接受Animal
类型,我们该咋办
通常我们会这么写一个方法进行先进行转换然后获取返回结果作为入参:
private Animal converObj(Dog dog){
Animal animal = new Animal();
animal.setAge(dog.getAge());
animal.setName(dog.getName());
animal.setType(dog.getType());
return animal;
}
这种写法非常繁琐无味,而且没有技术含量。甚至中间还牵涉了很多类型转换,嵌套之类的繁琐操作,而我们想要的只是建立它们之间的映射关系而已。有没有一种通用的映射工具来帮我们搞定这一切。当然有而且还不少。有人说apache
的BeanUtil.copyProperties
可以实现,但是性能差而且容易出异常,很多规范严禁使用这种途径。以下是对几种对象映射框架的对比,大多数情况下MapStruct
性能最高。原理类似于lombok
,MapStruct
都是在编译期进行实现,而且基于Getter
、Setter
,没有使用反射所以一般不存在运行时性能问题。
耗时结果如下所示:
运行次数 | setter方法耗时 | BeanUtils拷贝耗时 | MapperStruct拷贝耗时 |
---|---|---|---|
1 | 2921528(1) | 3973292(1.36) | 2989942(1.023) |
10 | 2362724(1) | 66402953(28.10) | 3348099(1.417) |
100 | 2500452(1) | 71741323(28.69) | 2120820(0.848) |
1000 | 3187151(1) | 157925125(49.55) | 5456290(1.711) |
10000 | 5722147(1) | 300814054(52.57) | 5229080(0.913) |
100000 | 19324227(1) | 244625923(12.65) | 12932441(0.669) |
简单使用
我们首先要在项目中集成它,集成方式有很多种pom、jar、gradle ,这里我们以pom的方式演示。
注意: jdk只支持jdk1.8或者更高的版本,maven插件要使用3.6.0版本以上、lombok使用1.16.16版本以上,另外编译的lombok mapstruct的插件不要忘了加上。否则会出现下面的错误:No property named “aaa” exists in source parameter(s). Did you mean “null”? 这种异常就是lombok编译异常导致缺少get setter方法造成的。还有就是缺少构造函数也会抛异常。
pom依赖:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<mapstruct.version>1.4.1.Final</mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
<scope>compile</scope>
</dependency>
<!-- other dependencies -->
</dependencies>
我们把开始的痛点解决一下。看看 Mapstruct 如何降低编程成本
@Mapper
public interface AnimalConver {
AnimalConver INSTANCE = Mappers.getMapper( AnimalConver.class );
/**
* @param dog 源数据
* @return Animal 转换后数据
*/
@Mapping(target = "name", source = "dog.name")
@Mapping(target = "type", source = "dog.type")
@Mapping(target = "age", source = "dog.age")
Animal dogToAnimal(Dog dog);
}
格式:转换后类型 方法名(源类型)
成员变量相同类型 相同变量名不用写{@link org.mapstruct.Mapping}来映射注意:mapstruct中是不可以使用重载的
再度简化
@Mapper
public interface AnimalConver {
AnimalConver INSTANCE = Mappers.getMapper( AnimalConver.class );
/**
* @param dog 源数据
* @return Animal 转换后数据
*/
Animal dogToAnimal(Dog dog);
}
在我们需要的地方这样调用 AnimalConver.INSTANCE.dogToAnimal( 入参 )
即可;
上面短短几行代码就可以了十分简单!解释一下操作步骤:
首先声明一个映射接口用@org.mapstruct.Mapper
(不要跟mybatis注解混淆)标记,说明这是一个实体类型转换接口。这里我们声明了一个 AnimalConver
接口 来方便我们调用,Animal dogToAnimal(Dog dog)
是不是很熟悉, 像mybatis
一样抽象出我们的转换方法。@org.mapstruct.Mapping
注解用来声明成员属性的映射。该注解有两个重要的属性:
source
代表转换的源。这里就是dog
。target
代表转换的目标。这里是Animal
。
MapStruct
最终调用的是 setter
和 getter
方法,而非反射。这也是其性能比较好的原因之一。而且对于包装类是自动拆箱封箱操作的,并且是线程安全的。MapStruct不单单有这些功能,还有其他一些复杂的功能。接下来带大家操作一下
List 转换
list转换需要先将泛型中的方法进行映射,如何在紧跟后面加上一个新方法用于转换list
@Mapper
public interface AnimalConver {
AnimalConver INSTANCE = Mappers.getMapper( AnimalConver.class );
Animal toAnimal(Dog dog);
List<Animal> toAnimals(List<Dog> dog);
}
多数据源同时映射
在进行多数据源映射时必须指定@Mapping
@Mapper
public interface AnimalConver {
AnimalConver INSTANCE = Mappers.getMapper( AnimalConver.class );
/**
* @param dog 源数据
* @return Animal 转换后数据
*/
@Mapping(target = "name", source = "dog1.name")
@Mapping(target = "type", source = "dog2.type")
@Mapping(target = "age", source = "dog3.age")
Animal dogToAnimal(Dog1 dog1,Dog2 dog2,Dog3 dog3);
}
使用 java 表达式
首先在@org.mapstruct.Mapper
的 imports
属性中导入我们需要使用的类,该属性是数组意味着你可以根据需要导入更多的处理类:
@Mapper(imports = {xx.class,xxxx.class})
这里我们我们以时间类的 LocalDateTime
为例子
接下来只需要在对应的方法上添加注解@org.mapstruct.Mapping
,其属性expression
接收一个 java()
包括的表达式:
- 无入参版本:
@Mapping(target = "addTime", expression = "java(LocalDateTime.now())")
- 携带入参的版本, 我们将
Dog
的日期字符串time
注入到Animal
的LocalDateTime
类型属性time
中去:
@Mapping(target = "time", expression = "java(LocalDateTime.parse(dog.time))")
Animal toAnimal(Dog dog);
同理如果我们自己写的了有工具类也可以使用这个方式,在进行映射的时候进行操作
Mapping常用参数介绍
source
:源数据,在上述例子中代表 Dog 指需要被映射的数据target
:目标值,在上述例子中代表 Animal 指映射成的数据constant
:常量值,如果在映射时目标值多于源数据 那么就可以使用这个代指defaultValue
:默认值,转换当源属性为null的时候,我们用defaultValue来指定默认值ignore
:忽略转换的值expression
:需要写一些表达式,如stream,optional,xxxUtil。但前提是需要在 Mapper中imports
Mapper 注入Spring IoC 容器
@Mapper(componentModel = "spring")
这样书写即可,spring必须是全小写
总结
lombok 包 与 mapstruct 包 两个包一起用,我们可以省去很多代码的编写。其实MapStruct 还有很多的功能。但是从可读性来说,我建议使用以上几种容易理解的功能即可。各有优劣,自己根据项目选择,没有最好的,只有最合适