告别get,set,企业级MapperStruct 使用说明

背景说明

绝大多数做业务开发的猿们,都会写大量的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()一样。

这种对象属性映射的方法,很多实际项目都在用,能够节省很多时间,但是它并不完美,体现在如下几种情况:

  1. 对象属性映射使用反射来实现,性能比较低;
  2. 对于不同名称或不同类型的属性,不能进行转换,还需要补一定的Getter、Setter方法;
  3. 对于嵌套的子对象也需要转换的情况,需要自行处理;
  4. 集合对象转换时,需要做循环,并且一个个对象进行转换;

MapperStruct

针对上面的几个问题,使用 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  

插件安装:建议安装如下插件

idea 中的 MapperStruct 插件

该插件主要增强以下功能

  1. 代码补全,补全@Mapping注释中的目标和源属性(嵌套属性也可以)补全@ValueMapping注释中的目标和源属性补全@Mapper和@MapperConfig注释中的组件模型
  2. 点击可转到setter/getter的目标和源中的属性定义
  3. 查找目标和源中属性的用法,并查找@Mapping注释中setter/getter的用法
  4. 突出显示目标和源中的属性
  5. 对属性和方法重命名的重构支持
  6. 错误和快速修复: @Mapper或@MapperConfig注解缺失 快速修复的未映射目标属性:自动添加未映射的目标属性并忽略未映射的对象属性。

看一个专业人士在项目中实际场景映射代码,里面每行代码都做了注释,请仔细看这段代码

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));}

在大多数项目中,使用如上的方式配置转换器足以

过程如下:

  1. 定义一个转换器接口
  2. 在接口上加上 @Mapper注解
  3. 加上TenantConvert INSTANCE = Mappers.getMapper(TenantConvert.class);
  4. 写convert方法,注意参数类型
  5. 调用TenantConvert.INSTANCE.convert(tenant)

MapStruct实现原理

那么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);

使用@InheritInverseConfiguration快速处理反向转换

针对某些对象,既有从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整合

如果我们使用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);}
  1. 常量,不管源对象的值是多少,映射完目标对象的值就是constant 指定的值;
  2. defaultValue 则使用于:如果原对象未提供该字段的值,则目标对象使用defaultValue 的值;
  3. expression 根据表达式生成值。

切面处理

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

标签:嵌套   注解   表达式   企业级   转换器   使用说明   属性   对象   目标   类型   代码   方法

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top