MapStruct 对象值拷贝

介绍

前提

在Java 项目开发中,存在需要连个不同类的对象的转化的情况,
例如VO与DO 的中同一逻辑对象的值转换。

@AllArgsConstructor
@Data
public class UserVo {
    private Long id;
    private String username;
    private String password;
    private String phoneNum;
    private String email;
    private Role role;
}
public class UserDo {
    private Long id;
    private String username;
    private String password;
    private String phoneNum;
    private String email;
    private Role role;
}

两个类中的属性大致相同,但是应为所属的逻辑分层,导致被分成不同的POJO对象,对于业务分层交互中会用到两个对象的值的交换。
通常的解决方案:

  • 自行书写转换类,进行值转换

    • 优点:转换方便,灵活度高
    • 缺点:代码冗余过高
  • 使用Java内省机制,Apache/Spring 的BeanUtil.copyProperties

    • 优点:代码简洁
    • 缺点:转换的灵活度不高,对于属性名称不同的无法转换,内省基于Java的反射性能较低
  • MapStruct

    • MapStruct 为两者的优势结合,在相对降低代码繁琐的同时,保留性能
    • 基于注解形式,简化代码冗余
    • 才用Java 提供的编译时Process ,在class 生成期 生成代理类,避免Java 运行时的反射带来的性能损耗

Demo

Order.java

package com.liaojl.test.mapstruct;

public class Order {
    /**
     * 订单id
     */
    private Long id;
    /**
     * 订单编号
     */
    private String orderSn;
    /**
     * 收货人姓名/号码
     */
    private String receiverKeyword;
    /**
     * 订单状态:0->待付款;1->待发货;2->已发货;3->已完成;4->已关闭;5->无效订单
     */
    private Integer status;
    /**
     * 订单类型:0->正常订单;1->秒杀订单
     */
    private Integer orderType;
    /**
     * 订单来源:0->PC订单;1->app订单
     */
    private Integer sourceType;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getOrderSn() {
        return orderSn;
    }

    public void setOrderSn(String orderSn) {
        this.orderSn = orderSn;
    }

    public String getReceiverKeyword() {
        return receiverKeyword;
    }

    public void setReceiverKeyword(String receiverKeyword) {
        this.receiverKeyword = receiverKeyword;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public Integer getOrderType() {
        return orderType;
    }

    public void setOrderType(Integer orderType) {
        this.orderType = orderType;
    }

    public Integer getSourceType() {
        return sourceType;
    }

    public void setSourceType(Integer sourceType) {
        this.sourceType = sourceType;
    }
}

OrderQueryParam.java

package com.liaojl.test.mapstruct;

public class OrderQueryParam {
    /**
     * 订单编号
     */
    private String orderSn;
    /**
     * 收货人姓名/号码
     */
    private String receiverKeyword;
    /**
     * 订单状态:0->待付款;1->待发货;2->已发货;3->已完成;4->已关闭;5->无效订单
     */
    private Integer status;
    /**
     * 订单类型:0->正常订单;1->秒杀订单
     */
    private Integer orderType;
    /**
     * 订单来源:0->PC订单;1->app订单
     */
    private Integer sourceType;

    public String getOrderSn() {
        return orderSn;
    }

    public void setOrderSn(String orderSn) {
        this.orderSn = orderSn;
    }

    public String getReceiverKeyword() {
        return receiverKeyword;
    }

    public void setReceiverKeyword(String receiverKeyword) {
        this.receiverKeyword = receiverKeyword;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public Integer getOrderType() {
        return orderType;
    }

    public void setOrderType(Integer orderType) {
        this.orderType = orderType;
    }

    public Integer getSourceType() {
        return sourceType;
    }

    public void setSourceType(Integer sourceType) {
        this.sourceType = sourceType;
    }
}

配置转换类

package com.liaojl.test.mapstruct.mapper;

import com.liaojl.test.mapstruct.Order;
import com.liaojl.test.mapstruct.OrderQueryParam;
import org.mapstruct.Mapper;

@Mapper
public interface OrderMapper {
    OrderQueryParam entity2queryParam(Order order);
}

测试

/**
 * @author: liaojl
 * @date: 2020/5/30 18:09
 * @since: 1.0
 */
public class Test {
    @org.junit.Test
    public void entity2queryParam() {
        Order order = new Order();
        order.setId(12345L);
        order.setOrderSn("orderSn");
        order.setOrderType(0);
        order.setReceiverKeyword("keyword");
        order.setSourceType(1);
        order.setStatus(2);
        OrderMapper mapper = Mappers.getMapper(OrderMapper.class);
        OrderQueryParam orderQueryParam = mapper.entity2queryParam(order);
        assertEquals(orderQueryParam.getOrderSn(), order.getOrderSn());
        assertEquals(orderQueryParam.getOrderType(), order.getOrderType());
        assertEquals(orderQueryParam.getReceiverKeyword(), order.getReceiverKeyword());
        assertEquals(orderQueryParam.getSourceType(), order.getSourceType());
        assertEquals(orderQueryParam.getStatus(), order.getStatus());
    }
}

Java 编译期 自动生成代理Java代码
在这里插入图片描述

package com.liaojl.test.mapstruct.mapper;

import com.liaojl.test.mapstruct.Order;
import com.liaojl.test.mapstruct.OrderQueryParam;
import javax.annotation.Generated;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2020-05-30T23:20:57+0800",
    comments = "version: 1.3.1.Final, compiler: javac, environment: Java 11.0.6 (Amazon.com Inc.)"
)
public class OrderMapperImpl implements OrderMapper {

