目录

MapStruct用法和实践

MapStruct用法和实践

一、MapStruct 用法

1. 嵌套对象深度映射(Deep Mapping)


// 源对象
public class User {
    private Address address;
    // getter/setter
}

public class Address {
    private String city;
    private String street;
}

// 目标对象
public class UserDTO {
    private String city;      // 直接映射 user.address.city
    private String fullAddr;  // user.address.city + street
}

@Mapper
public interface UserMapper {
    @Mapping(target = "city", source = "address.city")
    @Mapping(target = "fullAddr", source = "address")
    UserDTO toDTO(User user);

    // 自定义组合逻辑
    default String addressToFullAddr(Address address) {
        if (address == null) return null;
        return (address.getCity() != null ? address.getCity() : "") + " " +
               (address.getStreet() != null ? address.getStreet() : "");
    }
}

✅ 自动生成:


dto.setCity(user.getAddress().getCity());
dto.setFullAddr(addressToFullAddr(user.getAddress()));

2. 集合映射(List/Set/Map ↔ List/Set/Map)


@Mapper
public interface OrderMapper {
    List<OrderDTO> toDTOs(List<Order> orders);
    Set<ProductDTO> toProductDTOs(Set<Product> products);
    Map<String, String> toStringMap(Map<String, Object> map); // 需自定义转换
}

✅ 自动生成遍历逻辑,无需手动 stream().map().collect()

自定义集合元素转换:

@Mapper(uses = CustomConverters.class)
public interface OrderMapper {
    List<OrderSummaryDTO> toSummaries(List<Order> orders);
}

@Component
public class CustomConverters {
    public String formatAmount(BigDecimal amount) {
        return amount == null ? "0.00" : amount.setScale(2).toString();
    }
}

3. 双向映射(Bidirectional Mapping)


@Mapper
public interface UserMapper {

    // 正向
    @Mapping(target = "birthDate", dateFormat = "yyyy-MM-dd")
    UserDTO toDTO(User user);

    // 反向(自动推断)
    @InheritInverseConfiguration
    @Mapping(target = "id", ignore = true) // 创建时忽略 ID
    User fromDTO(UserDTO dto);
}

🔁 @InheritInverseConfiguration 自动继承正向配置,反向映射。


4. 更新现有对象(Update Mapping)

常用于 PUT/PATCH 接口更新实体。


@Mapper
public interface UserMapper {
    @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
    void updateUser(@MappingTarget User user, UserDTO dto);
}

✅ 仅拷贝非 null 字段,避免覆盖原有值。


5. 条件映射(Conditional Mapping)


@Mapper
public interface UserMapper {
    @Mapping(target = "status", source = "status", qualifiedByName = "statusToCode")
    UserDTO toDTO(User user);

    @Named("statusToCode")
    default String mapStatus(UserStatus status) {
        if (status == null) return null;
        return switch (status) {
            case ACTIVE -> "A";
            case INACTIVE -> "I";
            default -> "U";
        };
    }
}

qualifiedByName 指定使用哪个自定义方法。


6. 多源映射(Multi-Source Mapping)


@Mapper
public interface UserDetailMapper {
    @Mapping(target = "userName", source = "user.name")
    @Mapping(target = "roleName", source = "role.name")
    @Mapping(target = "deptName", source = "dept.title")
    UserDetailDTO toDTO(User user, Role role, Department dept);
}

✅ 适用于 VO 聚合多个 Entity 的场景。


7. Map 与 Object 映射


@Mapper
public interface ConfigMapper {
    @MapMapping
    Map<String, String> toStringMap(Config config);

    @MapMapping
    Config toConfig(Map<String, String> map);
}

✅ 自动生成 get/set 映射。


8. Builder 模式支持(Lombok @Builder)


@Mapper(builder = @Builder(disableBuilder = false))
public interface UserMapper {
    UserDTO toDTO(User user); // 自动调用 UserDTO.builder().name(...).build()
}

✅ 支持不可变对象构建。


9. 泛型映射(Generic Mapping)


@Mapper
public interface GenericMapper {
    <T, R> List<R> mapList(List<T> source, Class<R> targetClass);
}

⚠️ 泛型擦除问题:需在运行时传入 Class 对象。


10. 生命周期回调(Lifecycle Callbacks)


@Mapper
public abstract class UserMapper {

    @BeforeMapping
    protected void before(User user, @MappingTarget UserDTO dto) {
        System.out.println("开始映射: " + user.getId());
    }

    @AfterMapping
    protected void after(@MappingSource User user, @MappingTarget UserDTO dto) {
        dto.setAgeGroup(calculateAgeGroup(user.getAge()));
    }

    public abstract UserDTO toDTO(User user);

    private String calculateAgeGroup(int age) {
        return age < 18 ? "CHILD" : age < 60 ? "ADULT" : "SENIOR";
    }
}

✅ 适用于审计、日志、后处理逻辑。


二、MapStruct 与 DDD(领域驱动设计)融合实践

1. 实体 ↔ DTO 转换分层设计


