前言
持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
也不知道说啥,那就直入主题吧~
什么是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
只能用于生成新文件,不能更改现有的文件。
基本原理
通过官方介绍已经知道MapStruct
是通过Java编译期注解处理器也就是APT
来实现的。那么具体是怎么样实现的呢?
- 实现一个APT首先需要继承
AbstractProcessor
或者实现Processor
接口,基本都是继承抽象类来实现的。 - 随后在创建
META-INF/services/javax.annotation.processing.Processor
文件,并在里面注册自定义的Annotation Processor
。
通过查看MapStruct的源码,就可以发现它定义了一个MappingProcessor
的类同时继承了AbstractProcessor
来实现相应功能。并在javax.annotation.processing.Processor
文件中注册了MappingProcessor
类。
MapStruct
的具体怎么生成Mapper实现类
先不鸟他,下面就来看看怎么使用吧~
优缺点
在使用前,先来看一波MapStruct
的优缺点,在实践中就能切身体会到优缺点拉。
先吹优点:
- 高性能映射对象,相关于反射来实现的性能快将近20倍,差不多很接近原生的Getter/Setter方法。如果想要看具体性能比较可以参考这篇文章Performance of Java Mapping Frameworks | Baeldung。
- 编译时期安全,不会出现错误对象的映射,例如不会出现将
商品
映射成用户DTO。
- 仅在编译时期工作,不会有运行期依赖
那么代价是啥捏?就我个人而言认为缺点只有一个:繁琐
,相关于常用Spring中的BeanUtils
编写较为繁琐,会增加工作量,不能够准时下班了。
那么下面就来通过实例来了解MapStruct吧
如何简单使用
环境配置
- Jdk11
- MapStruct 1.5.0.RC1
需要注意一点,MapStruct
只支持1.8或者更高的版本。
Maven配置:
...
<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>
...
入门例子
先定两个平平无奇的模型类Goods
和GoodsDto
//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接口
@Mapper
public interface GoodsMapper {
GoodsMapper INSTANCE = Mappers.getMapper(GoodsMapper.class);
@Mapping(source = "name",target = "goodsName")
GoodsDto goodsToGoodsDto(Goods goods);
}
然后编译代码,跑一下测试方法,输出GoodsDto
属性结果,然后你就可以说掌握MapStruct
( ^_^ )拉。
@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}
分析一下
通过上面的例子,可以知道对于类型和名字一致的属性对自动映射,而对于属性名不一致的时候就需要手动声明一下。同时对于未匹配的属性字段在编译时期会有一个警告⚠信息,例如下面的编译告警信息。
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
是高性能的原因。
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类和包装类型的互相自动转换,例如
String
转Integer
等。 - 基本类型和其包装类型的互相转换
- String和枚举类型之间的互相转换
Date
,LocalDate
,LocalDateTime
等时间类和String
的转换
结合lombok
在同时使用MapStruct
和Lombok
的时候,需要注意编译顺序问题,当Lombok
的Source Code
未生成时,生成的Mapper
实现,将会缺少很多属性字段。
<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
也支持多个数据源同时映射一个对象。
//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;
}
@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的每个属性都映射到目标对象中。
更新已有实例
@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
一把梭,毕竟早点下班才是真谛~。