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.cityprivate 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) // 创建时忽略 IDUser 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 {@MapMappingMap<String, String> toStringMap(Config config);@MapMappingConfig 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 {@BeforeMappingprotected void before(User user, @MappingTarget UserDTO dto) {System.out.println("开始映射: " + user.getId());}@AfterMappingprotected 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 ↔ DTOApplicationMapper
:DTO ↔ Command/EventPersistenceMapper
: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 性能优化建议
- 复用 Mapper 实例(Spring Bean)
- 避免在循环中创建 Mapper(
Mappers.getMapper()
有反射开销) - 使用
@InheritConfiguration
减少重复配置 - 禁用不必要的空值检查(如已知非空)
七、真实企业架构案例
场景:电商平台订单详情聚合
@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 | 自定义限定符 |