26. MapStruct 映射框架
MapStruct 是一个 Java Bean 映射框架,通过注解在编译期自动生成类型转换代码,零反射、零运行时开销。
1. 为什么用 MapStruct
Section titled “1. 为什么用 MapStruct”Java 项目里经常要在不同层之间转换对象:
Entity → DTO → VO手写 get/set 又累又容易漏字段。常见方案对比:
| 方案 | 实现原理 | 性能 | 类型检查 |
|---|---|---|---|
| 手写属性赋值 | 直接编写代码 | 最高 | 编译期安全 |
| BeanUtils.copyProperties | 基于反射 | 较低 | 运行时检查 |
| ModelMapper | 反射 + 规则推断 | 较低 | 运行时检查 |
| MapStruct | 编译期生成代码 | 接近手写性能 | 编译期检查 |
MapStruct 的核心优势:生成的代码就是普通的 get/set,可以直接去 target/generated-sources 里看到它。
2. 快速上手
Section titled “2. 快速上手”2.1. 引入依赖
Section titled “2.1. 引入依赖”<properties> <mapstruct.version>1.5.5.Final</mapstruct.version></properties>
<dependencies> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${mapstruct.version}</version> </dependency></dependencies>
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <annotationProcessorPaths> <!-- ⚠️ 同时用 Lombok 时,Lombok 必须排在 MapStruct 前面 --> <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> </plugins></build>⚠️ Lombok 的注解处理器必须排在 MapStruct 前面,否则 MapStruct 处理时还没有
getter/setter,会报找不到属性的错误。
2.2. 简单示例
Section titled “2.2. 简单示例”// 实体类public class User { private Long id; private String username; private String email; // getter/setter...}
// DTOpublic class UserDTO { private Long id; private String username; private String email; // getter/setter...}// 定义 Mapper 接口,加上 @Mapper 注解就够了@Mapperpublic interface UserMapper { UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
UserDTO toDTO(User user); User toEntity(UserDTO dto);}// 使用User user = new User(1L, "张三", "zhangsan@example.com");UserDTO dto = UserMapper.INSTANCE.toDTO(user);字段名和类型相同时,MapStruct 自动映射,不需要任何额外配置。
3. 字段映射详解
Section titled “3. 字段映射详解”3.1. 字段名不同source/target
Section titled “3.1. 字段名不同source/target”// User.username → UserDTO.name@Mapperpublic interface UserMapper {
@Mapping(source = "username", target = "name") UserDTO toDTO(User user);
@Mapping(source = "name", target = "username") User toEntity(UserDTO dto);}3.2. 忽略某个字段ignore = true
Section titled “3.2. 忽略某个字段ignore = true”// 敏感字段不映射到 DTO@Mapping(target = "password", ignore = true)UserDTO toDTO(User user);3.3. 多个 @Mapping
Section titled “3.3. 多个 @Mapping”@Mapping(source = "username", target = "name")@Mapping(source = "email", target = "emailAddress")@Mapping(target = "password", ignore = true)UserDTO toDTO(User user);3.4. 常量和默认值
Section titled “3.4. 常量和默认值”// 固定值:不管 source 是什么,target 都是这个值@Mapping(target = "dataSource", constant = "SYSTEM")
// 默认值:source 字段为 null 时才使用@Mapping(source = "status", target = "status", defaultValue = "ACTIVE")3.5. 嵌套对象字段
Section titled “3.5. 嵌套对象字段”public class Order { private Long id; private User user; // 嵌套对象}
public class User { private Long id; private String userName; // 想取 user.username}@Mapperpublic interface OrderMapper { // 用 "." 访问嵌套属性 @Mapping(source = "user.username", target = "userName") OrderDTO toDTO(Order order);}4. 类型转换
Section titled “4. 类型转换”4.1. 内置自动转换
Section titled “4.1. 内置自动转换”MapStruct 内置了常见类型的自动转换:
| 源类型 | 目标类型 | 说明 |
|---|---|---|
int / Integer | String | 自动 toString |
String | int / Integer | 自动 parseInt |
BigDecimal | String | 自动转换 |
Date | String | 需指定 dateFormat |
4.2. 日期格式转换
Section titled “4.2. 日期格式转换”@Mapping(source = "createTime", target = "createTimeStr", dateFormat = "yyyy-MM-dd HH:mm:ss")UserDTO toDTO(User user);4.3. 自定义转换方法
Section titled “4.3. 自定义转换方法”当内置转换不满足需求时,在接口里写 default 方法并用 @Named 标记:
@Mapperpublic interface UserMapper {
@Mapping(source = "status", target = "statusDesc", qualifiedByName = "statusToDesc") UserDTO toDTO(User user);
@Named("statusToDesc") default String statusToDesc(Integer status) { if (status == null) return ""; return switch (status) { case 0 -> "禁用"; case 1 -> "正常"; default -> "未知"; }; }}4.4. 公共转换逻辑
Section titled “4.4. 公共转换逻辑”多个 Mapper 都需要同一个转换时,提取到独立类并通过 uses 引用:
// 公共转换类@Componentpublic class DateConverter { public String dateToString(Date date) { if (date == null) return null; return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date); }}// Mapper 引用@Mapper(componentModel = "spring", uses = DateConverter.class)public interface UserMapper { UserDTO toDTO(User user); // Date 字段自动调用 DateConverter}5. 集合映射
Section titled “5. 集合映射”单对象映射方法写好之后,集合方法会自动调用它,无需重复写映射规则:
@Mapper(componentModel = "spring")public interface UserMapper {
UserDTO toDTO(User user);
List<UserDTO> toDTOList(List<User> users); // 自动基于 toDTO 生成
Set<UserDTO> toDTOSet(Set<User> users);
Map<String, UserDTO> toDTOMap(Map<String, User> users);}6. Spring 集成
Section titled “6. Spring 集成”实际项目里不用 Mappers.getMapper(),让 Spring 来管理:
// componentModel = "spring":生成的实现类会带上 @Component@Mapper(componentModel = "spring")public interface UserMapper { UserDTO toDTO(User user); User toEntity(UserDTO dto);}// 直接 @Autowired 注入@Servicepublic class UserService {
@Autowired private UserMapper userMapper;
public UserDTO getUser(Long id) { User user = userRepository.findById(id).orElseThrow(); return userMapper.toDTO(user); }}配置了 componentModel = "spring" 后,就不需要 INSTANCE 常量了。
7. 更新已有对象@MappingTarget
Section titled “7. 更新已有对象@MappingTarget”不创建新对象,把数据”打入”一个已有对象(典型场景:更新操作):
@Mapper(componentModel = "spring")public interface UserMapper {
// 把 dto 的值更新到已有的 user 对象里,不替换整个对象 void updateEntityFromDTO(UserDTO dto, @MappingTarget User user);}// 更新场景User user = userRepository.findById(id).orElseThrow();userMapper.updateEntityFromDTO(updateDTO, user); // 只覆盖 dto 里有的字段userRepository.save(user);这样 createTime、createBy 等不在 DTO 里的字段不会被清空。
8. 合并多个源对象
Section titled “8. 合并多个源对象”把两个对象的字段合并成一个目标对象:
@Mapper(componentModel = "spring")public interface OrderMapper {
@Mapping(source = "order.id", target = "orderId") @Mapping(source = "user.username", target = "userName") @Mapping(source = "user.email", target = "userEmail") OrderDetailVO toDetailVO(Order order, User user);}9. 继承映射配置
Section titled “9. 继承映射配置”9.1. @InheritInverseConfiguration
Section titled “9.1. @InheritInverseConfiguration”正向映射和反向映射规则对称时,避免重复写:
@Mapperpublic interface UserMapper {
@Mapping(source = "username", target = "name") @Mapping(source = "email", target = "emailAddress") UserDTO toDTO(User user);
// 自动继承上面的 @Mapping 并反转 source/target @InheritInverseConfiguration User toEntity(UserDTO dto);}9.2. @InheritConfiguration
Section titled “9.2. @InheritConfiguration”同方向的两个方法想共享映射规则:
@Mapping(source = "username", target = "name")UserDTO toDTO(User user);
@InheritConfiguration // 复用 toDTO 的规则void updateDTOFromUser(User user, @MappingTarget UserDTO dto);10. 空值处理策略
Section titled “10. 空值处理策略”10.1. 源字段为 null 时
Section titled “10.1. 源字段为 null 时”@Mapper( componentModel = "spring", // SET_TO_NULL(默认):目标字段设为 null // IGNORE:目标字段保持原值(配合 @MappingTarget 做部分更新非常实用) nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)public interface UserMapper { void updateEntityFromDTO(UserDTO dto, @MappingTarget User user);}IGNORE 策略的典型场景:前端只传了要修改的字段,其余字段传 null,不希望把数据库里的值清掉。
10.2. 整个源对象为 null 时
Section titled “10.2. 整个源对象为 null 时”// ALWAYS:映射前先检查整个 source 对象是否为 null,为 null 直接返回 null@Mapper(nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)public interface UserMapper { UserDTO toDTO(User user);}11. 枚举映射
Section titled “11. 枚举映射”枚举默认按名字匹配(不是 ordinal),名字不同时用 @ValueMapping:
public enum GenderCode { MALE, FEMALE }public enum GenderEnum { M, F }@Mapperpublic interface GenderMapper { @ValueMapping(source = "MALE", target = "M") @ValueMapping(source = "FEMALE", target = "F") GenderEnum toGenderEnum(GenderCode code);}12. 全局配置@MapperConfig
Section titled “12. 全局配置@MapperConfig”所有 Mapper 共享同一套默认配置,避免每个 Mapper 都重复写:
// 定义全局配置@MapperConfig( componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR, // 有未映射字段时编译报错 nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)public interface BaseMapperConfig {}// 各 Mapper 引用@Mapper(config = BaseMapperConfig.class)public interface UserMapper { UserDTO toDTO(User user);}13. 查看生成的代码
Section titled “13. 查看生成的代码”MapStruct 是编译期生成,build 之后可以在这里看到实现类:
target/ generated-sources/ annotations/ com/yourpackage/mapper/ UserMapperImpl.java ← 生成的实现生成的代码大概长这样:
@Componentpublic class UserMapperImpl implements UserMapper {
@Override public UserDTO toDTO(User user) { if (user == null) return null;
UserDTO userDTO = new UserDTO(); userDTO.setName(user.getUsername()); userDTO.setEmailAddress(user.getEmail()); return userDTO; }}就是普通的 get/set,没有任何魔法,这也是它性能好的原因。
14. 常见坑
Section titled “14. 常见坑”- Lombok 和 MapStruct 处理器顺序错了
<!-- 错误:MapStruct 先执行,这时 Lombok 还没生成 getter/setter --><annotationProcessorPaths> <path>mapstruct-processor</path> <path>lombok</path></annotationProcessorPaths>
<!-- 正确:Lombok 必须在前 --><annotationProcessorPaths> <path>lombok</path> <path>mapstruct-processor</path></annotationProcessorPaths>- 字段不兼容,但没有任何报错。
MapStruct 找不到合适的转换时,默认静默跳过,目标字段为 null,运行时才发现。
解决方案:开启严格模式,编译期就报错:
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)public interface UserMapper { ... }建议在 @MapperConfig 里全局配置。
- 多源映射时字段来源不明确
// 不明确:MapStruct 自己猜来源@Mapping(source = "id", target = "orderId")
// 明确:指定来自哪个参数@Mapping(source = "order.id", target = "orderId")@Mapping(source = "user.username", target = "userName")OrderDetailVO toDetailVO(Order order, User user);- 改了接口但没重新编译
MapStruct 是编译期生成,改了 Mapper 接口后必须重新编译,否则运行的还是旧的实现类。IDEA 有时需要手动 Build → Rebuild Project。