内容纲要

背景

在 Java 开发中,对象之间的映射是一个常见的需求。例如,在电商平台的订单系统中,领域模型需要映射为数据库实体,数据库实体又需要映射为领域模型以进行下一步的业务处理。手动编写这些映射代码不仅繁琐,而且容易出错。

为了解决这个问题,MapStruct 应运而生。MapStruct 是一个代码生成器,它通过注解自动生成对象映射代码,极大地简化了对象转换的过程。相比直接使用 BeanUtils 进行浅拷贝,MapStruct 具有以下优势:

MapStruct 的优势

  1. 高性能
    • MapStruct 在编译时生成映射代码,生成的代码是普通的 Java 方法调用,没有运行时反射开销。
    • 性能接近手写代码,适合高性能场景。
  2. 类型安全
    • 在编译时检查映射的正确性,避免运行时错误。
    • 如果字段类型不匹配或字段名错误,编译时会报错。
  3. 灵活性
    • 支持复杂映射逻辑,如自定义方法、条件映射、嵌套对象映射、多态映射等。
    • 支持 @BeforeMapping@AfterMapping 注解,可以在映射前后执行自定义逻辑。
  4. 支持批量映射
    • 可以轻松实现集合或列表的批量映射。
  5. 与 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 属性指定了当前映射器依赖的其他映射器(如 OrderProductMapperCustomerMapperPaymentMapper),用于处理嵌套对象的映射。
  • @Mapping
    • 指定字段映射规则。
    • target 属性指定目标对象的字段名。
    • source 属性指定源对象的字段名。
    • ignore = true 表示忽略该字段的映射。
    • qualifiedByName 属性指定使用 @Named 注解标记的方法来处理该字段的映射。
  • @BeforeMapping
    • 在映射开始之前执行的方法。
  • @AfterMapping
    • 在映射完成之后执行的方法。

映射接口的实现

当 Spring 启动后,MapStruct 会根据接口定义自动生成实现类。以下是生成的 toEntitytoDomain 方法的示例:

  • 嵌套字段映射
    • itemscustomerpayment 等字段的映射使用了其他 Mapper 的方法。
  • 自定义字段映射
    • orderNumtotalAmount 字段的映射使用了 @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,它一定会让你事半功倍!

最后修改日期: 2025年2月23日

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。