MapStruct 使用指南:从基础映射到 DDD 分层实践
这里写目录标题
MapStruct 使用指南:从基础映射到 DDD 分层实践
一、为什么需要 MapStruct
在 Java 分层架构中,对象转换是高频操作。以 DDD 四层架构为例:
- 用户接口层(DTO)与领域层(Domain)之间的转换
- 领域层与基础设施层(PO)之间的转换

手写转换代码存在几个典型问题:
- 效率低:每个字段都需要手写
get/set,一个几十字段的对象需要几十行代码。 - 维护成本高:领域模型字段变更时,所有转换代码都要同步修改。
- 易错:字段名拼写错误、类型不匹配、遗漏赋值等问题在编译期无法发现。
- 性能顾虑:反射工具(如
BeanUtils)虽然简洁,但存在性能损耗,且无法处理复杂映射逻辑。
MapStruct 在编译期生成转换代码,生成的代码与手写 get/set 性能一致,同时提供类型安全和灵活的映射配置。
二、基础用法:快速实现对象转换
2.1 添加依赖(Maven)
<properties>
<mapstruct.version>1.5.5.Final</mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
2.2 定义 Mapper 接口
@Mapper
public interface OrderMapper {
OrderMapper INSTANCE = Mappers.getMapper(OrderMapper.class);
OrderDTO toDTO(Order order);
Order toEntity(OrderDTO dto);
}
2.3 使用
Order order = new Order(1L, "ORD001", new BigDecimal("100"));
OrderDTO dto = OrderMapper.INSTANCE.toDTO(order);
对于 Spring Boot 项目,推荐使用 @Mapper(componentModel = "spring"),MapStruct 会生成带 @Component 的实现类,可以直接通过 @Autowired 注入,无需手动维护 INSTANCE。
三、进阶映射:处理现实中的复杂场景
3.1 字段名称不一致
使用 @Mapping 指定源字段和目标字段。
@Mapper
public interface OrderMapper {
@Mapping(source = "orderNo", target = "orderNumber")
OrderDTO toDTO(Order order);
}
3.2 字段类型不一致
MapStruct 内置了常见类型的转换(如基本类型与包装类、字符串与数字、枚举与字符串等)。对于无法自动转换的类型,可以在 Mapper 接口中定义 default 方法,MapStruct 会根据参数类型和返回类型自动调用。
@Mapper
public interface OrderMapper {
default LocalDateTime toDateTime(String dateStr) {
return LocalDateTime.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
OrderDTO toDTO(Order order); // String 字段到 LocalDateTime 字段的转换会自动调用 toDateTime
}
3.3 忽略某些字段
使用 @Mapping(target = "fieldName", ignore = true)。
@Mapper
public interface OrderMapper {
@Mapping(target = "createTime", ignore = true)
@Mapping(target = "version", ignore = true)
OrderDTO toDTO(Order order);
}
3.4 多个源对象映射
当目标对象需要从多个源对象组装时,定义多个参数的映射方法。
@Mapper
public interface OrderMapper {
@Mapping(source = "order.id", target = "orderId")
@Mapping(source = "user.name", target = "userName")
OrderDTO toDTO(Order order, User user);
}
注意:当多个源对象存在同名属性时,MapStruct 会使用最后一个参数的属性值(覆盖前面)。为避免歧义,建议对所有字段显式指定 source。
3.5 复用其他 Mapper映射
通过 uses 属性引用其他 Mapper,MapStruct 会自动从 Spring 容器(或通过工厂)获取实例并调用。
@Mapper(componentModel = "spring", uses = AddressMapper.class)
public interface OrderMapper {
OrderDTO toDTO(Order order);
}
此时AddressMapper中定义映射关系会自动被OrderMapper中复用。
注:uses 属性可以指定任意类,并复用其任意映射,类其中的入参为A出参为B的方法都可以被看做是一种映射。
如果OrderMapper的某个属性映射想引用其它mapper的指定映射方法,可以通过qualifiedByName指定。
@Component
public class UserInfoConverter {
public UserInfo buildUserInfoFromOrder(Order order) {
if (order == null) return null;
UserInfo info = new UserInfo();
info.setId(order.getUserId());
info.setName(order.getUserName());
return info;
}
}
@Component
public class UserInfoConverterUserInfoConverter
// 指定引用其它convert中定义的映射关系
@Mapping(source = "order", target = "userInfo", qualifiedByName = "buildUserInfoFromOrder
OrderDTO toDTO(Order order);
}
3.6 需要复杂计算的字段
某些字段无法通过简单的字段映射得到,例如 fullName 需要由 firstName 和 lastName 拼接。
- 自定义
default方法
在 Mapper 接口中先定义一个标准的转换,然后再创建一个其它的 default 方法(方法名称不同),方法的实现中先引用标准转换实现,然后再针对一些复杂的映射关系手动处理。
@Mapper(componentModel = "spring")
public interface UserMapper {
// 标准转换
UserDTO toDTO(User user);
default UserDTO toDTOWithFullName(User user) {
// 先调用自动映射
UserDTO dto = toDTO(user);
// 其他字段手动赋值
dto.setFullName(user.getFirstName() + " " + user.getLastName());
return dto;
}
}
缺点:
- 接口中同时存在
toDTO和toDTOWithFullName,调用方不知道明确知道该用哪个,容易误用。在实际项目中,调用方希望有统一的入口,而不是A对象调用toDTO去转换,B对象又要调用toDTOWithFullName去转换。
- 使用
AfterMapping注解
此时可以使用 @AfterMapping 在自动映射完成后进行后处理,同时保持对外暴露的方法名不变。
java@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(target = "fullName", ignore = true)
UserDTO toDTO(User user);
// 后处理:自动映射完成后,再计算fullName
@AfterMapping
default void setFullName(@MappingTarget UserDTO dto, User user) {
dto.setFullName(user.getFirstName() + " " + user.getLastName());
}
}
如上MappingTarget标识的参数dto是最终需要映射的对象,其它参数user是指源对象。
执行流程:调用toDTO时先自动映射所有同名字段,然后会自动调用你写的@AfterMapping方法,你再把fullName算出来塞进去。这样对外调用还是userMapper.toDTO(user),干干净净。
原则:遇到复杂转换,优先用default方法或@AfterMapping,少用expression(字符串表达式,编译不检查,容易翻车)。
四、在 DDD 分层架构中设计 Assembler 与 Converter
4.1 分层职责与转换器定位
| 层级 | 对象形态 | 转换器类型 | 命名示例 |
|---|---|---|---|
| 用户接口层 / 应用层 | DTO ↔ 领域对象 | Assembler | OrderAssembler |
| 基础设施层 | 领域对象 ↔ PO | Converter | OrderConverter |
4.2 按聚合根组织转换器
每个聚合根(拥有全局唯一标识的业务实体,如 Order、User)对应一个 Assembler 和一个 Converter。
order/
├── interfaces/assembler/
│ └── OrderAssembler.java // DTO ↔ Domain
├── infrastructure/converter/
│ └── OrderConverter.java // Domain ↔ PO
4.3 Assembler 示例
@Mapper(componentModel = "spring", uses = AddressConverter.class)
public interface OrderAssembler {
// 多个 DTO 转换方法
Order toDomain(CreateOrderRequest request);
Order toDomain(UpdateOrderRequest request);
OrderResponse toResponse(Order order);
OrderSummaryResponse toSummaryResponse(Order order);
List<OrderResponse> toResponseList(List<Order> orders);
}
4.4 Converter 示例
@Mapper(componentModel = "spring")
public interface OrderConverter {
OrderPO toPO(Order order);
Order toDomain(OrderPO po);
List<Order> toDomainList(List<OrderPO> poList);
}
4.5 Repository 实现中使用 Converter
@Repository
public class OrderRepositoryImpl implements OrderRepository {
@Autowired
private OrderMapper orderMapper; // MyBatis / JPA Mapper
@Autowired
private OrderConverter converter;
@Override
public void save(Order order) {
OrderPO po = converter.toPO(order);
orderMapper.insert(po);
}
@Override
public Order findById(OrderId id) {
OrderPO po = orderMapper.selectById(id.getValue());
return converter.toDomain(po);
}
}
4.6 设计原则总结
- 围绕聚合根:每个聚合根只保留一个 Assembler 和一个 Converter。
- 职责分离:Assembler 不依赖 Converter,反之亦然。
- 共享值对象:对于被多个聚合根使用的值对象(如
Address),可以定义独立的 Converter,并通过uses引入。
五、配置建议与常见问题
5.1 编译期检查未映射字段
在 @Mapper 中添加 unmappedTargetPolicy = ReportingPolicy.ERROR,强制要求所有目标字段都被显式映射,避免遗漏。
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface OrderMapper {
// ...
}
5.2 与 Lombok 配合
需要在 maven-compiler-plugin 的 annotationProcessorPaths 中同时包含 mapstruct-processor 和 lombok,否则 MapStruct 无法识别 Lombok 生成的 getter/setter。
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
5.3 调试
MapStruct 生成的实现类位于 target/generated-sources/annotations/ 目录下,可以直接查看生成代码以排查映射问题。
更多推荐




所有评论(0)