如何在 Mapstruct 中进行嵌套映射?

1、概览

MapStruct 通过注解来定义 POJO 属性间的映射关系。其 Maven 插件会读取注解中定义的元数据,自动生成 Mapper 工具类。此外,它还支持通过自定义映射工具实现细粒度控制。

本文将带你了解如何用 MapStruct 将层次化的源实体嵌套属性映射到扁平化的目标实体

2、用例

类体系图如下。通过源实体 Order 和目标实体 OrderDto 来演示 MapStruct 库执行嵌套映射的能力:

类体系结构

源实体 Order 表示具有嵌套结构的复杂对象,包含 CustomerProduct

public class Order {
    private Customer customer;
    private Product product;
    
    // Getter/Setter 省略
}
public class Customer {
    private String name;
    private Address address;
    
    // Getter/Setter 省略
}
public class Product {
    private String name;
    private double price;
    
    // Getter/Setter 省略
}

此外,Customer 实体具有一个类型为 Addressaddress 属性:

public class Address {
    private String city;
    private String zipCode;
    
    // Getter/Setter 省略
}

目标实体 OrderDtoOrder 实体的简化扁平化版本。 它包含诸如 customerNamecustomerCitycustomerZipCodeproductNameproductPrice 等字段,这些字段均来自 Order 实体的嵌套结构:

public class OrderDto {
    private String customerName;
    private String customerCity;
    private String customerZipCode;
    private String productName;
    private double productPrice;
    
    // Getter/Setter 省略
}

接下来看看如何使用 MapStruct 库从 Order 对象创建 OrderDto

3、使用 @Mapping 注解

仅需一对一映射且无需定制化处理的场景,直接使用 @Mapping 注解即可实现。

首先,根据 MapStruct 库的规范,我们需要定义带有 @Mapper 注解的 Mapper 接口:

@Mapper
public interface OrderMapper {
    OrderMapper INSTANCE = Mappers.getMapper(OrderMapper.class);

    @Mapping(source = "customer.name", target = "customerName")
    @Mapping(source = "product.name", target = "productName")
    @Mapping(source = "product.price", target = "productPrice")
    @Mapping(source = "customer.address.city", target = "customerCity")
    @Mapping(expression = "java(order.getCustomer().getAddress().getZipCode())", 
      target = "customerZipCode")
    OrderDto orderToOrderDto(Order order);
}

OrderMapper 接口中,orderToOrderDto() 方法将 Order 对象转换为 OrderDto 对象。该方法上的 @Mapping 注解用于定义从 Order 对象属性(包括嵌套属性)到目标 OrderDto 属性的映射关系。

此外,我们通过 . 导航访问 Order#customerOrder#product 等嵌套属性。同理,也通过此方式访问 Customer#addresscity 属性。同时,我们还可自由使用 Java 表达式,例如这里通过表达式填充了 OrderDto#customerZipCode 属性。

最终,当我们构建代码时,Maven 插件 MapStruct Processor 会生成 OrderMapperImpl 类:

public class OrderMapperImpl implements OrderMapper {
    @Override
    public OrderDto orderToOrderDto(Order order) {
        if ( order == null ) {
            return null;
        }

        OrderDto orderDto = new OrderDto();

        orderDto.setCustomerName( orderCustomerName( order ) );
        orderDto.setProductName( orderProductName( order ) );
        orderDto.setProductPrice( orderProductPrice( order ) );
        orderDto.setCustomerCity( orderCustomerAddressCity( order ) );
        orderDto.setCustomerZipCode( order.getCustomer().getAddress().getZipCode() );
        return orderDto;
    }

    private String orderCustomerName(Order order) {
        Customer customer = order.getCustomer();
        if ( customer == null ) {
            return null;
        }
        return customer.getName();
    }

    private String orderCustomerAddressCity(Order order) {
        Customer customer = order.getCustomer();
        if ( customer == null ) {
            return null;
        }
        Address address = customer.getAddress();
        if ( address == null ) {
            return null;
        }
        return address.getCity();
    }

    //。。。其他私有方法
}

Maven 插件会解析 @Mapping 注解,并智能生成 orderToOrderDto() 方法及相关的私有工具方法,用于提取 Order 对象的嵌套属性。但填充 OrderDto#customerZipCode 属性时,直接使用了注解中的 Java 表达式。与其他场景不同,此处未生成任何 null 值检查逻辑。

接下来,调用生成的 Mapper 类:

