1.背景
在我们日常开发的分层结构的应用程序中,为了各层之间互相解耦,一般都会定义不同的对象用来在不同层之间传递数据,因此,就有了各种 XXXDTO、XXXVO、XXXBO 等基于数据库对象派生出来的对象,当在不同层之间传输数据时,不可避免地经常需要将这些对象进行相互转换。
此时一般处理两种处理方式:
- ① 直接使用 Setter 和 Getter 方法转换
- ② 使用一些工具类进行转换(e.g. BeanUtil.copyProperties)。
第一种方式如果对象属性比较多时,需要写很多的 Getter/Setter 代码。第二种方式看起来虽然比第一种方式要简单很多,但是因为其使用了反射,性能不太好,而且在使用中也有很多陷阱。而今天要介绍的主角MapStruct 在不影响性能的情况下,同时解决了这两种方式存在的缺点。
2.mapstruct简介
mapstruct 是一种实体类 映射框架 ,能够通过Java注解 将一个实体类的属性安全地赋值给另一个实体类。它基于约定优于配置方法 极大地简化了 Java bean 类型之间映射的实现,有了mapstruct,只需要定义一个映射器接口,声明需要映射的方法,在编译过程中 ,mapstruct会自动生成该接口的实现类,实现将源对象映射到目标对象的效果。
总的来说,有如下三个特点:
- 基于注解
- 在编译期自动生成映射转换代码
- 类型安全、高性能、无依赖性
源码地址:https://github.com/mapstruct/mapstruct
3.mapstruct与其他映射对比
在第一章我们也提到,实体类映射框架大致有两种:
- 一种是运行期通过java反射机制 动态映射;
- 另一种是编译期动态生成getter/setter ,在运行期直接调用框架编译好的class类实现实体映射。
由于mapstruct映射是在编译期间实现的,因此相比运行期的映射框架有以下几个优点:
- 1.安全性高 。因为是编译期就实现源对象到目标对象的映射, 如果编译器能够通过,运行期就不会报错。
- 2.速度快 。速度快指的是运行期间直接调用实现类的方法,不会在运行期间使用反射进行转化。
4.mapstruct底层原理解析
mapstruct是基于JSR 269实现的,JSR 269是JDK引进的一种规范。有了它,能够实现在编译期处理注解,并且读取、修改和添加抽象语法树中的内容。
JSR 269使用Annotation Processor 在编译期间处理注解,Annotation Processor相当于编译器的一种插件,因此又称为插入式注解处理 。想要实现JSR 269,主要有以下几个步骤:
- 1.继承AbstractProcessor类 ,并且重写process方法,在process方法中实现自己的注解处理逻辑。
- 2.在META-INF/services目录下创建javax.annotation.processing.Processor文件注册自己实现的Annotation Processor
说到此处就不得不提一下Java程序编译的流程:
上图中Java源码到class文件的过程其实是一个比较复杂的过程。其中的经过可以用下图描述:
上图的流程可以概括为下面几个步骤:
- 1.生成抽象语法树 。Java编译器对Java源码进行编译,生成抽象语法树(Abstract Syntax Tree,AST)。
- 2.调用实现了JSR 269 API的程序 。只要程序实现了JSR 269 API,就会在编译期间调用实现的注解处理器。
- 3.修改抽象语法树 。在实现JSR 269 API的程序中,可以修改抽象语法树,插入自己的实现逻辑。
- 4.生成字节码 。修改完抽象语法树后,Java编译器会生成修改后的抽象语法树对应的字节码文件件。
5.具体使用和底层实现
5.1 添加maven依赖
<properties>
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
</properties>
<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>
5.2 对象转换
我们直接创建两个对象:
UserA,UserB,然后我们将AB进行互转
此时UserA和UserB有两种情景:
1.UserA和UserB字段相同
@Data
public class UserA {
private Integer id;
private String name;
}
创建对象转换器(Mapper)
需要注意的是,转换器不一定都要使用 Mapper 作为结尾,只是官方示例推荐以 XXXMapper 格式命名转换器名称,这里举例的是最简单的映射情况(字段名称和类型都完全匹配),只需要在转换器类上添加 @Mapper 注解即可,转换器代码如下所示:
@Mapper
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
UserB toUserB(UserA userA);
}
调用Mapper进行对象的转换
@SpringBootTest
class DemoApplicationTests {
@Test
void test1(){
Integer id = 1;
String name = "ninesun";
UserA userA = new UserA();
userA.setId(id);
userA.setName(name);
UserB userB = UserMapper.INSTANCE.toUserB(userA);
assertEquals(id, userB.getId());
assertEquals(name, userB.getName());
}
}
可以看到测试通过
编译之后,MapStruct注解处理器插件会识别出DoctorMapper接口并为其生成一个实现类。
,在Mapper上就可以看见其具体的实现
可以看到UserMapperImpl类中包含一个toUserB()方法,里面它帮我们实现了字段的映射,最终实现了UserA到UserB的转换
2.UserA和UserB字段不同
如果两者字段名称不同,MapStuct无法做到直接进行映射,需要将字段不一致的地方借助@Mappings进行对应
我们先创建一个对象UserC
@Data
public class UserC {
private Integer id;
private String userName;
}
和UserA相比发现name字段其实是无法映射的
只需要在Mapper里加入toUserC的方法即可
UserC toUserC(UserA userA);
进行一下测试
@Test
void test2(){
Integer id = 1;
String name = "ninesun";
UserA userA = new UserA();
userA.setId(id);
userA.setName(name);
UserC userC = UserMapper.INSTANCE.toUserC(userA);
assertEquals(id, userC.getId());
assertEquals(name, userC.getUserName());
}
可以发现UserC的name并没有转换成功,原因就是UserA中的name无法和UserBC中的userName字段进行映射
怎么办呢?
方法也很简单
@Mappings({
@Mapping(source = "name", target = "userName"),
})
UserC toUserC(UserA userA);
只需要我们手动去实现映射即可,再次允许Test2方法进行测试,可以发现测试可以通过
上面我们已经提到了@Mapping属性中的source和target,除此之外,他还有几个基本属性:
- ignore : 表示忽略映射当前字段
- true:忽略该字段
- false:不忽略,默认为false
- defaultValue 默认值
@Mapping(source = "UserA.specialty", target = "specialization", defaultValue = "Information Not Available")
- expressions 可以通过表达式来构造一些简单的转化关系
虽然设计的时候想兼容很多语言,不过目前只能写Java代码。
上面均是一对一的情况,即一个对象和一个对象之间的互转,多对一的场景我们一起看一下是什么样子的
3.多个源类
我们新增两个对象UserDto、Education
@Data
public class UserDto {
private Integer id;
private String name;
private String degree;
}
@Data
public class Education {
private String degreeName;
private String institute;
private Integer yearOfPassing;
}
我们接下来需要做的便是将UserA和Education中的属性值映射到UserDto中,对应的UserMapper中的内容如下:
@Mappings({
@Mapping(source = "userA.name", target = "userName"),
@Mapping(source = "education.degreeName", target = "degree"),
})
UserDto toUserDto(UserA userA, Education education);
接下来继续测试
@Test
void test5() {
Integer id = 1;
String name = "ninesun";
String degreeName = "博士";
UserA userA = new UserA();
userA.setId(id);
userA.setName(name);
Education education = new Education();
education.setDegreeName(degreeName);
UserDto userDto = UserMapper.INSTANCE.toUserDto(userA, education);
System.out.println(JSON.toJSONString(userDto));
}
4.子对象映射
多数情况下,POJO中不会只包含基本数据类型,其中往往会包含其它类。比如说,一个User类中会有多门课程:
@Data
public class UserA {
private Integer id;
private String name;
private List<Course> courseList;
}
@Data
public class Course {
private String name;
private Integer time;
}
@Data
public class UserDto {
private Integer id;
private String userName;
private String degree;
private List<CourseDto> courseDtoList;
}
@Data
public class CourseDto {
private String name;
private Integer time;
}
对应的Mapper如下:
@Mappings({
@Mapping(source = "name", target = "userName"),
@Mapping(source = "courseList", target = "courseDtoList"),
})
UserDto toUserDto(UserA userA);
写个单元测试进行测试:
@Test
void test6() {
Integer id = 1;
String name = "ninesun";
UserA userA = new UserA();
userA.setId(id);
userA.setName(name);
Course course = new Course();
course.setName("高等数学");
course.setTime(12);
List<Course> courseList = Arrays.asList(course);
userA.setCourseList(courseList);
UserDto userDto = UserMapper.INSTANCE.toUserDto(userA);
System.out.println(JSON.toJSONString(userDto));
}
5.数据类型转换
数据类型映射
MapStruct支持source和target属性之间的数据类型转换。它还提供了基本类型及其相应的包装类之间的自动转换。
自动类型转换适用于:
- 基本类型及其对应的包装类之间 。比如, int 和 Integer, float 和 Float, long 和 Long,boolean 和 Boolean 等。
- 任意基本类型与任意包装类之间 。如 int 和 long, byte 和 Integer 等。
- 所有基本类型及包装类与String之间 。如 boolean 和 String, Integer 和 String, float 和 String 等。
- 枚举和String之间 。
- Java大数类型 (java.math.BigInteger, java.math.BigDecimal) 和Java基本类型(包括其包装类)与String之间。
- 其它情况详见MapStruct官方文档
因此,在生成映射器代码的过程中,如果源字段和目标字段之间属于上述任何一种情况,则MapStrcut会自行处理类型转换。
我们修改 UserA ,新增一个birthdate字段:
@Data
public class UserA {
private Integer id;
private String name;
private LocalDate birthdate;
private List<Course> courseList;
}
UserDto中增加一个String类型的birthdate;
@Data
public class UserDto {
private Integer id;
private String userName;
private String degree;
private String birthdate;
private List<CourseDto> courseDtoList;
}
Mapper映射
@Mappings({
@Mapping(source = "name", target = "userName"),
@Mapping(source = "courseList", target = "courseDtoList"),
@Mapping(source = "birthdate", target = "birthdate", dateFormat = "dd/MMM/yyyy"),
})
UserDto toUserDto(UserA userA);
测试:
@Test
void test7() {
Integer id = 1;
String name = "ninesun";
UserA userA = new UserA();
userA.setId(id);
userA.setName(name);
userA.setBirthdate(LocalDate.now());
Course course = new Course();
course.setName("高等数学");
course.setTime(12);
List<Course> courseList = Arrays.asList(course);
userA.setCourseList(courseList);
UserDto userDto = UserMapper.INSTANCE.toUserDto(userA);
System.out.println(JSON.toJSONString(userDto));
}
除此之外,对于数字的转换,也可以使用numberFormat指定显示格式:
// 数字格式转换示例
@Mapping(source = "price", target = "price", numberFormat = "$#.00")
枚举映射
枚举映射的工作方式与字段映射相同。MapStruct会对具有相同名称的枚举进行映射,这一点没有问题。但是,对于具有不同名称的枚举项,我们需要使用@ValueMapping注解 。同样,这与普通类型的@Mapping注解也相似。
我们先创建两个枚举。
public enum PaymentType {
CASH,
CHEQUE,
CARD_VISA,
CARD_MASTER,
CARD_CREDIT
}
public enum PaymentTypeView {
CASH,
CHEQUE,
CARD
}
单元测试:
@Test
void test8() {
PaymentType paymentType1=PaymentType.CASH;
PaymentType paymentType2=PaymentType.CARD_VISA;
PaymentTypeView paymentTypeView1=PaymentTypeMapper.INSTANCE.paymentTypeToPaymentTypeView(paymentType1);
PaymentTypeView paymentTypeView2=PaymentTypeMapper.INSTANCE.paymentTypeToPaymentTypeView(paymentType2);
System.out.println(paymentTypeView1);
System.out.println(paymentTypeView2);
}
但是,如果你要将很多值转换为一个更一般的值,这种方式就有些不切实际了。其实我们不必手动分配每一个值,只需要让MapStruct将所有剩余的可用枚举项(在目标枚举中找不到相同名称的枚举项),直接转换为对应的另一个枚举项。
可以通过 MappingConstants实现这一点:
@ValueMapping(source = MappingConstants.ANY_REMAINING, target = "CARD")
PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);
还有一种选择是使用ANY UNMAPPED :
@ValueMapping(source = MappingConstants.ANY_UNMAPPED, target = "CARD")
PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);
采用这种方式时,MapStruct不会像前面那样先处理默认映射,再将剩余的枚举项映射到target值。而是,直接将所有未通过@ValueMapping注解做显式映射的值都转换为target值。
6.集合映射
使用MapStruct处理集合映射的方式与处理简单类型相同
MapStruct将根据我们的声明自动生成映射代码。 通常,生成的代码会遍历源集合,将每个元素转换为目标类型,并将每个转换后元素添加到目标集合中。
List映射
如果我们的Mapper中饭只有下面这一个方法
List<UserDto> map(List<UserA> userAList);
测试:
@Test
void test9() {
Integer id = 1;
String name = "ninesun";
UserA userA = new UserA();
userA.setId(id);
userA.setName(name);
userA.setBirthdate(LocalDate.now());
Course course = new Course();
course.setName("高等数学");
course.setTime(12);
List<Course> courseList = Arrays.asList(course);
userA.setCourseList(courseList);
List<UserA> userAList = Arrays.asList(userA);
List<UserDto> userDtoList = UserMapper.INSTANCE.map(userAList);
System.out.println(JSON.toJSONString(userDtoList));
}
可以发现字段名称不一致的均未转换,这个时候我们把实体和实体之间的字段映射加进去
@Mapper
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
@Mappings({
@Mapping(source = "name", target = "userName"),
@Mapping(source = "courseList", target = "courseDtoList"),
@Mapping(source = "birthdate", target = "birthdate", dateFormat = "dd/MMM/yyyy"),
})
UserDto toUserDto(UserA userA);
List<UserDto> map(List<UserA> userAList);
}
再次运行刚刚的测试用例,可以发现所有的字段映射成功
Set和Map映射
Set与Map型数据的处理方式与List相似。按照以下方式修改UserMapper:
@Mapper
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
Set<UserDto> setConvert(Set<UserA> doctor);
Map<String, UserDto> mapConvert(Map<String, UserA> doctor);
}
同样,我们如果只要这些映射,字段名称不同的仍然无法映射,所以同样需要加入
@Mappings({
@Mapping(source = "name", target = "userName"),
@Mapping(source = "courseList", target = "courseDtoList"),
@Mapping(source = "birthdate", target = "birthdate", dateFormat = "dd/MMM/yyyy"),
})
UserDto toUserDto(UserA userA);
来处理单个对象字段不一致的情况
集合映射策略
@Data
public class UserA {
private Integer id;
private String name;
private LocalDate birthdate;
private List<Course> courseList;
}
@Data
public class UserDto {
private Integer id;
private String userName;
private String degree;
private String birthdate;
private List<CourseDto> courseDtoList;
public void setCourseDtoList(List<CourseDto> courseDtoList) {
this.courseDtoList = courseDtoList;
}
public void addCourseDto(CourseDto courseDto) {
if (courseDtoList == null) {
courseDtoList = new ArrayList<>();
}
courseDtoList.add(courseDto);
}
}
可以发现UserDto中多了一个set方法和add方法
@Mappings({
@Mapping(source = "name", target = "userName"),
@Mapping(source = "courseList", target = "courseDtoList"),
@Mapping(source = "birthdate", target = "birthdate", dateFormat = "dd/MMM/yyyy"),
})
UserDto toUserDto(UserA userA);
编译之后的实现类:
可以看到,在默认情况下采用的策略是ACCESSOR_ONLY ,使用setter方法setCourseDtoList()向UserDto对象中写入列表数据。
相对的,如果使用ADDER_PREFERRED 作为映射策略:
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
public interface UserMapper {}
此时,会使用adder方法逐个将转换后的子类型DTO对象加入父类型的集合字段中。
如果目标DTO中既没有setter方法也没有adder方法,会先通过getter方法获取子类型集合,再调用集合的对应接口添加子类型对象。
5.3 进阶操作
1.依赖注入
到目前为止,我们一直在通过getMapper()方法访问生成的映射器:
但是,如果你使用的是Spring,只需要简单修改映射器配置,就可以像常规依赖项一样注入映射器。
@Mapper(componentModel = "spring")
public interface UserMapper {}
在@Mapper注解中添加(componentModel = “spring”),是为了告诉MapStruct,在生成映射器实现类时,我们希望它能支持通过Spring的依赖注入来创建。现在,就不需要在接口中添加 INSTANCE 字段了。
此时,生成的UserMapperImpl中会带有 @Component 注解:
只要被标记为@Component,Spring就可以把它作为一个bean来处理,你就可以在其它类(如控制器)中通过@Autowire或@Resourece注解来使用它:
@SpringBootTest
class DemoApplicationTests {
@Autowired
UserMapper userMapper;
@Test
void test1() {
Integer id = 1;
String name = "ninesun";
UserA userA = new UserA();
userA.setId(id);
userA.setName(name);
UserB userB = userMapper.toUserB(userA);
assertEquals(id, userB.getId());
assertEquals(name, userB.getName());
}
}
即使你不使用Spring框架(PS:我估计现在使用Java的同学们应该没有不使用Spring的了吧),MapStruct也支持Java CDI:
@Mapper(componentModel = "cdi")
public interface UserMapper {}
2.默认值
@Mapping 注解有两个很实用的标志就是常量 constant 和默认值 defaultValue 。无论source如何取值,都将始终使用常量值; 如果source取值为null,则会使用默认值。
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mappings({
@Mapping(target = "id",constant = "-1"),
@Mapping(source = "courseList", target = "courseDtoList"),
@Mapping(source = "birthdate", target = "birthdate", dateFormat = "dd/MMM/yyyy"),
@Mapping(target = "name", defaultValue = "Information Not Available"),
})
UserDto toUserDto(UserA userA);
}
如果name不可用,我们会替换为"Information Not Available"字符串,此外,我们将id硬编码为-1。
3.添加表达式
MapStruct甚至允许在@Mapping注解中输入Java表达式。你可以设置 defaultExpression ( source 取值为 null时生效),或者一个expression(类似常量,永久生效)。
@Mappings({
@Mapping(source = "userA.name", target = "userName"),
@Mapping(source = "education.degreeName", target = "degree"),
@Mapping(source = "userA.birthdate", target = "birthdate", defaultExpression = "java(LocalDateTime.now())")
})
UserDto toUserDto(UserA userA, Education education);
编译完成之后,可以发现:
虽然已经转换成我们想要的样子,但是,会发现LocalDate没有被引入,在我们本地开发时,编译之后我们可以手动引入,但是编译成jar或者部署到服务器上的时候就无法手动去添加,此时我们可以事先在 @Mapper 中添加 imports = {LocalDate.class} 。
再次编译,就可以发现已经正常了
4.添加自定义方法
到目前为止,我们一直使用的策略是添加一个“占位符”方法,并期望MapStruct能为我们实现它。其实我们还可以向接口中添加自定义的default方法,也可以通过default方法直接实现一个映射。然后我们可以通过实例直接调用该方法,没有任何问题。
接下来我们创建一个UserCourseSummary类
@Data
@Builder
public class UserCourseSummary {
private Integer userId;
private String userName;
private Integer courseCount;
private List<String> courseNames;
private Integer courseTime;
}
在UserMapper中添加:
default UserCourseSummary toUserCourseSummary(UserA userA, Course course) {
return UserCourseSummary.builder()
.userId(userA.getId())
.userName(userA.getName())
.courseCount(userA.getCourseList().size())
.courseNames(userA.getCourseList()
.stream()
.map(Course::getName)
.collect(Collectors.toList()))
.courseTime(course.getTime())
.build();
}
后面我们则可以通过这个方法直接去映射UserCourseSummary
@Test
void test10() {
Integer id = 1;
String name = "ninesun";
UserA userA = new UserA();
userA.setId(id);
userA.setName(name);
userA.setBirthdate(LocalDate.now());
Course course = new Course();
course.setName("高等数学");
course.setTime(12);
List<Course> courseList = Arrays.asList(course);
userA.setCourseList(courseList);
UserCourseSummary userCourseSummary = userMapper.toUserCourseSummary(userA, course);
System.out.println(JSON.toJSONString(userCourseSummary));
}
5.创建自定义映射器
前面我们一直是通过接口来设计映射器功能,其实我们也可以通过一个带@Mapper 的 abstract 类来实现一个映射器。MapStruct也会为这个类创建一个实现,类似于创建一个接口实现。
@Mapper(componentModel = "spring")
public abstract class UserCustomMapper {
public UserCourseSummary toUserCourseSummary(UserA userA, Course course) {
return UserCourseSummary.builder()
.userId(userA.getId())
.userName(userA.getName())
.courseCount(userA.getCourseList().size())
.courseNames(userA.getCourseList()
.stream()
.map(Course::getName)
.collect(Collectors.toList()))
.courseTime(course.getTime())
.build();
}
}
@BeforeMapping 和 @AfterMapping
为了进一步控制和定制化,我们可以定义@BeforeMapping 和 @AfterMapping方法。显然,这两个方法是在每次映射之前和之后执行的。也就是说,在最终的实现代码中,会在两个对象真正映射之前和之后添加并执行这两个方法。
可以在UserCustomMapper中添加两个方法:
@BeforeMapping
protected void validate(UserA user) {
if(CollectionUtils.isEmpty(user.getCourseList())){
user.setCourseList(new ArrayList<>());
}
}
@AfterMapping
protected void updateResult(@MappingTarget UserDto userDto) {
userDto.setUserName(userDto.getUserName().toUpperCase());
userDto.setDegree(userDto.getDegree().toUpperCase());
}
并新增一个user->UserDto的映射
@Mapping(source = "userA.name", target = "userName")
public abstract UserDto toDoctorDto(UserA userA);
编译之后:
可以看到, validate() 方法会在 DoctorDto 对象实例化之前执行,而updateResult()方法会在映射结束之后执行。
6.映射异常处理
异常处理是不可避免的,应用程序随时会产生异常状态。MapStruct提供了对异常处理的支持,可以简化开发者的工作。
考虑这样一个场景,我们想在 User 映射为UserDto之前校验一下 User 的数据。我们新建一个独立的 Validator 类进行校验:
@Component
public class Validator {
public int validateId(int id) throws ValidationException {
if(id == -1){
throw new ValidationException("Invalid value in ID");
}
return id;
}
}
并修改userMapper
@Mapper(componentModel = "spring", uses = {Validator.class},imports = {LocalDate.class,})
public interface UserMapper {
@Mappings({
@Mapping(target = "id", constant = "-1"),
@Mapping(source = "courseList", target = "courseDtoList"),
@Mapping(source = "birthdate", target = "birthdate", dateFormat = "dd/MMM/yyyy"),
@Mapping(source = "name", target = "userName", defaultValue = "Information Not Available"),
})
UserDto toUserDto(UserA userA);
}
再次重新编译:
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2023-11-03T14:13:15+0800",
comments = "version: 1.5.5.Final, compiler: javac, environment: Java 1.8.0_371 (Oracle Corporation)"
)
@Component
public class UserMapperImpl implements UserMapper {
@Autowired
private Validator validator;
@Override
public UserDto toUserDto(UserA userA) {
if ( userA == null ) {
return null;
}
UserDto userDto = new UserDto();
userDto.setCourseDtoList( courseListToCourseDtoList( userA.getCourseList() ) );
userDto.setBirthdate( userA.getBirthdate() );
if ( userA.getName() != null ) {
userDto.setUserName( userA.getName() );
}
else {
userDto.setUserName( "Information Not Available" );
}
try {
userDto.setId( validator.validateId( -1 ) );
}
catch ( ValidationException e ) {
throw new RuntimeException( e );
}
return userDto;
}
}
可以发现编译之后对于参数的校验也是十分的的优雅
值得我们注意的是:如果映射前后的一对属性的类型与Validator中的方法出入参类型一致,那该字段映射时就会调用Validator中的方法,所以该方式请谨慎使用。
7.映射配置
MapStruct为编写映射器方法提供了一些非常有用的配置。多数情况下,如果我们已经定义了两个类型之间的映射方法,当我们要添加相同类型之间的另一个映射方法时,我们往往会直接复制已有方法的映射配置。
其实我们不必手动复制这些注解,只需要简单的配置就可以创建一个相同/相似的映射方法。
7.1 继承配置
现在我们看一下这个场景:
我们创建了一个映射器,根据User对象的属性更新现有的UserDto对象的属性值
@Mapping(source = "userName", target = "name")
void updateModel(UserA userA, @MappingTarget UserDto userDto);
当然我们还有另一中方式将UserDto转换为User:
@Mapping(source = "name", target = "userName", defaultValue = "Information Not Available")
UserDto toUserDto(UserA userA);
这两个映射方法使用了相同的注解配置, source和 target都是相同的。其实我们可以使用@InheritConfiguration注释,从而避免这两个映射器方法的重复配置。
如果对一个方法添加 @InheritConfiguration 注解,MapStruct会检索其它的已配置方法,寻找可用于当前方法的注解配置。一般来说,这个注解都用于mapping方法后面的update方法,如下所示:
@InheritConfiguration
void updateModel(UserA userA, @MappingTarget UserDto userDto);
同样,我们编译之后,可以看出两者实现一致,编译之后的代码过长,不在给出,自己可以build之后自行查看
7.2 继承逆向配置
还有另外一个类似的场景,就是编写映射函数将Model 转为 DTO,以及将 DTO 转为 Model。如下面的代码所示,我们必须在两个函数上添加相同的注释。
@Mappings({
@Mapping(source = "name", target = "userName"),
})
UserC toUserC(UserA userA);
@Mappings({
@Mapping(source = "userName", target = "name"),
})
UserA toUserA(UserC userC);
两个方法的配置不会是完全相同的,实际上,它们应该是相反的。将Model 转为 DTO,以及将 DTO 转为 Model——映射前后的字段相同,但是源属性字段与目标属性字段是相反的。
我们可以在第二个方法上使用@InheritInverseConfiguration 注解,避免写两遍映射配置:
@Mappings({
@Mapping(source = "name", target = "userName"),
})
UserC toUserC(UserA userA);
@InheritInverseConfiguration
UserA toUserA(UserC userC);
编译之后:
6.对象与Map的转换
我们可以利用MapStruct工具轻松的实现对象之间的互转,也可以轻松实现Map到对象的转换
6.1 Map转对象
如下所示:
UserA map2User(Map<String, String> map);
编译之后提示我们:
这句话的意思是String类型无法转化为List类型的Course对象,此时,我们需要借助default方法,去将它转化一下:
在UserMapper中添加:
UserA map2User(Map<String, String> map);
default List<Course> str2List(String str) {
return JSON.parseArray(str, Course.class);
}
再次编译,可以看出:
6.2 对象转Map
在UserMapper中添加
Map<String, String> user2String(UserA userA);
编译之后,发现:
这个错误提示我们返回的对象不能是一个抽象类或接口,需要返回一个具体的实例化对象,那我们简单修改一下:
HashMap<String, String> user2String(UserA userA);
再次编译,发现虽然不报错,但是其具体的实现如下:
相当于什么都没做,就返回了一个空的HashMap,那我们该怎么去做,才能轻松优雅的实现对象转化为Map呢?
当然,实现的方式远远不止一种,下面我把常见的几种给大家写一下,自己看着选择一种即可
- 实现方式1:利用ObjectMapper实现
default Map<String, String> toMap1(Object object) {
if (Objects.isNull(object)) {
return new HashMap<>();
}
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(new JavaTimeModule());
Map<String, String> targetMap = objectMapper.convertValue(object, HashMap.class);
return targetMap;
}
- 实现方式2:利用反射实现,像BeanUtils中的Map转换底层便是利用反射机制
default Map<String, String> toMap2(Object object) {
Map<String, String> map = new HashMap<>(36);
ReflectionUtils.doWithFields(object.getClass(), field -> {
field.setAccessible(true);
// 不处理自动化测试的插码字段
if (field.isSynthetic()) {
return;
}
Object fieldValue = null;
try {
fieldValue = field.get(object);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
if (null != fieldValue) {
if (String.class.equals(field.getType())) {
map.put(field.getName(), fieldValue.toString());
} else {
map.put(field.getName(), JSON.toJSONString(fieldValue));
}
}
});
return map;
}
- 实现方式3:利用JSON序列化实现
default Map<String, String> toMap3(Object object) {
if (Objects.isNull(object)) {
return new HashMap<>();
}
String str = JSON.toJSONString(object);
Map<String, String> map = JSON.parseObject(str, HashMap.class);
return map;
}
每种方式均已自测,大家可以自行尝试
6.总结
在本文中,我们探讨了MapStruct——一个用于创建映射器类的库。从基本映射到自定义方法和自定义映射器,此外, 我们还介绍了MapStruct提供的一些高级操作选项,包括依赖注入,数据类型映射、枚举映射和表达式使用。
MapStruct提供了一个功能强大的集成插件,可减少开发人员编写模板代码的工作量,使创建映射器的过程变得简单快捷。
如果想知道更多关于该工具类的使用和操作,可移步:MapStruct官方提供的参考指南