    @Override
    public OrderQueryParam entity2queryParam(Order order) {
        if ( order == null ) {
            return null;
        }

        OrderQueryParam orderQueryParam = new OrderQueryParam();

        orderQueryParam.setOrderSn( order.getOrderSn() );
        orderQueryParam.setReceiverKeyword( order.getReceiverKeyword() );
        orderQueryParam.setStatus( order.getStatus() );
        orderQueryParam.setOrderType( order.getOrderType() );
        orderQueryParam.setSourceType( order.getSourceType() );

        return orderQueryParam;
    }
}

技术点

基于Java Processororg.mapstruct.ap.MappingProcessor

org.mapstruct.Mapper

@org.mapstruct.Mapping注解用来声明成员属性的映射。该注解有两个重要的属性:

  • source 代表转换的源。
  • target 代表转换的目标。

当两者属性相同时可以忽略,MapStruct 转换时终调用的是 target的setter 和 source 的 getter 方法,而非反射。这也是其性能比较好的原因之一。

@Mapper
public interface OrderMapper {
    OrderQueryParam entity2queryParam(Order order);
}

与org.mapstruct.Mapping

针对 target与source 的属性名称 存在差异性时,可以进行自定义转换

  • target的a属性转为source中B的a属性
  • target的b属性转为source中b1属性
 @Mapping(target = "a", source = "B.a")
     @Mapping(target = "b", source = "b1")
     Target sourceToTarget(Source source);

支持默认值

@Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")

格式化操作

格式化也是我们经常使用的操作,比如数字格式化,日期格式化。
这是处理数字格式化的操作,遵循java.text.DecimalFormat的规范:

@Mapping(source = "price", numberFormat = "$#.00")

下面展示了将一个日期集合映射到日期字符串集合的格式化操作上:

 @IterableMapping(dateFormat = "dd.MM.yyyy")
 List<String> stringListToDateList(List<Date> dates);

使用 java 表达式

下面演示如何使用LocalDateTime 作为当前的时间值注入 addTime 属性中。

首先在@org.mapstruct.Mapper 的 imports 属性中导入 LocalDateTime,该属性是数组意味着你可以根据需要导入更多的处理类:

@Mapper(imports = {LocalDateTime.class})

接下来只需要在对应的方法上添加注解@org.mapstruct.Mapping ,其属性expression 接收一个 java() 包括的表达式:

  • 无入参版本:
    @Mapping(target = "addTime", expression = "java(LocalDateTime.now())")
  • 携带入参的版本, 我们将 Car 的出厂日期字符串manufactureDateStr 注入到 CarDTO 的 LocalDateTime 类型属性addTime 中去:
@Mapping(target = "addTime", expression = "java(LocalDateTime.parse(car.manufactureDateStr))")
CarDTO carToCarDTO(Car car);

org.mapstruct.Mappings

配置多个bean属性的映射。

提示:使用Java 8或更高版本时,可以省略@Mappings 包装器注释,并直接在一个方法上指定多个@Mapping注释。

这两个示例是相等的。


  // before Java 8
  @Mapper
  public interface MyMapper {
      @Mappings({
          @Mapping(source = "first", target = "firstProperty"),
          @Mapping(source = "second", target = "secondProperty")
      })
      HumanDto toHumanDto(Human human);
  }
  

  // Java 8 and later
  @Mapper
  public interface MyMapper {
      @Mapping(source = "first", target = "firstProperty"),
      @Mapping(source = "second", target = "secondProperty")
      HumanDto toHumanDto(Human human);
  }
  

Spring 集成

MapStruct 转换 Mapper 注入Spring IoC 容器

如果使用要把Mapper 注入Spring IoC 容器我们只需要这么声明,不用再构建一个单例,就可以像其他 spring bean一样对注解的Bean 进行引用了:

 @Mapper(componentModel = "spring")
public interface OrderMapper {
    OrderQueryParam entity2queryParam(Order order);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章