前言
工作中常常出现的一种情况是,我们需要把Entity/PO/DTO/VO/QueryParam之间做转换,解决这类问题的工具有很多,如Orika、BeanUtils、Hutool工具包,为何对MapStrucet情有独钟,用来单独推荐呢?
简介
MapSturct 是一个生成类型安全, 高性能且无依赖的 JavaBean 映射代码的注解处理器
怎么理解呢,对于BeanUtils来说,映射主要是靠反射来实现,当有大量的拷贝时,意味着大量的使用了反射,效率相对低下,就连《阿里巴巴开发手册》中也明确提到,不准使用BeanUtils
众所周知,效率最快的当然是手写的get()、set()
,当然开发效率也是最慢的,而MapStruct通过编译器编译生成常规的方法,我们通过写接口和注解就可以手动帮我们生成get()、set()
代码,效率不知提高了多少倍
具体性能测试已经有人做过了,可以参考这篇文章:5种常见Bean映射工具的性能比对
示例
首先引入MapStruct的依赖
Maven:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.2.Final</version>
</dependency>
在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>1.4.2.Final</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
现有两个实体类
@Data
public class Student {
String name;
Integer age;
String idCard;
Date birthDay;
}
@Data
public class StudentVO {
String name;
String birthDay;
String idCard;
Integer studentAge;
}
场景1:单个对象之间的映射 / 批量映射
按照需求,我们要把Student对象转换为StudentVO对象,其中student.age
要映射到studentVO.studentAge
中,而 student.idCard
在这次查询中无需显示,student.birthDay
格式化成字符串再传入studentVO.bidthDay
中
我们只需要创建一个接口:
@Component
@Mapper(componentModel = "spring")
public interface StudentConverter {
@Mapping(target = "studentAge", source = "age")
@Mapping(target = "idCard", ignore = true)
@Mapping(target = "birthDay", dateFormat = "yyyy-MM-dd HH:mm:ss")
StudentVO studentToStudentVO(Student student);
List<StudentVO> studentToStudentVO(List<Student> students);
}
使用:
@RestController
public class TestController {
@Autowired
private StudentConverter studentConverter;
@GetMapping("/test")
public void beanConvertTest() {
Student student = new Student();
student.setName("name");
student.setAge(18);
student.setIdCard("123456");
student.setBirthDay(new Date());
StudentVO studentVO = studentConverter.studentToStudentVO(student);
System.out.println(studentVO);
List<Student> students = Collections.singletonList(student);
List<StudentVO> studentVOS = studentConverter.studentToStudentVO(students);
System.out.println(studentVOS);
}
}
输出结果:
可以看到出生日期被成功被格式化成我们想要的字符串,idCard
我们不需要于是没有被映射
场景2:多个对象映射为一个
有时候会出现要将多个对象映射为一个对象的情况,而多个对象之间可能有重复的字段,这个根据@Mapping
注解就能灵活解决
现在新增一个类Address.class
,其中name
字段和student.name
是重复字段
@Data
public class Address {
String name;
String address;
}
需求是在场景1的基础上,将address
字段加到StudentVO.class
中,但是name
字段得用Student.class
的。
@Data
public class StudentVO {
String name;
String birthDay;
String idCard;
Integer studentAge;
//新增字段
String address;
}
我们可以通过source
参数来设置需要映射的字段来自于哪个实例,接口方法如下:
@Mapping(target = "studentAge", source = "student.age")
@Mapping(target = "idCard", ignore = true)
@Mapping(target = "birthDay", source = "student.birthDay", dateFormat = "yyyy-MM-dd HH:mm:ss")
@Mapping(target = "name", source = "student.name")
StudentVO studentAndAddressToVO(Student student, Address address);
使用:
@GetMapping("/test")
public void beanConvertTest() {
Student student = new Student();
student.setName("name");
student.setAge(18);
student.setIdCard("123456");
student.setBirthDay(new Date());
Address address = new Address();
address.setName("addressName");
address.setAddress("address");
StudentVO studentVO = studentConverter.studentAndAddressToVO(student, address);
System.out.println(studentVO);
}
输出结果:
对于上面的例子,需要注意的是,多个对象映射为一个对象时,只有重复的字段,或者字段名不一样的字段进行映射,才需要用注解告诉MapStruct,到底要使用哪个来源的字段
更多更复杂的用法可以参考官方示例仓库,非常齐全:mapstruct/mapstruct-examples: Examples for using MapStruct (github.com)
其他注意事项
- 如果项目中也同时使用到了 Lombok,一定要注意 Lombok的版本要等于或者高于1.18.10,否则会有编译不通过的情况发生
- 如果接口的存放包名为mapper,可能与Mybatis冲突会导致项目起不来
- 当两个对象属性不一致时,比如
Student
对象中某个字段不存在于StudentVO
当中时,在编译时会有警告提示,可以在@Mapping
中配置ignore = true
,当字段较多时,可以直接在@Mapper
中设置unmappedTargetPolicy
属性或者unmappedSourcePolicy
属性为ReportingPolicy.IGNORE
即可
总结:应该如何选择对象映射工具
- 若是字段少,写起来也不麻烦,就没必要用框架了,手写
get()/set()
就好了,技术不应该为用而用,应该为了解决对应的问题而用 - 若是字段多,转换不频繁,为省事就用
BeanUtils
吧,稍微复杂点的场景也可以使用 Hutool工具包的BeanUtils
,提供的拷贝选项要多一些 - 字段多又转换频繁,从性能方面考虑,还是从高性能的工具中选一个用吧,比如本文推荐的MapStruct