背景
最近在欣赏其他项目的代码时,偶然发现一个MapStruct 的使用案例,一见之下,颇为欣喜。回头再看看我们团队那群卧龙凤雏写的代码,感觉简直是在玷污我的眼睛,我决定在我们团队推广使用MapStruct,而在做出这个决定之前,我还有一项无比艰巨的任务——说服团队的胡大佬。于是乎,就有了此次的华山论 MapStruct 之争。
MapStruct介绍
MapStruct 是一个用于 Java bean 映射的代码生成器。它提供了一个注解处理器,可以根据接口定义自动生成 Java bean 映射代码。它能够自动生成类型转换器,从而使得在不同类型之间进行转换变得简单,而无需程序员编写冗长、重复的映射代码。它支持自定义类型转换器,可以自动生成类型转换代码,从而减少手动编写代码的工作量。
MapStruct 的使用非常简单,只需要定义一个接口,然后使用注解指定需要转换的类型和属性。然后,MapStruct 会生成一个实现该接口的类,该类将自动执行类型转换。
现如今,分层结构的服务架构(如领域驱动模型)大行其道,为了实现各层之间的解耦,对象之间的相互转换变得格外频繁,时势造英雄,这也成了MapStruct大显身友的好机会,相比于其他的对象转换方法,MapStruct具有以下特点:
1、安全性高,在编译期就实现源对象到目标对象的映射,不会把隐患留到运行期。
2、速度快,在运行期间直接调用实现类的方法,不需要使用反射进行转化,提升了转换效率。
下面,我们用一个小案例来领略MapStruct的魅力。
MapStruct实践
1、引入maven依赖
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
</dependency>
2、整理对象类UserEntity(实体类) 和 UserResp(返回类),注意,两个类的属性大致相同,但有部分属性不同,比如birthDay,一个是Date型,一个是String型,而对于部分字段如sex,实体类中存的是“0”,“1”,而在返回类中,更希望显示成“男“,”女“,这是很普遍的需求。
// 实体类
@Data
public class UserEntity {
/**
* 主键ID
*/
private String id;
/**
* 用户名
*/
private String userName;
/**
* 性别
*/
private String sex;
/**
* 生日
*/
private Date birthDay;
/**
* 年龄
*/
private Integer age;
/**
* 数据状态
*/
private String status;
/**
* 静态数据
*/
private String contentDesc;
/**
* 需要忽略的数据
*/
private String ignoreDesc;
/**
* entity独有数据
*/
private String entityDesc;
}
// 返回类
@Data
public class UserResp {
/**
* 主键ID
*/
private String id;
/**
* 用户名
*/
private String userName;
/**
* 性别
*/
private String sex;
/**
* 生日
*/
private String birthDay;
/**
* 年龄
*/
private Integer age;
/**
* 数据状态
*/
private String status;
/**
* 静态数据
*/
private String contentDesc;
/**
* 忽略数据
*/
private String ignoreDesc;
/**
* resp独有数据
*/
private String respDesc;
}
3、编写转换类
//对象转换接口
@Mapper
public interface UserConverter {
UserConverter INSTANCE = Mappers.getMapper(UserConverter.class);
List<UserResp> toRespList(List<UserEntity> list);
// 用SexEnum.getNameByValue()方法对UserEntity里的sex字段进行转换
@Mapping(target = "sex", expression = "java(com.leixi.converter.pojo.enums.SexEnum.getNameByValue(entity.getSex()))")
// 用DateUtil.formatDate() 对UserEntity里的birthDay字段进行转换
@Mapping(target = "birthDay", expression = "java(com.leixi.converter.util.DateUtil.formatDate(entity.getBirthDay()))")
// 用getStatusBySexAndAge(String sex, Integer) 方法计算status,存入UserResp中
@Mapping(target = "status", expression = "java(getStatusBySexAndAge(entity.getSex(), entity.getAge()))")
// UserEntity里的字段entityDesc赋值给UserResp里的respDesc
@Mapping(target = "respDesc", source = "entityDesc")
// UserResp里的contentDesc设为静态值ConverterConstant.RESP_DESC
@Mapping(target = "contentDesc", constant = ConverterConstant.RESP_DESC)
// 不对UserEntity中的ignoreDesc字段进行转换
@Mapping(target = "ignoreDesc", source = "ignoreDesc", ignore = true)
UserResp toResp(UserEntity entity);
default String getStatusBySexAndAge(String sex, Integer age) {
if (Objects.equals(SexEnum.Male.getValue(), sex) && age >= 18) {
return "成年男性";
} else if (Objects.equals(SexEnum.Female.getValue(), sex) && age >= 18){
return "成年女性";
} else if (Objects.equals(SexEnum.Male.getValue(), sex)) {
return "幼年男性";
} else {
return "幼年女性";
}
}
List<UserEntity> toEntityList(List<UserResp> list);
@Mapping(target = "sex", expression = "java(com.leixi.converter.pojo.enums.SexEnum.getValueByName(resp.getSex()))")
@Mapping(target = "birthDay", expression = "java(com.leixi.converter.util.DateUtil.parseDate(resp.getBirthDay()))")
@Mapping(target = "contentDesc", constant = ConverterConstant.ENTITY_DESC)
@Mapping(target = "ignoreDesc", source = "ignoreDesc", ignore = true)
@Mapping(target = "entityDesc", source = "respDesc")
UserEntity toEntity(UserResp resp);
}
//性别枚举
public enum SexEnum {
Male("1", "男"),
Female("0", "女"),
Unknown("-1", "未知"),
;
private final String value;
public String getValue() {
return value;
}
public String getName() {
return name;
}
private final String name;
SexEnum(String value, String name) {
this.value = value;
this.name = name;
}
public static String getNameByValue(String value) {
for (SexEnum en : SexEnum.values()) {
if (value.equals(en.value)) {
return en.getName();
}
}
return SexEnum.Unknown.getName();
}
public static String getValueByName(String name) {
for (SexEnum en : SexEnum.values()) {
if (name.equals(en.name)) {
return en.getValue();
}
}
return SexEnum.Unknown.getValue();
}
}
//时间转换工具类
public class DateUtil {
public static final String DATE_YYYY_MM_DD = "yyyy-MM-dd";
public static String formatDate(Date date) {
if (Objects.isNull(date)) {
return "";
}
try {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DATE_YYYY_MM_DD);
return simpleDateFormat.format(date);
} catch (Exception e) {
return "";
}
}
public static Date parseDate(String date) {
if (StringUtils.isEmpty(date)) {
return null;
}
SimpleDateFormat sdf = new SimpleDateFormat(DATE_YYYY_MM_DD);
try {
return sdf.parse(date);
} catch (ParseException e) {
return new Date();
}
}
}
4、编写Controller
@RestController
public class DemoController {
@PostMapping("/getUserEntity")
public Object getUserEntity(@RequestBody UserResp user) {
return UserConverter.INSTANCE.toEntity(user);
}
@PostMapping("/getUserResp")
public Object getUserResp(@RequestBody UserEntity user) {
user.setBirthDay(new Date());
return UserConverter.INSTANCE.toResp(user);
}
}
5、编译代码后,可以看到Target里生成了对象转换的实体方法,如下
6、执行postman进行测试,效果杠杠滴。
MapStruct正名之争
(后面这部分都是没营养的口水战,不看也罢!)
整理好测试用例后,我志得意满的找到胡大佬,盛气凌人的说:”嘞个……大佬,请教下你平常是用什么方式实现两个对象之间的转换的?”
胡大佬:“这种事还用问我?手写呗,能费你多少功夫?”
雷袭:哦,你看是不是这样……
public UserResp transEntityToResp(UserEntity entity) {
UserResp userResp = new UserResp();
userResp.setRespDesc(entity.getEntityDesc());
userResp.setId(entity.getId());
userResp.setUserName(entity.getUserName());
userResp.setAge(entity.getAge());
userResp.setName1(entity.getName1());
userResp.setName2(entity.getName2());
userResp.setName3(entity.getName3());
userResp.setName4(entity.getName4());
userResp.setName5(entity.getName5());
userResp.setName6(entity.getName6());
userResp.setName7(entity.getName7());
userResp.setName8(entity.getName8());
userResp.setName9(entity.getName9());
userResp.setName10(entity.getName10());
userResp.setName11(entity.getName11());
userResp.setName12(entity.getName12());
userResp.setName13(entity.getName13());
userResp.setName14(entity.getName14());
userResp.setName15(entity.getName15());
userResp.setName16(entity.getName16());
userResp.setName17(entity.getName17());
userResp.setName18(entity.getName18());
userResp.setName19(entity.getName19());
userResp.setName20(entity.getName20());
userResp.setName21(entity.getName21());
userResp.setName22(entity.getName22());
userResp.setName23(entity.getName23());
userResp.setName24(entity.getName24());
userResp.setName25(entity.getName25());
userResp.setName26(entity.getName26());
userResp.setName27(entity.getName27());
userResp.setName28(entity.getName28());
userResp.setName29(entity.getName29());
userResp.setName30(entity.getName30());
userResp.setName31(entity.getName31());
userResp.setName32(entity.getName32());
userResp.setName33(entity.getName33());
return userResp;
}
雷袭:“再提一嘴哈,我还有个类八十多个属性,都这么写的话有点费头发啊!”
胡大佬: “这……确实会费点功夫哈!你也没说属性这么多啊!这种情况,我一般是用反射的,几行代码就能搞定。”
雷袭:“哦还能这么干啊!”
胡大佬:“这不废话嘛,办法总比问题多,你平常还是要多动脑子!”
雷袭:“那大佬你帮忙看看,我这个用反射咋报错了呢?”
胡大佬:“……你这个同名属性类型不同肯定不能用反射啊,你刚也没说有这种情况啊!”
雷袭:“废话,你又不是第一天当程序员,这种情况都想不到?”
胡大佬:”那,那我用BeanUtils.copyProperties()总行了吧,再对这种字段进行特殊处理?”
雷袭:“可是BeanUtils效率差啊,而且遇到Integer转成int的情况也会在执行时报错。现在正经人谁用copyProperties啊!”
胡大佬(怒):“这也不行,那也不行,你这不是在针对我吗?”
雷袭:“没没没,我可没有针对你的意思,我就是单纯的觉得你说的不对!”
胡大佬:“那你行你上啊,你能有啥好办法?”
雷袭(拿出准备好的代码):“大佬你看这样实现行不行?”
胡大佬(神情中有着掩饰不住的震惊):“MapStruct?我从来不用这种东西,我们程序员不能太依赖工具,程序员唯一能依仗的就是自己高超的技术!”
雷袭:“可是人与动物最大的区别不就是人擅长使用工具吗?”
胡大佬:“那……那你这个工具也不咋行啊!这么多方法都写在注解里,我要是变更了方法维护起来多不方便?要是没同步,一编译不就报错了!”
雷袭:“这也正是它的优点啊,在编译的时候就能发现问题,总比留坑到执行时再暴露要好吧!”
胡大佬(犹豫):“道理是这个道理,可是我还是觉得你在针对我!”
雷袭:“呵呵呵,您看人真准!”
胡大佬(冷笑):“行啊,胆儿肥了是吧,敢跟我杠上,有种下班了别走!”
雷袭:“你还威胁我,你你你信不信我告诉总监去!”
总监:“行了,你俩都别吵了,我这里有两颗果果,你们一人一个,吃完了都给我搬砖去。谁要是不听话,下次可就没好果子吃了!”