内容纲要
背景
在 Java 开发中,对象之间的映射是一个常见的需求。例如,在电商平台的订单系统中,领域模型需要映射为数据库实体,数据库实体又需要映射为领域模型以进行下一步的业务处理。手动编写这些映射代码不仅繁琐,而且容易出错。
为了解决这个问题,MapStruct 应运而生。MapStruct 是一个代码生成器,它通过注解自动生成对象映射代码,极大地简化了对象转换的过程。相比直接使用 BeanUtils
进行浅拷贝,MapStruct 具有以下优势:
MapStruct 的优势
- 高性能:
- MapStruct 在编译时生成映射代码,生成的代码是普通的 Java 方法调用,没有运行时反射开销。
- 性能接近手写代码,适合高性能场景。
- 类型安全:
- 在编译时检查映射的正确性,避免运行时错误。
- 如果字段类型不匹配或字段名错误,编译时会报错。
- 灵活性:
- 支持复杂映射逻辑,如自定义方法、条件映射、嵌套对象映射、多态映射等。
- 支持
@BeforeMapping
和@AfterMapping
注解,可以在映射前后执行自定义逻辑。
- 支持批量映射:
- 可以轻松实现集合或列表的批量映射。
- 与 JPA 和 Spring 集成:
- 支持与 JPA 实体、Spring Bean 等框架无缝集成。
实战
添加依赖
在 build.gradle
中添加 MapStruct 依赖:
dependencies {
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
}
定义映射对象
领域模型Domain
public class Order {
private String orderNum;
private List<OrderProduct> items;
private Customer customer;
private Payment payment;
private BigDecimal totalAmount;
}
public class OrderProduct {
private Long id;
private String name;
private Integer amount;
private BigDecimal price;
}
public class Payment {
private Long id;
private String address;
private PayMethodType type;
private BigDecimal totalPrice;
}
public class Customer {
private Long id;
private String name;
private String email;
private String phoneNumber;
}
数据库实体Entity
@Entity
@Table(name = "`order`")
public class OrderEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "order_id")
private List<OrderProductEntity> items;
@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "customer_id", referencedColumnName = "id")
private CustomerEntity customer;
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "payment_id", referencedColumnName = "id")
private PaymentEntity payment;
}
@Entity
@Table(name = "order_product")
public class OrderProductEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "order_id")
private Long orderId;
private String name;
private Integer amount;
private BigDecimal price;
}
@Entity
@Table(name = "customer")
public class CustomerEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private String phoneNumber;
}
@Entity
@Table(name = "payment")
public class PaymentEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String address;
@Enumerated(value = EnumType.STRING)
private PayMethodType type;
private BigDecimal totalPrice;
}
定义Mapstruct映射接口
@Mapper(uses = {OrderProductMapper.class, CustomerMapper.class, PaymentMapper.class})
public interface OrderMapper {
OrderMapper ORDER_MAPPER = Mappers.getMapper(OrderMapper.class);
@Mapping(target = "id", ignore = true)
OrderEntity toEntity(Order domain);
@Mapping(target = "orderNum", source = "id", qualifiedByName = "getOrderNum")
@Mapping(target = "totalAmount", source = "items", qualifiedByName = "calculateTotalAmount")
Order toDomain(OrderEntity entity);
List<Order> toDomains(List<OrderEntity> entities);
@Named("calculateTotalAmount")
default BigDecimal calculateTotalAmount(List<OrderProductEntity> items) {
return items.stream()
.map(product -> product.getPrice().multiply(BigDecimal.valueOf(product.getAmount())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
@Named("getOrderNum")
default String getOrderNum(Long id) {
return "ORDER-" + id;
}
@BeforeMapping
default void preProcess() {
System.out.println("执行前置处理......");
}
@AfterMapping
default void postProcess() {
System.out.println("执行后置处理......");
}
}
注解详解
@Mapper
:- 标记一个接口为 MapStruct 的映射器接口。
uses
属性指定了当前映射器依赖的其他映射器(如OrderProductMapper
、CustomerMapper
、PaymentMapper
),用于处理嵌套对象的映射。
@Mapping
:- 指定字段映射规则。
target
属性指定目标对象的字段名。source
属性指定源对象的字段名。ignore = true
表示忽略该字段的映射。qualifiedByName
属性指定使用@Named
注解标记的方法来处理该字段的映射。
@BeforeMapping
:- 在映射开始之前执行的方法。
@AfterMapping
:- 在映射完成之后执行的方法。
映射接口的实现
当 Spring 启动后,MapStruct 会根据接口定义自动生成实现类。以下是生成的 toEntity
和 toDomain
方法的示例:
- 嵌套字段映射:
items
、customer
、payment
等字段的映射使用了其他 Mapper 的方法。
- 自定义字段映射:
orderNum
和totalAmount
字段的映射使用了@Named
定义的方法。
批量映射的实现
批量映射方法 toDomains
的实现是通过遍历调用单条记录的映射方法完成的:
@BeforeMapping
和@AfterMapping
:- 在每个映射方法中,
@BeforeMapping
和@AfterMapping
方法都会生效。
- 在每个映射方法中,
测试
插入测试数据
首先,插入一些测试数据:
INSERT INTO customer (name, email, phone_number)
VALUES ('John Doe', 'john.doe@example.com', '123-456-7890'),
('Jane Smith', 'jane.smith@example.com', '987-654-3210');
INSERT INTO payment (address, type, total_price)
VALUES ('123 Main St, City, Country', 'CREDIT_CARD', 199.99),
('456 Elm St, City, Country', 'ALIPAY', 299.99);
INSERT INTO `order` (customer_id, payment_id)
VALUES (1, 1), -- John Doe 的订单
(2, 2); -- Jane Smith 的订单
INSERT INTO order_product (order_id, name, amount, price)
VALUES (1, 'iPhone 15', 2, 999.99), -- 订单 1 的商品
(1, 'MacBook Pro', 1, 1999.99), -- 订单 1 的商品
(2, 'AirPods Pro', 3, 249.99); -- 订单 2 的商品
查询与映射
定义查询方法:
public Order findAny() {
return orderRepository.findAll().stream().findAny()
.map(ORDER_MAPPER::toDomain)
.orElse(new Order());
}
public List<Order> queryAll() {
return ORDER_MAPPER.toDomains(orderRepository.findAll());
}
测试用例
@Test
void test_single_mapping() {
Order order = orderUseCase.getOrder();
assertEquals("ORDER-1", order.getOrderNum());
assertEquals(2, order.getItems().size());
assertEquals(new BigDecimal("3999.97"), order.getTotalAmount());
assertEquals("John Doe", order.getCustomer().getName());
assertEquals(PayMethodType.CREDIT_CARD, order.getPayment().getType());
}
@Test
void test_batch_mapping() {
List<Order> orders = orderUseCase.getAllOrders();
assertEquals(2, orders.size());
assertEquals("ORDER-1", orders.get(0).getOrderNum());
assertEquals(2, orders.get(0).getItems().size());
assertEquals(new BigDecimal("3999.97"), orders.get(0).getTotalAmount());
assertEquals("John Doe", orders.get(0).getCustomer().getName());
assertEquals(PayMethodType.CREDIT_CARD, orders.get(0).getPayment().getType());
assertEquals("ORDER-2", orders.get(1).getOrderNum());
assertEquals(1, orders.get(1).getItems().size());
assertEquals(new BigDecimal("749.97"), orders.get(1).getTotalAmount());
assertEquals("Jane Smith", orders.get(1).getCustomer().getName());
assertEquals(PayMethodType.ALIPAY, orders.get(1).getPayment().getType());
}
总结
通过 MapStruct,我们可以优雅地处理对象映射问题,避免手动编写繁琐的映射代码。它不仅提高了开发效率,还增强了代码的可维护性和可读性。
在实际项目中,MapStruct 尤其适用于以下场景:
- DTO 和 Entity 之间的映射。
- API 请求体和响应体的映射。
- 复杂对象之间的转换。
如果你正在为对象映射问题头疼,不妨试试 MapStruct,它一定会让你事半功倍!
留言