1、MapStruct介绍
- MapStruct是一个Java注释处理器,用于生成类型安全的bean映射类,它基于约定优于配置方法,极大地简化了 Java bean
类型之间映射的实现。 - 您所要做的就是定义一个mapper接口,该接口声明任何所需的映射方法。在编译期间,MapStruct将生成此接口的实现。此实现使用普通的Java方法调用来在源对象和目标对象之间进行映射,即没有反射或类似。
- 与手工编写映射代码相比,MapStruct通过生成繁琐且易于编写的代码来节省时间。遵循约定优于配置方法,MapStruct使用合理的默认值,但在配置或实现特殊行为时会采取措施。
- 与动态映射框架相比,MapStruct具有以下优势:
-
通过使用普通方法调用而不是反射来快速执行
-
编译时类型安全:只能映射相互映射的对象和属性,不会将订单实体意外映射到客户DTO等。
-
在构建时清除错误报告,如果
-
- 映射不完整(并非所有目标属性都已映射)
-
- 映射不正确(找不到合适的映射方法或类型转换)
2、应用设置
2.1 Maven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.siayou</groupId>
<artifactId>mapstruct-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<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>
<org.mapstruct.version>1.4.1.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.12</org.projectlombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<!-- lombok dependencies should not end up on classpath -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
<scope>provided</scope>
</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.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
3、功能实战
3.1 常用注解
- @Mapper 标记这个接口作为一个映射接口,并且是编译时 MapStruct 处理器的入口
- @Mapping 解决源对象和目标对象中,属性名字不同的情况
- @Mappings 当存在多个 @Mapping 需要配置;可以通过 @Mappings 批量指定
- Mappers.getMapper Mapper 的 class 获取自动生成的实现对象,从而让客户端可以访问 Mapper 接口的实现
3.2 基本映射
3.2.1 定义映射器的Java接口
@Mapper
public interface StudentMapper {
StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
//mapper可以进行字段映射,改变字段类型,指定格式化的方式,包括一些日期的默认处理。
//无论date转string,还是string转date,都是用dateFormat
@Mapping(source = "gender.name", target = "gender")
@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss")
StudentVO student2StudentVO(Student student);
}
// @Data 在编译时会自动添加 Getter、Setter、equals、canEqual、hasCode、toString 等方法,高效且代码非常简洁。
// @Builder 可代替需要的很多构造函数,解决了某个类有很多构造函数的情况。
// @AllArgsConstructor 在编译时会自动添加一个含有所有已声明字段的构造函数,不必再手动编写含有所有已声明字段的构造函数。
// @NoArgsConstructor 在编译时会自动添加一个无参的构造函数,不必再手动编写无参构造函数。
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Student {
//姓名
private String name;
//年龄
private int age;
//性别
private GenderEnum gender;
//身高
private Double height;
//生日
private Date birthday;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class StudentVO {
//姓名
private String name;
//年龄
private int age;
//性别
private String gender;
//身高
private Double height;
//生日
private String birthday;
}
public enum GenderEnum {
Male("1", "男"),
Female("0", "女");
private String code;
private String name;
public String getCode() {
return this.code;
}
public String getName() {
return this.name;
}
GenderEnum(String code, String name) {
this.code = code;
this.name = name;
}
}
3.2.2 测试验证
public static void main(String[] args) {
Student student = Student.builder()
.name("张三")
.age(16)
.gender(GenderEnum.Male)
.height(174.3)
.birthday(new Date())
.build();
System.out.println(student);
StudentVO studentVO = StudentMapper.INSTANCE.student2StudentVO(student);
System.out.println(studentVO);
}
//测试结果
Student(name=张三, age=16, gender=Male, height=174.3, birthday=Fri Sep 22 16:22:38 CST 2023)
StudentVO(name=张三, age=16, gender=男, height=174.3, birthday=2023-09-22 16:22:38)
3.3 参数引用映射
3.3.1 定义映射器的Java接口
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class StudentVO {
//姓名
private String name;
//年龄
private int age;
//性别
private String gender;
//身高
private Double height;
//生日
private String birthday;
private ExtendDto extendDto;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Student {
//姓名
private String name;
//年龄
private int age;
//性别
private GenderEnum gender;
//身高
private Double height;
//生日
private Date birthday;
private ExtendDto extendDto;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ExtendDto {
//邮箱
private String email;
//地址
private String address;
//电话
private String phone;
}
@Mapper
public interface StudentMapper {
@Mapping(source = "student.birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss")
@Mapping(source = "email", target = "extendDto.email")
StudentVO studentTwoStudentVO(Student student,String email);
}
3.3.2 测试验证
public static void main(String[] args) {
Student student = Student.builder()
.name("李四")
.age(16)
.gender(GenderEnum.Male)
.height(174.3)
.birthday(new Date())
.build();
System.out.println(student);
StudentVO studentVO = StudentMapper.INSTANCE.studentTwoStudentVO(student,"dmjxsy@126.com");
System.out.println(studentVO);
}
//测试结果
Student(name=李四, age=16, gender=Male, height=174.3, birthday=Thu Sep 28 15:12:36 CST 2023, extendDto=null)
StudentVO(name=李四, age=16, gender=Male, height=174.3, birthday=2023-09-28 15:12:36, extendDto=ExtendDto(email=dmjxsy@126.com, address=null, phone=null))
3.4 多对象参数映射
3.4.1 定义映射器的Java接口
@Mapping(source = "student.birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss")
@Mapping(source = "extendDto", target = "extendDto")
StudentVO studentMapStudentVO(Student student,ExtendDto extendDto);
3.4.2 测试验证
public static void main(String[] args) {
Student student = Student.builder()
.name("李四")
.age(16)
.gender(GenderEnum.Male)
.height(174.3)
.birthday(new Date())
.build();
ExtendDto extendDto = ExtendDto.builder()
.email("dmjxsy@126.com")
.phone("119")
.address("陕西")
.build();
System.out.println(student);
System.out.println(extendDto);
StudentVO studentVO = StudentMapper.INSTANCE.studentMapStudentVO(student,extendDto);
System.out.println(studentVO);
}
//测试结果
Student(name=李四, age=16, gender=Male, height=174.3, birthday=Thu Sep 28 15:24:18 CST 2023, extendDto=null)
ExtendDto(email=dmjxsy@126.com, address=陕西, phone=119)
StudentVO(name=李四, age=16, gender=Male, height=174.3, birthday=2023-09-28 15:24:18, extendDto=ExtendDto(email=dmjxsy@126.com, address=陕西, phone=119))
}
3.4.3 注意点
当配置了参数时,在配置对象属性关系映射时,也要显示的指明对象参数的名称。如@Mapping(source = “student.birthday”, target = “birthday”) 就显示指明了 source = “student.birthday” 。
3.5 嵌套映射
3.5.1 定义映射器的Java接口
@Mapping(source = "student.birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss")
@Mapping(source = "extendDto", target = ".")
StudentVO studentMapStudentVO(Student student);
3.5.2 测试验证
public static void main(String[] args) {
Student student = Student.builder()
.name("李四")
.age(16)
.gender(GenderEnum.Male)
.height(174.3)
.birthday(new Date())
.extendDto( ExtendDto.builder()
.email("dmjxsy@126.com")
.phone("110")
.address("陕西")
.build())
.build();
System.out.println(student);
StudentVO studentVO = StudentMapper.INSTANCE.studentMapStudentVO(student);
System.out.println(studentVO);
}
//测试结果
Student(name=李四, age=16, gender=Male, height=174.3, birthday=Thu Sep 28 15:31:14 CST 2023, extendDto=ExtendDto(email=dmjxsy@126.com, address=陕西, phone=110))
StudentVO(name=李四, age=16, gender=Male, height=174.3, birthday=2023-09-28 15:31:14, extendDto=ExtendDto(email=dmjxsy@126.com, address=陕西, phone=110))
3.5.3 注意点
使用 “ . ” ,将一个嵌套的bean的值合并到一个扁平化的对象中。
3.6 对象更新
3.6.1 定义映射器的Java接口
@Mapping(source = "student.birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss")
@Mapping(source = "extendDto", target = ".")
// @Mapping(source = "name",target = "name", ignore = true)
void updateStudent(Student student,@MappingTarget StudentVO studentVO);
@Mapping(source = "student.birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss")
@Mapping(source = "extendDto", target = ".")
@Mapping(source = "name",target = "name", ignore = true)
void updateStudent(Student student,@MappingTarget StudentVO studentVO);
3.6.2 测试验证
public static void main(String[] args) {
Student student = Student.builder()
.name("李四")
.age(16)
.gender(GenderEnum.Male)
.height(174.3)
.birthday(new Date())
.extendDto( ExtendDto.builder()
.email("dmjxsy@126.com")
.phone("110")
.address("陕西")
.build())
.build();
StudentVO studentVO = StudentVO.builder()
.name("王五")
.age(20)
.height(204.3)
.birthday("2020-01-01 12:11:11")
.extendDto( ExtendDto.builder()
.email("dmjxsy@163.com")
.phone("110110119")
.address("陕西西安")
.build())
.build();
System.out.println("更新前 student:"+student.toString());
System.out.println("更新前 studentVO:"+studentVO.toString());
StudentMapper.INSTANCE.updateStudent(student,studentVO);
System.out.println("更新后 student:"+student.toString());
System.out.println("更新后 studentVO:"+studentVO.toString());
}
//测试结果
更新前 :Student(name=李四, age=16, gender=Male, height=174.3, birthday=Thu Sep 28 15:56:35 CST 2023, extendDto=ExtendDto(email=dmjxsy@126.com, address=陕西, phone=110))
更新前 :StudentVO(name=王五, age=20, gender=null, height=204.3, birthday=2020-01-01 12:11:11, extendDto=ExtendDto(email=dmjxsy@163.com, address=陕西西安, phone=110110119))
更新后 :Student(name=李四, age=16, gender=Male, height=174.3, birthday=Thu Sep 28 15:56:35 CST 2023, extendDto=ExtendDto(email=dmjxsy@126.com, address=陕西, phone=110))
更新后 :StudentVO(name=李四, age=16, gender=Male, height=174.3, birthday=2023-09-28 15:56:35, extendDto=ExtendDto(email=dmjxsy@126.com, address=陕西, phone=110))
//添加 @Mapping(source = "name",target = "name", ignore = true)
更新前 :Student(name=李四, age=16, gender=Male, height=174.3, birthday=Thu Sep 28 15:57:39 CST 2023, extendDto=ExtendDto(email=dmjxsy@126.com, address=陕西, phone=110))
更新前 :StudentVO(name=王五, age=20, gender=null, height=204.3, birthday=2020-01-01 12:11:11, extendDto=ExtendDto(email=dmjxsy@163.com, address=陕西西安, phone=110110119))
更新后 :Student(name=李四, age=16, gender=Male, height=174.3, birthday=Thu Sep 28 15:57:39 CST 2023, extendDto=ExtendDto(email=dmjxsy@126.com, address=陕西, phone=110))
更新后 :StudentVO(name=王五, age=16, gender=Male, height=174.3, birthday=2023-09-28 15:57:39, extendDto=ExtendDto(email=dmjxsy@126.com, address=陕西, phone=110))
3.6.3 注意点
@MappingTarget 使用改注解标记目标对象我们就可以更新该对象的值。如果想不更新某个值,可以给加一个ignore = true的标签来忽略。
4、性能比较
4.1 Apache BeanUtils 与 MapStruct比较
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
for (int i = 0; i < 10; i++) {
Long start = System.currentTimeMillis();
for (int i1 = 0; i1 < 1000000; i1++) {
Student student = Student.builder()
.name("小明")
.age(6)
.gender(GenderEnum.Male)
.height(121.1)
.birthday(new Date())
.build();
StudentVO studentVO = new StudentVO();
org.apache.commons.beanutils.BeanUtils.copyProperties(studentVO, student);
}
System.out.println("org.apache.commons.beanutils.BeanUtils第"+ i +"次执行---100W次转换耗时:" + (System.currentTimeMillis() - start));
Long start2 = System.currentTimeMillis();
for (int i1 = 0; i1 < 1000000; i1++) {
Student student = Student.builder()
.name("小明")
.age(6)
.gender(GenderEnum.Male)
.height(121.1)
.birthday(new Date())
.build();
StudentMapper.INSTANCE.student2StudentVO(student);
}
System.out.println("MapStruct第"+ i +"次执行-----100W次转换耗时:" + (System.currentTimeMillis() - start2));
}
}
//测试结果
org.apache.commons.beanutils.BeanUtils第0次执行---100W次转换耗时:11854
MapStruct第0次执行-----100W次转换耗时:1981
org.apache.commons.beanutils.BeanUtils第1次执行---100W次转换耗时:10828
MapStruct第1次执行-----100W次转换耗时:2080
org.apache.commons.beanutils.BeanUtils第2次执行---100W次转换耗时:9894
MapStruct第2次执行-----100W次转换耗时:1891
org.apache.commons.beanutils.BeanUtils第3次执行---100W次转换耗时:9543
MapStruct第3次执行-----100W次转换耗时:1735
org.apache.commons.beanutils.BeanUtils第4次执行---100W次转换耗时:6862
MapStruct第4次执行-----100W次转换耗时:1958
4.2 Hutool BeanUtil与 MapStruct比较
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
for (int i = 0; i < 5; i++) {
Long start = System.currentTimeMillis();
for (int i1 = 0; i1 < 1000000; i1++) {
Student student = Student.builder()
.name("小明")
.age(6)
.gender(GenderEnum.Male)
.height(121.1)
.birthday(new Date())
.build();
StudentVO studentVO = new StudentVO();
cn.hutool.core.bean.BeanUtil.copyProperties(studentVO,student,true);
}
System.out.println("cn.hutool.core.bean.BeanUtil第"+ i +"次执行---100W次转换耗时:" + (System.currentTimeMillis() - start));
Long start2 = System.currentTimeMillis();
for (int i1 = 0; i1 < 1000000; i1++) {
Student student = Student.builder()
.name("小明")
.age(6)
.gender(GenderEnum.Male)
.height(121.1)
.birthday(new Date())
.build();
StudentMapper.INSTANCE.student2StudentVO(student);
}
System.out.println("MapStruct第"+ i +"次执行-----100W次转换耗时:" + (System.currentTimeMillis() - start2));
}
}
//测试结果
cn.hutool.core.bean.BeanUtil第0次执行---100W次转换耗时:7169
MapStruct第0次执行-----100W次转换耗时:2538
cn.hutool.core.bean.BeanUtil第1次执行---100W次转换耗时:4797
MapStruct第1次执行-----100W次转换耗时:1707
cn.hutool.core.bean.BeanUtil第2次执行---100W次转换耗时:5582
MapStruct第2次执行-----100W次转换耗时:1505
cn.hutool.core.bean.BeanUtil第3次执行---100W次转换耗时:4906
MapStruct第3次执行-----100W次转换耗时:1322
cn.hutool.core.bean.BeanUtil第4次执行---100W次转换耗时:3789
MapStruct第4次执行-----100W次转换耗时:1635
4.3 Spring BeanUtils 与 MapStruct比较
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
for (int i = 0; i < 5; i++) {
Long start = System.currentTimeMillis();
for (int i1 = 0; i1 < 1000000; i1++) {
Student student = Student.builder()
.name("小明")
.age(6)
.gender(GenderEnum.Male)
.height(121.1)
.birthday(new Date())
.build();
StudentVO studentVO = new StudentVO();
org.springframework.beans.BeanUtils.copyProperties(studentVO, student);
}
System.out.println("org.springframework.beans.BeanUtils第"+ i +"次执行---100W次转换耗时:" + (System.currentTimeMillis() - start));
Long start2 = System.currentTimeMillis();
for (int i1 = 0; i1 < 1000000; i1++) {
Student student = Student.builder()
.name("小明")
.age(6)
.gender(GenderEnum.Male)
.height(121.1)
.birthday(new Date())
.build();
StudentMapper.INSTANCE.student2StudentVO(student);
}
System.out.println("MapStruct第"+ i +"次执行-----100W次转换耗时:" + (System.currentTimeMillis() - start2));
}
}
//测试结果
org.springframework.beans.BeanUtils第0次执行---100W次转换耗时:1156
MapStruct第0次执行-----100W次转换耗时:3376
org.springframework.beans.BeanUtils第1次执行---100W次转换耗时:290
MapStruct第1次执行-----100W次转换耗时:1367
org.springframework.beans.BeanUtils第2次执行---100W次转换耗时:506
MapStruct第2次执行-----100W次转换耗时:1586
org.springframework.beans.BeanUtils第3次执行---100W次转换耗时:318
MapStruct第3次执行-----100W次转换耗时:1319
org.springframework.beans.BeanUtils第4次执行---100W次转换耗时:304
MapStruct第4次执行-----100W次转换耗时:1226
4.4 Cglib BeanCopier 与 MapStruct比较
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
for (int i = 0; i < 5; i++) {
Long start = System.currentTimeMillis();
for (int i1 = 0; i1 < 1000000; i1++) {
Student student = Student.builder()
.name("小明")
.age(6)
.gender(GenderEnum.Male)
.height(121.1)
.birthday(new Date())
.build();
StudentVO studentVO = new StudentVO();
org.springframework.cglib.beans.BeanCopier.create(StudentVO.class, Student.class, false);
}
System.out.println("org.springframework.cglib.beans.BeanCopier第"+ i +"次执行---100W次转换耗时:" + (System.currentTimeMillis() - start));
Long start2 = System.currentTimeMillis();
for (int i1 = 0; i1 < 1000000; i1++) {
Student student = Student.builder()
.name("小明")
.age(6)
.gender(GenderEnum.Male)
.height(121.1)
.birthday(new Date())
.build();
StudentMapper.INSTANCE.student2StudentVO(student);
}
System.out.println("MapStruct第"+ i +"次执行-----100W次转换耗时:" + (System.currentTimeMillis() - start2));
}
}
//测试结果
org.springframework.cglib.beans.BeanCopier第0次执行---100W次转换耗时:725
MapStruct第0次执行-----100W次转换耗时:4524
org.springframework.cglib.beans.BeanCopier第1次执行---100W次转换耗时:210
MapStruct第1次执行-----100W次转换耗时:1663
org.springframework.cglib.beans.BeanCopier第2次执行---100W次转换耗时:72
MapStruct第2次执行-----100W次转换耗时:1446
org.springframework.cglib.beans.BeanCopier第3次执行---100W次转换耗时:80
MapStruct第3次执行-----100W次转换耗时:1482
org.springframework.cglib.beans.BeanCopier第4次执行---100W次转换耗时:84
MapStruct第4次执行-----100W次转换耗时:1441
4.4 性能对比结果(5次平均值)
工具类 | 执行1000 | 执行1w | 执行10w | 执行100w |
---|---|---|---|---|
Apache BeanUtils | 103.2 | 323.3 | 1105.8 | 7227.8 |
Hutool BeanUtil | 111 | 249.2 | 925.4 | 4420.6 |
Spring BeanUtils | 80 | 93.4 | 206.4 | 728.6 |
Cglib BeanCopier | 86.4 | 103.8 | 103.8 | 263 |
MapStruct | 53.6 | 137.8 | 447.8 | 2672.2 |