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 需要由 firstNamelastName 拼接。

  1. 自定义 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;
    }
}

缺点:

  • 接口中同时存在 toDTOtoDTOWithFullName,调用方不知道明确知道该用哪个,容易误用。在实际项目中,调用方希望有统一的入口,而不是A对象调用toDTO去转换,B对象又要调用toDTOWithFullName去转换。
  1. 使用 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 ↔ 领域对象AssemblerOrderAssembler
基础设施层领域对象 ↔ POConverterOrderConverter

4.2 按聚合根组织转换器

每个聚合根(拥有全局唯一标识的业务实体,如 OrderUser)对应一个 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-pluginannotationProcessorPaths 中同时包含 mapstruct-processorlombok,否则 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/ 目录下,可以直接查看生成代码以排查映射问题。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