void givenOrder_whenMapToOrderDto_thenMapNestedAttributes() {
     Order order = createSampleOrderObject();

     OrderDto orderDto = OrderMapper.INSTANCE.orderToOrderDto(order);

     assertEquals("John Doe", orderDto.getCustomerName());
     assertEquals("New York", orderDto.getCustomerCity());
     assertEquals("10001", orderDto.getCustomerZipCode());
     assertEquals("Laptop", orderDto.getProductName());
     assertEquals(1200.00, orderDto.getProductPrice());
 }

createSampleOrder() 方法用于创建示例 Order 对象,该对象作为参数传递给 Mapper 的 orderToOrderDto() 方法。最终,通过断言来验证所有嵌套属性均已正确映射到目标 OrderDto 对象。

4、使用抽象 Mapper 实现

@Mapping 注解无法满足映射过程中的灵活需求时,可通过抽象 Mapper 类实现:

@Mapper
public abstract class AbstractOrderMapper {
    public static final AbstractOrderMapper INSTANCE = Mappers.getMapper(AbstractOrderMapper.class);

    public OrderDto orderToOrderDto(Order order) {
        OrderDto orderDto = applyCustomMappings(order);
        orderDto = mapCustomer(order);
        mapProduct(order, orderDto);
        return orderDto;
    }

    @Mapping(source = "customer.name", target = "customerName")
    @Mapping(source = "customer.address.city", target = "customerCity")
    @Mapping(source = "customer.address.zipCode", target = "customerZipCode")
    protected abstract OrderDto mapCustomer(Order order);

    @Mapping(source = "product.name", target = "productName")
    @Mapping(source = "product.price", target = "productPrice")
    protected abstract void mapProduct(Order order, @MappingTarget OrderDto orderDto);
}

首先,orderToOrderDto() 方法会调用 applyCustomMappings() 方法来初始化 OrderDto 对象。此外,applyCustomMappings() 可实现特定逻辑或调用下游服务来创建 OrderDto 对象。接着,我们会调用如 mapCustomer()mapProduct() 等抽象方法。这些抽象方法带有与之前章节讨论过的基本 @Mapping 注解,用于从 Order 对象更新 OrderDto 对象中 customerproduct 属性的值。

在另一种场景中,我们可以选择将哪些方法保留为抽象方法,哪些方法具体实现以进行定制化。此外,我们还可以使用其他 MapStruct 功能或注解(例如 @ObjectFactory)来实现需要精细控制的定制化逻辑。

接下来,构建该类以生成 AbstractOrderMapperImpl 类:

public class AbstractOrderMapperImpl extends AbstractOrderMapper {
    @Override
    protected OrderDto mapCustomer(Order order) {
        if ( order == null ) {
            return null;
        }

        OrderDto orderDto = new OrderDto();

        orderDto.setCustomerName( orderCustomerName( order ) );
        orderDto.setCustomerCity( orderCustomerAddressCity( order ) );
        orderDto.setCustomerZipCode( orderCustomerAddressZipCode( order ) );

        return orderDto;
    }

    @Override
    protected void mapProduct(Order order, OrderDto orderDto) {
        if ( order == null ) {
            return;
        }

        orderDto.setProductName( orderProductName( order ) );
        orderDto.setProductPrice( orderProductPrice( order ) );
    }

    private String orderCustomerName(Order order) {
        Customer customer = order.getCustomer();
        if ( customer == null ) {
            return null;
        }
        return customer.getName();
    }
    // 其他生成的私有方法,用于从 Order 对象的嵌套属性中提取属性值
}

Maven 插件解析了 mapCustomer()mapProduct() 方法上的 @Mapping 注解元数据,并生成了它们的实现。继承自父类 AbstractOrderMapperAbstractOrderMapperImpl#orderToOrderDto() 方法会调用这些已实现的方法。

现在,可以运行 AbstractOrderMapper#orderToOrderDto() 方法并验证映射结果:

void givenOrder_whenMapToOrderDto_thenMapNestedAttributesWithAbstractMapper() {
    Order order = createSampleOrderObject();

    OrderDto orderDto = AbstractOrderMapper.INSTANCE.orderToOrderDto(order);

    assertEquals("John Doe", orderDto.getCustomerName());
    assertEquals("New York", orderDto.getCustomerCity());
    assertEquals("10001", orderDto.getCustomerZipCode());
    assertEquals("Laptop", orderDto.getProductName());
    assertEquals(1200.00, orderDto.getProductPrice());
}

首先,我们调用 createSampleOrderObject() 方法创建示例 Order 对象。接着实例化 AbstractOrderMapper 的实现类。随后将示例 Order 对象传递给 AbstractOrderMapper#orderToOrderDto() 方法。最终验证 OrderDto 对象各属性的值。

5、总结

本文介绍了如何通过 MapStruct 将源实体的嵌套属性映射到目标实体。


Ref:https://www.baeldung.com/mapstruct-nested-mapping