MapStruct 使用

对象映射工具的由来

大型项目采用分层开发,每层的数据模型都不同:在持久化层,模型层为 PO(Persistent Object)、在开放服务层,模型为数据传输对象 DTO(Data Transfer Object)。

如果开放服务直接将 PO (持久化模型对象)对外暴露,叫开放领域模型风格。
如果开放服务只能将 DTO(数据传输对象)对外暴露,叫封闭领域模型风格。

在小型项目,和其它系统交互不多,对安全性要求不高的场景下,可以考虑使用开放领域模型风格。
在大型项目,分层开发,系统交互多,为了不暴露底层模型细节,我们推荐使用封闭领域模型风格。
这样各层数据模型交互时不可避免需要做映射处理,简单场景我们可以使用 Spring 框架提供的 BeanUtils.copyProperties,但它有局限性。
首先是不适合对性能有严苛要求的情况,因为 BeanUtils.copyProperties 是基于 Java 反射实现的。
其次不适合复杂映射场景:

比如性别在后台是通过 0 和 1,但是需要返回前端 男 或者 女,如何映射?
比如 PO <=> DTO 属性名不同,如何映射?
比如 多个 PO => DTO 如何映射?
再比如 属性名和属性类型都不同,又如何映射?

下面介绍的 MapStruct 就是专门应对为这种场景的。

MapStruct 简介

MapStruct 是一个 Java 注释处理器,用于生成类型安全的 bean 映射类。您只需定义一个 mapper接口,该接口声明任何必需的映射方法。在编译期间,MapStruct 将生成此接口的实现。此实现使用普通的 Java 方法调用在源对象和目标对象之间进行映射,注意:它不是通过反射实现的,因此效率很高,这也是我们推荐的主要原因。
与动态映射框架相比,MapStruct 具有以下优点:

映射灵活,可定制化程度高。
使用普通方法调用而不是反射,效率很好。
具备编译时类型安全性检查能力,在编译期就能规避很多映射的潜在问题。

使用示例

示例一

订单 PO 类定义:

import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 订单 PO
 * @date 2020-03-16
 */
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class Order implements Serializable {

    /**
     * 订单 Id
     */
    private Long id;

    /**
     * 买家电话
     */
    private String buyerPhone;

    /**
     * 买家地址
     */
    private String buyerAddress;

    /**
     * 订单金额
     */
    private Long amount;

    /**
     * 支付状态
     */
    private Integer payStatus;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

}

订单 DTO 类定义:

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * @date 2020-03-16
 */
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class OrderDTO implements Serializable {

    /**
     * 订单 Id
     */
    private Long orderId;

    /**
     * 买家电话
     */
    private String buyerPhone;

    /**
     * 买家地址
     */
    private String buyerAddress;

    /**
     * 订单金额
     */
    private Long amount;

    /**
     * 支付状态
     */
    private Integer payStatus;

    /**
     * 创建时间
     */
    private String orderTime;

}

复制代码注意到,由于业务场景的特殊:

订单Id 在 PO 对象 Order 里叫 id,在 DTO 对象 OrderDTO 里叫 orderId。
订单创建时间在 PO 对象 Order 里是 LocalDateTime 类型,且名称为 createTime,而在对应的 OrderDTO 里叫哦 orderTime,且类型为 String

面对这种情况,传统的 BeanUtils.copyProperties 方法似乎不好处理,而且前面也说过,BeanUtils.copyProperties 是基于反射实现的,效率并不高。这里我们用 MapStruct 来处理就比较简单了,首先定义一个映射接口(我们以后都将这类接口统称为 Convert 接口)。

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

/**
 * @date 2020-03-16
 */
@Mapper(componentModel = "spring")
public interface OrderConvert {