Web Layer:     OrderRequest  → OrderDTO
Application:   OrderDTO     → Command
Domain:        Command      → Order (Entity)
Persistence:   Order        → OrderEntity (JPA)
映射层划分:
  • WebMapper:Request/Response ↔ DTO
  • ApplicationMapper:DTO ↔ Command/Event
  • PersistenceMapper:Entity ↔ JPA Entity

@Mapper(componentModel = "spring")
public interface WebOrderMapper {
    @Mapping(target = "customerId", source = "customer.id")
    OrderDTO toDTO(CreateOrderRequest request);
}

2. 值对象(Value Object)映射


public class Address {
    private String street;
    private String city;
    // 不可变
}

@Mapper
public interface AddressMapper {
    @Mapping(target = "street", source = "street")
    @Mapping(target = "city", source = "city")
    Address createAddress(String street, String city);
}

✅ 使用 @Context 或工厂方法创建 VO。

三、MapStruct 企业级配置与最佳实践

1. 全局配置(@MapperConfig)


@MapperConfig(
    componentModel = "spring",
    nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
    nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
    unmappedTargetPolicy = ReportingPolicy.WARN,
    uses = { DateMapper.class, CustomConverters.class }
)
public class BaseMappingConfig {}

然后所有 Mapper 继承:


@Mapper(config = BaseMappingConfig.class)
public interface UserMapper { ... }

2. Spring 集成(推荐方式)


@Mapper(componentModel = "spring")
public interface UserMapper {
    UserDTO toDTO(User user);
}

✅ 自动生成 Spring Bean,可直接 @Autowired


3. 错误策略配置


@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface UserMapper { ... }
  • IGNORE:忽略未映射字段(默认)
  • WARN:编译警告
  • ERROR:编译报错(推荐用于生产)

4. 空值处理策略

策略说明
NullValuePropertyMappingStrategy.SET_TO_NULL源为 null 时,目标也设为 null
IGNORE忽略 null 值(常用于更新)
SET_TO_DEFAULT设为默认值(如 0, false)

四、MapStruct 与主流框架集成

1. Spring Boot(推荐 starter)


<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-spring-extensions</artifactId>
    <version>1.0.0.CR1</version>
</dependency>

支持 @Mapper SpringBean 自动注入。


2. Lombok 集成(避免冲突)

问题:Lombok 和 MapStruct 都是 APT,处理顺序冲突。
解决方案 1:Maven 配置 processor 顺序

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <annotationProcessorPaths>
            <path>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </path>
            <path>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>${mapstruct.version}</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>
解决方案 2:使用 lombok.config

# lombok.config
lombok.addLombokGeneratedAnnotation = true

3. Quarkus / GraalVM 原生镜像

  • MapStruct 生成的代码无反射,天然支持原生编译
  • 无需额外配置反射规则。

五、MapStruct 调试与问题排查

1. 查看生成的代码

  • Maven 项目:target/generated-sources/annotations/
  • 手动编译:-s 指定生成目录

2. 常见编译错误

错误原因解决
Can't map property ...字段不存在或类型不匹配使用 @Mapping(ignore=true) 或自定义转换
Ambiguous mapping methods多个转换方法匹配使用 @Named + qualifiedByName
No property named ...拼写错误检查 getter/setter

六、MapStruct 性能优化建议

  1. 复用 Mapper 实例(Spring Bean)
  2. 避免在循环中创建 MapperMappers.getMapper() 有反射开销)
  3. 使用 @InheritConfiguration 减少重复配置
  4. 禁用不必要的空值检查(如已知非空)

七、真实企业架构案例

场景:电商平台订单详情聚合


@Mapper(uses = {DateMapper.class, PriceFormatter.class})
public interface OrderDetailMapper {

    @Mapping(target = "customerName", source = "customer.name")
    @Mapping(target = "productName", source = "product.name")
    @Mapping(target = "totalPrice", source = "order.total", qualifiedByName = "formatPrice")
    @Mapping(target = "items", source = "orderItems")
    OrderDetailView toView(Order order, Customer customer, Product product);

    List<OrderItemDTO> toItemDTOs(List<OrderItem> items);
}

✅ 一行代码聚合三个服务的数据。


八、总结:MapStruct 使用 checklist

✅ 是否使用 @Mapper(componentModel = "spring")
✅ 是否配置 unmappedTargetPolicy = ERROR
✅ 是否使用 @InheritInverseConfiguration 减少重复?
✅ 是否避免在循环中调用 Mappers.getMapper()
✅ 是否为日期、枚举等配置了自定义转换器?
✅ 是否在 pom.xml 正确配置了 annotationProcessorPaths?
✅ 是否检查了生成的代码是否符合预期?


九、附录:MapStruct 常用注解速查表

注解用途
@Mapper定义映射接口
@Mapping字段级映射配置
@Mappings多个 @Mapping
@InheritConfiguration继承配置
@InheritInverseConfiguration反向继承
@BeanMapping方法级配置(如 null 策略)
@AfterMapping / @BeforeMapping生命周期回调
@Context传递上下文对象
@Named标记转换方法
@Qualifier自定义限定符