对象映射工具的由来
大型项目采用分层开发,每层的数据模型都不同:在持久化层,模型层为 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)