按照日常开发习惯,对于不同领域层使用不同JavaBean对象传输数据,避免相互影响,因此基于数据库实体对象User衍生出比如UserDto、UserVo等对象,于是在不同层之间进行数据传输时,不可避免地需要将这些对象进行互相转换操作。
常见的转换方式有:
调用getter/setter方法进行属性赋值
调用BeanUtil.copyPropertie进行反射属性赋值
第一种方式不必说,属性多了就需要写一大坨getter/setter代码。第二种方式比第一种方式要简便很多,但是坑巨多,比如sources与target写反,难以定位某个字段在哪里进行的赋值,同时因为用到反射,导致性能也不佳,
BeanUtils, BeanCopier 等在使用反射的时候都会影响到性能。
Apache的BeanUtils和Spring``BeanUtils,BeanCopier 的分析:
https://www.toutiao.com/i6800547642263732743/?tt_from=weixin&utm_campaign=client_share&wxshare_count=1×tamp=1592800960&app=news_article&utm_source=weixin&utm_medium=toutiao_ios&req_id=202006221242400100260770730F08BE20&group_id=6800547642263732743
鉴于此,今天写一写第三种对象转换方式,本文使用的是 MapStruct 工具进行转换,MapStruct 原理也很简单,就是在代码编译阶段生成对应的赋值代码,底层原理还是调用getter/setter方法,但是这是由工具替我们完成,MapStruct在不影响性能的情况下,解决了前面两种方式弊端,很赞~
依赖包
首先需要把依赖包导入,主要由两个包组成:
org.mapstruct:mapstruct:包含了一些必要的注解,例如@Mapping。r若我们使用的JDK版本高于1.8,当我们在pom里面导入依赖时候,建议使用坐标是:org.mapstruct:mapstruct-jdk8,这可以帮助我们利用一些Java8的新特性。
org.mapstruct:mapstruct-processor:注解处理器,根据注解自动生成mapper的实现。
<dependency>
<groupId>org.mapstruct</groupId>
<!-- jdk8以下就使用mapstruct -->
<artifactId>mapstruct-jdk8</artifactId>
<version>1.2.0.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.2.0.Final</version>
</dependency>
2.1、定义实体类以及被映射类
// 实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
private Integer id;
private String name;
private String createTime;
private LocalDateTime updateTime;
}
// 被映射类VO1:和实体类一模一样
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserVO1 {
private Integer id;
private String name;
private String createTime;
private LocalDateTime updateTime;
}
// 被映射类VO1:比实体类少一个字段
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserVO2 {
private Integer id;
private String name;
private String createTime;
}
2.2、定义接口:
当实体类和被映射对象属性相同或者被映射对象属性值少几个时:
@Mapper(componentModel = "spring")
public interface UserCovertBasic {
UserCovertBasic INSTANCE = Mappers.getMapper(UserCovertBasic.class);
/**
* 字段数量类型数量相同,利用工具BeanUtils也可以实现类似效果
* @param source
* @return
*/
UserVO1 toConvertVO1(User source);
User fromConvertEntity1(UserVO1 userVO1);
/**
* 字段数量类型相同,数量少:仅能让多的转换成少的,故没有fromConvertEntity2
* @param source
* @return
*/
UserVO2 toConvertVO2(User source);
}
从上面的代码可以看出:接口中声明了一个成员变量INSTANCE,母的是让客户端可以访问 Mapper 接口的实现。
2.3、使用
@RestController
public class TestController {
@GetMapping("convert")
public Object convertEntity() {
User user = User.builder()
.id(1)
.name("张三")
.createTime("2020-04-01 11:05:07")
.updateTime(LocalDateTime.now())
.build();
List<Object> objectList = new ArrayList<>();
objectList.add(user);
// 使用mapstruct
UserVO1 userVO1 = UserCovertBasic.INSTANCE.toConvertVO1(user);
objectList.add("userVO1:" + UserCovertBasic.INSTANCE.toConvertVO1(user));
objectList.add("userVO1转换回实体类user:" + UserCovertBasic.INSTANCE.fromConvertEntity1(userVO1));
// 输出转换结果
objectList.add("userVO2:" + " | " + UserCovertBasic.INSTANCE.toConvertVO2(user));
// 使用BeanUtils
UserVO2 userVO22 = new UserVO2();
BeanUtils.copyProperties(user, userVO22);
objectList.add("userVO22:" + " | " + userVO22);
return objectList;
}
}
2.4、查看编译结果
通过IDE的反编译功能查看编译后自动生成 UserCovertBasic 的实现类 UserCovertBasicImpl ,内容如下:
@Component
public class UserCovertBasicImpl implements UserCovertBasic {
public UserCovertBasicImpl() {
}
public UserVO1 toConvertVO1(User source) {
if (source == null) {
return null;
} else {
UserVO1 userVO1 = new UserVO1();
userVO1.setId(source.getId());
userVO1.setName(source.getName());
userVO1.setCreateTime(source.getCreateTime());
userVO1.setUpdateTime(source.getUpdateTime());
return userVO1;
}
}
public User fromConvertEntity1(UserVO1 userVO1) {
if (userVO1 == null) {
return null;
} else {
User user = new User();
user.setId(userVO1.getId());
user.setName(userVO1.getName());
user.setCreateTime(userVO1.getCreateTime());
user.setUpdateTime(userVO1.getUpdateTime());
return user;
}
}
public UserVO2 toConvertVO2(User source) {
if (source == null) {
return null;
} else {
UserVO2 userVO2 = new UserVO2();
userVO2.setId(source.getId());
userVO2.setName(source.getName());
userVO2.setCreateTime(source.getCreateTime());
return userVO2;
}
}
}
2.5、浏览器查看结果
好了,一个流程就走完了,是不是感觉贼简单呢?
而且呀,温馨提醒:
如果是要转换一个集合的话,只需要把这里的实体类换成集合就行了,例如:
List<UserVO1> toConvertVOList(List<User> source);
三、不简单的情况
3.1、类型不一致
实体类我们还是沿用 User;被映射对象 UserVO3 改为:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserVO3 {
private String id;
private String name;
// 实体类该属性是String
private LocalDateTime createTime;
// 实体类该属性是LocalDateTime
private String updateTime;
}
那么我们定义的接口就要稍稍修改一下了:
@Mappings({
@Mapping(target = "createTime", expression = "java(com.java.mmzsblog.util.DateTransform.strToDate(source.getCreateTime()))"),
})
UserVO3 toConvertVO3(User source);
User fromConvertEntity3(UserVO3 userVO3);
上面 expression 指定的表达式内容如下:
public class DateTransform {
public static LocalDateTime strToDate(String str){
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyy-MM-dd HH:mm:ss");
return LocalDateTime.parse("2018-01-12 17:07:05",df);
}
}
通过IDE的反编译功能查看编译后的实现类,结果是这样子的:
从图中我们可以看到,编译时使用了expression中定义的表达式对目标字段 createTime 进行了转换;然后你还会发现 updateTime 字段也被自动从 LocalDateTime 类型转换成了 String 类型。
小结:
当字段类型不一致时,以下的类型之间是 mapstruct 自动进行类型转换的:
1、基本类型及其他们对应的包装类型。
此时 mapstruct 会自动进行拆装箱。不需要人为的处理
2、基本类型的包装类型和string类型之间
除此之外的类型转换我们可以通过定义表达式来进行指定转换。
3.2、字段名不一致
实体类我们还是沿用 User;被映射对象 UserVO4 改为:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserVO4 {
// 实体类该属性名是id
private String userId;
// 实体类该属性名是name
private String userName;
private String createTime;
private String updateTime;
}
那么我们定义的接口就要稍稍修改一下了:
@Mappings({
@Mapping(source = "id", target = "userId"),
@Mapping(source = "name", target = "userName")
})
UserVO4 toConvertVO(User source);
User fromConvertEntity(UserVO4 userVO4);
通过IDE的反编译功能查看编译后的实现类,编译后的结果是这样子的:
很明显, mapstruct 通过读取我们配置的字段名对应关系,帮我们把它们赋值在了相对应的位置上,可以说是相当优秀了,但这也仅仅是优秀,而更秀的还请继续往下看:
小结:
当字段名不一致时,通过使用 @Mappings 注解指定对应关系,编译后即可实现对应字段的赋值。
3.3、属性是枚举类型
实体类我们还是改用 UserEnum:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserEnum {
private Integer id;
private String name;
private UserTypeEnum userTypeEnum;
}
被映射对象 UserVO5 改为:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserVO5 {
private Integer id;
private String name;
private String type;
}
枚举对象是:
@Getter
@AllArgsConstructor
public enum UserTypeEnum {
Java("000", "Java开发工程师"),
DB("001", "数据库管理员"),
LINUX("002", "Linux运维员");
private String value;
private String title;
}
那么我们定义的接口还是照常定义,不会受到它是枚举就有所变化:
@Mapping(source = "userTypeEnum", target = "type")
UserVO5 toConvertVO5(UserEnum source);
UserEnum fromConvertEntity5(UserVO5 userVO5);
通过IDE的反编译功能查看编译后的实现类,编译后的结果是这样子的:
很明显, mapstruct 通过枚举类型的内容,帮我们把枚举类型转换成字符串,并给type赋值