绝大多数做业务开发的猿们,都会写大量的getter、setter方法。一是将各个业务系统间的请求数据进行组装和转换。二是将自己系统内部PO、VO、DTO等各种O进行转换,以满足接收方的要求。
看下面的代码
public UserInfoVO originalCopyItem(UserDTO userDTO){ UserInfoVO userInfoVO = new UserInfoVO(); userInfoVO.setUserName(userDTO.getName()); userInfoVO.setAge(userDTO.getAge()); userInfoVO.setBirthday(userDTO.getBirthday()); userInfoVO.setIdCard(userDTO.getIdCard()); userInfoVO.setGender(userDTO.getGender()); userInfoVO.setIsMarried(userDTO.getIsMarried()); userInfoVO.setPhoneNumber(userDTO.getPhoneNumber()); userInfoVO.setAddress(userDTO.getAddress()); return userInfoVO;}
这种多个对象之间的get,set方法带来的问题就是代码冗长,重复性劳动过大。
1>重复的劳动一定是低效的,易出错的;
2>任何重复的业务逻辑必定需要进行封装。
针对各个对象之间的属性赋值,我们一般有什么好的处理方式?
我列出可能大多数人使用最多的处理方式:
一、使用spring框架所带的方法
使用 org.springframework.beans 包里面的 BeanUtils.copyProperties() 来进行属性复制
// 将source属性复制到target同名属性中,名称对应不上,则忽略// 第3个参数为 String... ignoreProperties 可变参数,表示要忽略的属性名称BeanUtils.copyProperties(source, target, "userName");
二、使用Hutool工具包中的BeanUtil来处理
cn.hutool hutool-all 5.7.22
具体使用方式,和spring框架的BeanUtils.copyProperties()一样。
这种对象属性映射的方法,很多实际项目都在用,能够节省很多时间,但是它并不完美,体现在如下几种情况:
针对上面的几个问题,使用 MapperStruct 能够很好地解决
某猿:每天都在写Getter、Setter方法,我不耐烦了,于是用了神器 MapperStruct,crud效率一下子提高了!
先引入依赖
org.mapstruct mapstruct 1.5.2.Final org.mapstruct mapstruct-processor 1.5.2.Final compile org.projectlombok lombok-mapstruct-binding 0.2.0
org.mapstruct:mapstruct:包含必需的注释,例如@Mapping;
org.mapstruct:mapstruct-processor:包含注释处理器,该注释处理器生成映射器实现;
org.projectlombok:lombok-mapstruct-binding 处理lombok和mapstruct一起用时,有可能导致@Data注解无法正常生成代码的问题:[no property named .... 的错误]
加入MVN编译处理插件,一个是lombok,一个是mapstruct-processor,用来在编译器自动生成一些代码和实现类,参考如下配置:
...... ...... org.apache.maven.plugins maven-compiler-plugin ${maven-compiler-plugin.version} org.projectlombok lombok ${lombok.version} org.mapstruct mapstruct-processor ${mapstruct.version}
注意:最好是和 lombok 一起使用
org.projectlombok lombok 1.18.24 true
插件安装:建议安装如下插件
该插件主要增强以下功能
看一个专业人士在项目中实际场景映射代码,里面每行代码都做了注释,请仔细看这段代码
package com.ly.yph.api.organization.convert.tenant;import com.ly.yph.api.organization.controller.tenant.vo.tenant.TenantCreateReqVO;import com.ly.yph.api.organization.controller.tenant.vo.tenant.TenantExcelVO;import com.ly.yph.api.organization.controller.tenant.vo.tenant.TenantRespVO;import com.ly.yph.api.organization.controller.tenant.vo.tenant.TenantUpdateReqVO;import com.ly.yph.api.organization.controller.user.vo.user.UserCreateReqVO;import com.ly.yph.api.organization.entity.SystemTenant;import com.ly.yph.core.base.page.PageResp;import org.mapstruct.Mapper;import org.mapstruct.factory.Mappers;import java.util.List;// @Mapper 定义这是一个MapStruct对象属性转换接口,在这个类里面规定转换规则// 在项目构建时,会自动生成改接口的实现类,这个实现类将实现对象属性值复制,请在target中查找自动生成的实现类// 也可以使用 abstract class,不一定要是interface@Mapperpublic interface TenantConvert { /** * 获取该类自动生成的实现类的实例 * 接口中的属性都是 public static final 的 方法都是public abstract的 */ TenantConvert INSTANCE = Mappers.getMapper(TenantConvert.class); // 将 TenantCreateReqVO 转换为 SystemTenant,默认映射规则【两个对象中同名称,同类型属性进行映射】 // @Mapping 用来定义属性复制规则 source 指定源对象属性 target指定目标对象属性 @Mappings({ @Mapping(source = "id", target = "tenantId"), @Mapping(source = "name", target = "tenantName"), @Mapping(source = "address.detail", target = "addressInfo") }) SystemTenant convert(TenantCreateReqVO bean); // 将 TenantUpdateReqVO 转换为 SystemTenant ,使用默认映射规则【两个对象中同名称,同类型属性进行映射】 SystemTenant convert(TenantUpdateReqVO bean); // 将 SystemTenant 转换为 TenantRespVO ,使用默认映射规则【两个对象中同名称,同类型属性进行映射】 TenantRespVO convert(SystemTenant bean); // 集合类型的转换,无需自己写循环进行单个对象转换 List convertList(List list); // 嵌套对象转换,PageResp 存在一个属性为data 的List集合,MapperStruct可以自动进行转换 PageResp convertPage(PageResp page); // List集合转换 List convertList02(List list); // 可以添加额外的默认方式 // 如果需要特殊的转换,可以同样写在转换器里面,方便外部统一调用 default UserCreateReqVO convert02(TenantCreateReqVO bean) { UserCreateReqVO reqVO = new UserCreateReqVO(); reqVO.setUsername(bean.getUsername()); reqVO.setPassword(bean.getPassword()); reqVO.setNickname(bean.getContactName()).setMobile(bean.getContactMobile()); return reqVO; }}
提示:使用Java 8或更高版本时,可以省略@Mappings 包装器注释,并直接在一个方法上指定多个@Mapping注释
使用转换器:
下面的 TenantConvert.INSTANCE.convert(tenant) 调用了 TenantConvert里面的第三个convert方法
@GetMapping("/get")@ApiOperation("获得租户")@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)@SaCheckPermission("system:tenant:query")public ServiceResult getTenant(@RequestParam("id") Long id) { SystemTenant tenant = tenantService.getTenant(id); // 使用转换器 return ServiceResult.succ(TenantConvert.INSTANCE.convert(tenant));}
在大多数项目中,使用如上的方式配置转换器足以
过程如下:
那么MapStruct解决了我们之前使用BeanUtils.copyProperties()遇到的问题么?
先分析下MapStruct实现原理
其实MapStruct的实现原理很简单,就是根据我们在Mapper接口中使用的@Mapper和@Mapping等注解,在运行时生成接口的实现类,我们可以打开项目的target目录看下
注意到没有,是不是很惊讶
MapStruct并不是通过反射来对对象属性进行的赋值,而是通过get,set方法,所以性能上肯定高过BeanUtils.copyProperties()方法
那么使用BeanUtils.copyProperties()的1性能问题,4集合转换问题 都解决了之后,还有一个可能会出现的问题,就是如果两个对象中的属性名称不一样【user:userName -> userVo:displayName】,那要如何映射?
针对【user:userName -> userVo:displayName】这样的映射,如果使用BeanUtils.copyProperties(),在调用此方法后,再补充set方法,对于完美主义来说,还是没法接受;
BeanUtils.copyProperties(user,uservo);// 需要补充 set 方法uservo.setDispalyName(user.getDisplayName);
那如何通过MapStruct来处理类型需求呢,非常容易,一看代码便知
@Mapperpublic interface UserConvert { UserConvert INSTANCE = Mappers.getMapper(UserConvert.class); @Mapping(source = "tel", target = "telNumber") @Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd") @Mapping(source = "userName", target = "displayName") UserRespVO convert(SystemUsers bean); @Mapping(source = "tel", target = "telNumber") @Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd") @Mapping(source = "userName", target = "displayName") List convertList05(List userList);}
看完上面的代码,详细已经不用作者再做解释
注意 MapperStruct 会做隐式数据类型转换,上面 birthday 在 UserRespVO 对象中为字符串,在 SystemUsers 为 Date 类型,会自动进行转换,我们指定一个 dateFormat = "yyyy-MM-dd" 用来给转换指定格式。
在处理了不同名称,不通类型的属性映射之后,还有最后一种情况,没有处理,就是嵌套对象的转换 [user.product.name userVO:productVO.name]
假设一种场景,我们有一个订单 DO 对象 Order,嵌套有 User 和 Product 对象,为减少理解难度,下面代码简化了模型
@Datapublic class Order { private User user; private List productList; }@Datapublic class User { private String name;}@Datapublic class Product { private Long id; private String productSn; private String name;}
对应的 VO
@Datapublic class OrderVo { private UserVo userVo; private List productVoList; }@Data@EqualsAndHashCode(callSuper = false)public class ProductVo { private Long id; private String productSn; private String name;}
转换器接口:
// 使用 uses = {UserMapper.class,ProductMapper.class}// 表示:此转换器,还需要使用 UserMapper 和 ProductMapper 两个转换器@Mapper(uses = {UserMapper.class,ProductMapper.class})public interface OrderMapper { OrderMapper INSTANCE = Mappers.getMapper(OrderMapper.class); // 这里需要注明source 对象和target 对象 @Mapping(source = "user",target = "UserVo") @Mapping(source = "productList",target = "productVoList") OrderVo convertToVo(Order order);}// imports 表示:需要UUID类的功能@Mapper(imports = {UUID.class})public interface ProductMapper { ProductMapper INSTANCE = Mappers.getMapper(ProductMapper.class); @Mapping(target = "id",constant = "-1L") @Mapping(source = "count",target = "number",defaultValue = "1") // expression = "java(UUID.randomUUID().toString())" 表示生成规则 @Mapping(target = "productSn",expression = "java(UUID.randomUUID().toString())") ProductVo convertToVo(Product product);}
使用方式
1.使用@Mapper(uses = {UserMapper.class,ProductMapper.class}) 方式来包含嵌套的映射器;
2.使用@Mapping(source = "user",target = "UserVo") 标注需要从原对象转换到目标对象;
额外技巧
1. 使用@Mapper(imports = {UUID.class}) 导入需要在转换过程中,表达式需要使用的类
2.使用@Mapping(target = "productSn",expression = "java(UUID.randomUUID().toString())"),来使用1步骤导入的类通过表达式来生成值里面 expression 后面有具体说明这里先不用细说。
如此一来,嵌套对象转换问题,我们也解决了。
那么有人就问了,如果需要多个对象的属性,映射到某一个对象中的属性,该怎么办呢?请看合并映射
设计一个场景,需要将 user 和 order 的属性,映射到 UserOrderVo 中,具体代码如下:
@Mapperpublic interface UserMapper { UserMapper INSTANCE = Mappers.getMapper(UserMapper.class); @Mapping(source = "user.tel",target = "telNumber") @Mapping(source = "user.birthday",target = "birthday",dateFormat = "yyyy-MM-dd") @Mapping(source = "user.id",target = "id") @Mapping(source = "order.orderNo", target = "orderNo") @Mapping(source = "order.receiverAddress", target = "receiverAddress") // 可以使用表达式来,将属性进行合并映射 @Mapping(target = "display",expression = "java(user.getUserName()+':'+order.getPrice())") Target sourceToTarget(Source s); UserOrderVo toUserOrderVo(User user, Order order); }
因为合并映射比较简单,相信上面的代码一看就懂
可以使用 @Mapping(target = "display",expression = "java(user.getUserName()+':'+order.getPrice())") 来使用表达式将传入参数多个属性合并到一个属性上面
另外也可直接在 convert 方法中传递参数,将参数映射到目标对象的属性中;以下的例子,将传入的 returnTime 绑定到目标 UserVO 中的 returnTime 上了
@Mappings({ @Mapping(source = "returnTime", target = "datareturnTime")})UserVO convert(User user, String returnTime);
针对某些对象,既有从DO到VO的转换,同时也存在VO到DO的转换;我们把它叫反向映射。
在映射配置比较复杂的时候,我们针对这两个方向是否要配置两次映射呢?答案是否定的,这时我们需要使用@InheritInverseConfiguration注解
@Mapperpublic interface UserMapper { UserMapper INSTANCES = Mappers.getMapper(UserMapper.class); @Mapping(source = "userName", target = "name") User convert(UserVO userVO); //无需再次配置映射规则 @InheritInverseConfiguration UserVO convert1(User user);}
可以看到通过 @InheritInverseConfiguration 注解,我们不用再次配置反向的@Mapping 注解
很多时候,在对象转换的时候可能会出现这样一个问题,就是源对象和目标对象中的类型不一致;
在大部门情况下,MapStruct会自动处理类型转换。例如,如果一个属性在源bean中属于int类型,但在目标bean中属于String类型,那么生成的代码将通过分别调用String#valueOf(int)和Integer#parseInt(String)来透明地执行转换。
所有Java原语类型(包括它们的包装)和字符串之间,例如int和String之间或Boolean和String之间。可以转换,有些可以指定转换的格式,比如上文的@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
如果默认的类型转换不满足我们的业务需求,我们可以自定义转换器
正常情况下,我们很少会见到类型转换的业务场景,如果出现了,我们可以去查官方文档具体透明转换执行的逻辑,负责任的说,专业人士目前的项目,没有遇到任何需要类型转换的地方 专业人士建议的学习方式是,不要把大量时间浪费在很少会出现的业务场景上面
假设一个业务场景,需要把 isDisable bool类型的值转换成中文的“是”和“否”
通过@Mapper的uses属性来实现:
// 先定义转换// 如果并没有使用spring,则不要加上@Component@Componentpublic class BooleanStrFormat { public String toStr(Boolean isDisable) { if (isDisable) { return "是"; } else { return "否"; } } public Boolean toBoolean(String str) { if (str.equals("是")) { return true; } else { return false; } }}
定义mapper转换器
// 使用uses 将转换器引入// 如果并没有使用spring,则不要加上componentModel = "spring"@Mapper(componentModel = "spring", uses = { BooleanStrFormat.class})public interface UserMapper { UserMapper INSTANCES = Mappers.getMapper(UserMapper.class); // 自动应用 BooleanStrFormat 方法中的 toStr方法 @Mapping(source = "isDisable", target = "disable") UserVO convert(User user); @InheritInverseConfiguration User convert1(UserVO userVO);}
MapStruct 在转换时,会自动应用uses引入的转换器,自动找到对应的转换方法【根据参数】,转换具体的属性。
如果我们使用spring框架,那么 UserMapper INSTANCE = Mappers.getMapper(UserMapper.class); 这行代码就可以省略,我们使用spring的容器来存放具体转换对象
@Mapper(componentModel = "spring") public interface UserMapper { // 此行代码不再需要 // UserMapper INSTANCE = Mappers.getMapper(UserMapper.class); @Mapping(source = "tel", target = "telNumber") @Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd") UserVo convertToVo(User user);}
然后使用spring的自动注入 @Autowired 注解注入即可将 mapper 对象获取到
@RestController@RequestMapping("/user")public class UserController { // 使用spring容器 @Autowired UserMapper userMapper; @GetMapping("/mapStructToVo") public Result mapStructToVo() { // ...... UserVo userVo = userMapper.convertToVo(user); // ...... }}
实际原理是:
MapStruct 在生成的 convert 实现类上,加上了@Component 注解,所以使用 @Autowired能够获取具体对象,有兴趣可以查看 target 目录中对应的 convert 具体的实现类。
在进行属性转换的时候,有些字段无法从原对象获取,可能需要提供一些默认值,或者根据表达式进行计算
MapperStruct 也考虑到这点,请看如下示例
// 导入表达式需要的类@Mapper(imports = {UUID.class})public interface ProductMapper { ProductMapper INSTANCE = Mappers.getMapper(ProductMapper.class); // 给转换后的productVo的id字段设置为常量-1,无论原始对象的id是多少 @Mapping(target = "id",constant = "-1L") // 如果原对象未提供,则使用defaultValue 定义的值 @Mapping(source = "count",target = "number",defaultValue = "1") // 根据表达式生成 @Mapping(target = "productSn",expression = "java(UUID.randomUUID().toString())") ProductVo convertToVo(Product product);}
MapStruct也支持在映射前后做一些自定义操作,类似Spring的AOP中的切面
在转换器接口中,使用@BeforeMapping和@AfterMapping两个注解,配合@MappingTarget注解
@BeforeMappingpublic void beforeMapping(Product product){ //映射前当price<0时设置为0 if(product.getPrice().compareTo(BigDecimal.ZERO)<0){ product.setPrice(BigDecimal.ZERO); }}@AfterMappingpublic void afterMapping(@MappingTarget ProductVo productVo){ //映射后设置当前时间为createTime productVo.setCreateTime(new Date());}
相当于在转换前和转换后,给具体的target和source进行了一些属性修改的机会
有赋值怎么能没有来源值校验呢?MapStruct也考虑到了这一需求
我们先创建一个验证类
public class UserValidator { public String validatePrice(String tel) throws Exception { if(StringUtils.isNotBlank(tel)&&tel.length()>11){ throw new Exception("手机号位数超过11位了"); } return tel; }}
运用校验类
// 校验类同样使用 uses 方法引入@Mapper(uses = {UserValidator.class})public interface UserExceptionMapper { UserExceptionMapper INSTANCE = Mappers.getMapper(UserExceptionMapper.class); @Mapping(source = "tel", target = "telNumber") @Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd") UserVo convertToVo(User user) throws Exception;}
当校验失败时则会抛出异常
try { UserVo userVo = UserExceptionMapper.INSTANCE.convertToVo(user);}catch (Exception e) { System.out.println(e.getMessage());}
最后附上各种方式的性能测试,可见MapStruct的性能是非常高的
理解了上面的几个内容,相信对象之间的属性应该不会再次困扰到你。
本文还未说明的MapperStruct内容,大概率你的项目永远都不会用到。
如果你的项目要用到某个框架的最边边角角的功能,大概率你的项目设计出了一点问题
本文为 【非专业的专业人士】 原创文章。
专业人士原创文章以前,现在,以后,都不会要求读者,加群,点击链接,或者夹杂软广告。
原创不易,请【点赞】【收藏】,给予作者动力,如果有技术方面问题想与作者探讨,欢迎回复。
需要转载,请联系作者说明,作者持欢迎态度!
页面更新:2024-04-15
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号