美文网首页
MapStruct 使用

MapStruct 使用

作者: holmes000 | 来源:发表于2020-07-17 15:52 被阅读0次

    对象映射工具的由来

    大型项目采用分层开发,每层的数据模型都不同:在持久化层,模型层为 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 使用

          本文链接:https://www.haomeiwen.com/subject/nvighktx.html