    @Mapping(source = "id", target = "orderId")
    @Mapping(source = "createTime", target = "orderTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
    OrderDTO from(Order order);

}

源和目标对象的属性名和属性一致的话,并不需要在转换接口中明确定义,框架会自动处理。
只有需要映射的属性名不同,或者类型不一致,或有特殊转换需求才需要明确定义。

如何使用:

@Test
public void test() {
    Order order = Order.builder()
        .id(123L)
        .buyerPhone("13707318123")
        .buyerAddress("中电软件园")
        .amount(10000L)
        .payStatus(1)
        .createTime(LocalDateTime.now())
        .build();

    OrderConvert orderConvert = Mappers.getMapper(OrderConvert.class);
    OrderDTO orderDTO = orderConvert.from(order);

    System.out.println("order:    " + order);
    System.out.println("orderDTO: " + orderDTO);
}

运行结果:

order: Order(id=123, buyerPhone=13707318123, buyerAddress=中电软件园, amount=10000, payStatus=1, createTime=2020-03-17T09:13:32.622)
orderDTO: OrderDTO(orderId=123, buyerPhone=13707318123, buyerAddress=中电软件园, amount=10000

示例二

适用于有两个,或多个 PO 对象,映射到一个 DTO 的场景。
例如我有两个 PO 对象:GoodInfo 和 GoodType,如下:

import lombok.Builder;
import lombok.Data;

/**
 * 商品信息
 * @date 2020-03-16
 */
@Builder
@Data
public class GoodInfo {
    private Long id;
    private String title;
    private double price;
    private int order;
    private Long typeId;
}
/**
 * 商品类型
 * @date 2020-03-16
 */
@Builder
@Data
public class GoodType {
    private Long id;
    private String name;
    private int show;
    private int order;
}

目标 DTO 为 GoodInfoDTO:

/**
 * @date 2020-03-16
 */
@Data
public class GoodInfoDTO {
    private String goodId;
    private String goodName;
    private double goodPrice;
    private String typeName;
}
/**
 * N Object => 1 Object
 * @date 2020-03-16
 */
@Mapper(componentModel = "spring")
public interface GoodInfoConvert {

    /** Long => String 隐式类型转换 */
    @Mapping(source = "good.id", target = "goodId")
    /** 属性名不同, */
    @Mapping(source = "type.name", target = "typeName")
    /** 属性名不同 */
    @Mapping(source = "good.title", target = "goodName")
    /** 属性名不同 */
    @Mapping(source = "good.price", target = "goodPrice")
    GoodInfoDTO from(GoodInfo good, GoodType type);

}
/**
 * N Object => 1 Object
 */
@Test
public void test() {
    GoodInfo goodInfo = GoodInfo.builder()
        .id(1L)
        .title("Mybatis技术内幕")
        .price(79.00)
        .order(100)
        .typeId(2L)
        .build();
    
    GoodType goodType = GoodType.builder()
        .id(2L)
        .name("计算机")
        .show(1)
        .order(3)
        .build();

    GoodInfoConvert convert = Mappers.getMapper(GoodInfoConvert.class);
    GoodInfoDTO goodInfoDTO = convert.from(goodInfo, goodType);
    System.out.println("goodInfo:    " + goodInfo);
    System.out.println("goodType:    " + goodType);
    System.out.println("goodInfoDTO: " + goodInfoDTO);

}

goodInfo: GoodInfo(id=1, title=Mybatis技术内幕, price=79.0, order=100, typeId=2)
goodType: GoodType(id=2, name=计算机, show=1, order=3)
goodInfoDTO: GoodInfoDTO(goodId=1, goodName=Mybatis技术内幕, goodPrice=79.0, typeName=计算机)

示例三

假设有一个 PO:Student

PO 里有个 sex 的 Integer 属性,0 表示 女,1 表示男,2 表示未知。
PO 里有个 age 的 Integer 属性,表示年龄。

目标 DTO:StudentDTO

DTO 里的 ageLevel 是根据 age 计算出来的年龄阶段描述:"少年"、"青年","中年"、"老年"。
DTO 里的 sexName 是根据 sex 属性计算出来的性别描述:"女"、"男"、"未知"。

PO:Student

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

/**
 * @date 2020-03-16
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Student {
    private Long id;
    private String name;
    private Integer age;
    private Integer sex;

    /** 入学时间 */
    private LocalDateTime admissionTime;

}
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDate;

/**
 * @date 2020-03-16
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class StudentDTO {
    private Long studentId;
    private String studentName;
    private Integer age;
    private String ageLevel;
    private String sexName;
    private LocalDate admissionDate;
}
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

import java.time.LocalDate;
import java.time.LocalDateTime;

/**
 * @date 2020-03-16
 */
@Mapper(imports = {CustomMapping.class})
public interface StudentConvert {

    @Mapping(source = "id", target = "studentId")
    @Mapping(source = "name", target = "studentName")
    @Mapping(source = "age", target = "age")
    @Mapping(target = "ageLevel", expression = "java(CustomMapping.ageLevel(student.getAge()))")
    @Mapping(target = "sexName", expression = "java(CustomMapping.sexName(student.getSex()))")
    @Mapping(source = "admissionTime", target = "admissionDate", dateFormat = "yyyy-MM-dd")
    StudentDTO from(Student student);

    default LocalDate map(LocalDateTime time) {
        return time.toLocalDate();
    }

}
public class CustomMapping {

    static final String[] SEX = {"女", "男", "未知"};

    public static String sexName(Integer sex) {

        if (sex < 0 && sex > 2){
            throw new IllegalArgumentException("invalid sex: " + sex);
        }
        return SEX[sex];
    }

    public static String ageLevel(Integer age) {
        if (age < 18) {
            return "少年";
        } else if (age >= 18 && age < 30) {
            return "青年";
        } else if (age >= 30 && age < 60) {
            return "中年";
        } else {
            return "老年";
        }
    }

}
import lombok.extern.slf4j.Slf4j;
import org.junit.Before;
import org.junit.Test;
import org.mapstruct.factory.Mappers;

import java.time.LocalDateTime;

import static org.junit.Assert.assertEquals;

/**
 * @date 2020-03-16
 */
@Slf4j
public class Demo3Test {

    private Student student;
    private StudentDTO studentDTO;

    @Before
    public void setUp() {
        student = Student.builder().id(1L).name("John").age(18).admissionTime(LocalDateTime.now()).sex(0).build();
    }

    @Test
    public void testCarToCarDTO() {


        StudentConvert studentConvert = Mappers.getMapper(StudentConvert.class);
        studentDTO = studentConvert.from(student);

        log.info("student:    {}", student);
        log.info("studentDTO: {}", this.studentDTO);

        assertEquals(student.getId(), this.studentDTO.getStudentId());
        assertEquals(student.getName(), this.studentDTO.getStudentName());
        //assertEquals(student.getAge(), studentDTO.getAge());
        assertEquals(student.getAdmissionTime().toLocalDate(), this.studentDTO.getAdmissionDate());
    }

}

运行结果:

09:34:39.134 [main] INFO com.asiainfo.bits.core.mapstruct.demo3.Demo3Test - student: Student(id=1, name=John, age=18, sex=0, admissionTime=2020-03-17T09:34:39.126)
09:34:39.141 [main] INFO com.asiainfo.bits.core.mapstruct.demo3.Demo3Test - studentDTO: StudentDTO(studentId=1, studentName=John, age=18, ageLevel=青年, sexName=女, admissionDate=2020-03-17)

你可能感兴趣的:(MapStruct 使用)