在 Java 开发中,对象之间的属性拷贝(DTO ↔ VO ↔ Entity ↔ BO) 几乎是每个项目的”日常刚需”。手写 getter/setter 既枯燥又容易漏字段,用 BeanUtils(Spring/Apache)又有性能问题(反射)和类型不安全的隐患。
MapStruct 是目前业界公认的”最佳对象映射框架”:它在编译期通过注解处理器生成纯 Java 的 getter/setter 调用代码,无反射、零运行时开销、类型安全,同时支持极其灵活的自定义映射规则。
这篇教程覆盖 MapStruct 从 初级 → 中级 → 高级 的全部核心用法,每个特性都配完整可运行的示例,读完即可在项目中落地。
0)MapStruct 是什么 & 为什么选它
0.1 核心定位
MapStruct 是一个编译期代码生成器(JSR 269 Annotation Processor),专门用于简化 Java Bean 之间的映射。
它的工作原理非常直白:
- 你定义一个映射接口/抽象类,用
@Mapper 注解标注。 - 在接口中声明映射方法(如
UserDTO toDTO(User entity))。 - 编译时,MapStruct 自动生成这个接口的实现类,代码就是纯手工风格的 getter/setter 调用。
- 运行时直接调用生成的实现类,没有反射,没有动态代理。
0.2 常见映射方案对比
| 方案 | 原理 | 性能 | 类型安全 | 灵活性 | 推荐度 |
|---|
| 手写 getter/setter | 纯 Java | ⭐⭐⭐⭐⭐ | ✅ | ⭐⭐⭐⭐⭐ | 字段少还行,多了想死 |
| Spring BeanUtils | 反射 | ⭐⭐ | ❌(运行时才抛错) | ⭐ | 不推荐生产 |
| Apache BeanUtils | 反射(更慢) | ⭐ | ❌ | ⭐ | 绝对不推荐 |
| Dozer / Orika | 反射 / 字节码 | ⭐⭐⭐ | ⚠️ | ⭐⭐⭐⭐ | 老项目维护 |
| MapStruct | 编译期生成代码 | ⭐⭐⭐⭐⭐ | ✅ 编译期报错 | ⭐⭐⭐⭐⭐ | ✅ 首选 |
| Selma | 编译期生成 | ⭐⭐⭐⭐⭐ | ✅ | ⭐⭐⭐ | 小众,社区弱 |
一句话结论:新项目直接上 MapStruct,它就是目前的”标准答案”。
1)快速开始:5 分钟跑通第一个 Demo
1.1 环境准备:Maven / Gradle 依赖
Maven(最常用) — 在 pom.xml 中加入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| <properties> <mapstruct.version>1.6.2</mapstruct.version> <lombok.version>1.18.34</lombok.version> </properties>
<dependencies> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${mapstruct.version}</version> </dependency>
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> <scope>provided</scope> </dependency> </dependencies>
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.13.0</version> <configuration> <source>17</source> <target>17</target> <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> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok-mapstruct-binding</artifactId> <version>0.2.0</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build>
|
⚠️ 重点提醒:如果同时用 Lombok 和 MapStruct,lombok-mapstruct-binding 这个桥接依赖必须加,否则 MapStruct 看不到 Lombok 生成的 getter/setter,会报错找不到属性。
Gradle(简洁版):
1 2 3 4 5 6 7 8
| dependencies { implementation 'org.mapstruct:mapstruct:1.6.2' annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.2'
compileOnly 'org.projectlombok:lombok:1.18.34' annotationProcessor 'org.projectlombok:lombok:1.18.34' annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0' }
|
1.2 第一步:准备源对象和目标对象
先来两个最简单的 POJO,故意取几个相同字段名和不同字段名:
1 2 3 4 5 6 7 8 9 10
| @Data public class User { private Long id; private String username; private String password; private String email; private Integer age; private LocalDateTime createTime; }
|
1 2 3 4 5 6 7 8 9
| @Data public class UserDTO { private Long id; private String name; private String email; private Integer age; private String createTimeStr; }
|
1.3 第二步:定义 Mapper 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.factory.Mappers;
@Mapper public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
@Mapping(source = "username", target = "name") UserDTO toDTO(User user); }
|
1.4 第三步:编译 & 查看生成的代码
执行 mvn compile 后,MapStruct 会在 target/generated-sources/annotations/ 下生成实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class UserMapperImpl implements UserMapper {
@Override public UserDTO toDTO(User user) { if (user == null) { return null; } UserDTO userDTO = new UserDTO(); userDTO.setName(user.getUsername()); userDTO.setId(user.getId()); userDTO.setEmail(user.getEmail()); userDTO.setAge(user.getAge()); return userDTO; } }
|
1.5 第四步:使用 Mapper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class DemoTest { public static void main(String[] args) { User user = new User(); user.setId(1L); user.setUsername("zhangsan"); user.setPassword("123456"); user.setEmail("zhangsan@example.com"); user.setAge(25);
UserDTO dto = UserMapper.INSTANCE.toDTO(user);
System.out.println(dto); } }
|
搞定!第一个 MapStruct 程序跑通了。小结一下核心机制:
- ✅ 同名同类型字段自动映射(
id, email, age) - ✅ 不同名字段用
@Mapping(source, target) 指定(username → name) - ✅ 没有配置的字段自动忽略(
password 没进 DTO,createTimeStr 保持 null)
2)初级篇:80% 场景够用的基础用法
2.1 多字段映射 & 忽略字段(@Mapping 完整参数)
@Mapping 是 MapStruct 最常用的注解,完整参数列表:
1 2 3 4 5 6 7 8 9 10 11
| @Mapping( source = "源字段名", // 源对象中的属性名 target = "目标字段名", // 目标对象中的属性名 ignore = true, // 设为 true 表示忽略这个字段(不映射) defaultValue = "默认值", // 当源字段为 null 时使用的默认值 constant = "常量值", // 直接给目标字段赋常量,不管源是什么 dateFormat = "yyyy-MM-dd HH:mm:ss", // 日期格式化字符串 numberFormat = "#.00", // 数字格式化字符串 expression = "java(...)", // 自定义 Java 表达式 defaultExpression = "java(...)" // 源为 null 时执行的表达式 )
|
实战示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Data public class Order { private Long orderId; private String orderNo; private BigDecimal amount; private LocalDateTime payTime; private String remark; private String internalCode; }
@Data public class OrderDTO { private Long id; private String orderNo; private String amountStr; private String payTimeStr; private String remark; private String channel = "OFFICIAL"; }
|
1 2 3 4 5 6 7 8 9 10 11 12
| @Mapper public interface OrderMapper { OrderMapper INSTANCE = Mappers.getMapper(OrderMapper.class);
@Mapping(source = "orderId", target = "id") @Mapping(source = "amount", target = "amountStr", numberFormat = "#,##0.00") @Mapping(source = "payTime", target = "payTimeStr", dateFormat = "yyyy-MM-dd HH:mm:ss") @Mapping(target = "internalCode", ignore = true) @Mapping(target = "channel", constant = "OFFICIAL") @Mapping(target = "remark", defaultValue = "该订单暂无备注") OrderDTO toDTO(Order order); }
|
测试:
1 2 3 4 5 6 7 8 9 10 11
| Order order = new Order(); order.setOrderId(1001L); order.setOrderNo("ORD20260803001"); order.setAmount(new BigDecimal("1288.5")); order.setPayTime(LocalDateTime.of(2026, 8, 3, 14, 30, 0));
OrderDTO dto = OrderMapper.INSTANCE.toDTO(order);
|
⚠️ 小贴士:ignore = true 常用于”目标对象有这个字段但我不想让 MapStruct 管”,也常用于避免多余属性未映射的警告。
2.2 集合映射(List / Set / Map)
MapStruct 会自动帮你处理集合,只要单个元素的映射定义好了,集合映射直接声明方法即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Mapper public interface UserMapper { UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
@Mapping(source = "username", target = "name") UserDTO toDTO(User user);
List<UserDTO> toDTOList(List<User> users);
Set<UserDTO> toDTOSet(Set<User> users);
Map<String, UserDTO> toDTOMap(Map<String, User> userMap); }
|
生成的代码:
1 2 3 4 5 6 7 8 9 10
| @Override public List<UserDTO> toDTOList(List<User> users) { if (users == null) return null;
List<UserDTO> list = new ArrayList<>(users.size()); for (User user : users) { list.add(toDTO(user)); } return list; }
|
就是这么省心!不需要你写任何循环。
2.3 获取 Mapper 实例的三种方式
MapStruct 提供了三种获取 Mapper 实例的方式,适应不同项目风格:
方式一:Mappers.getMapper()(工厂模式,最简单)
适合非 Spring 项目或简单项目:
1 2 3 4 5 6 7 8
| @Mapper public interface UserMapper { UserMapper INSTANCE = Mappers.getMapper(UserMapper.class); }
UserDTO dto = UserMapper.INSTANCE.toDTO(user);
|
方式二:Spring 注入(推荐 SpringBoot 项目)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Mapper(componentModel = "spring") public interface UserMapper { @Mapping(source = "username", target = "name") UserDTO toDTO(User user); }
@Service public class UserService { @Autowired private UserMapper userMapper;
public UserDTO getUser(Long id) { User user = userRepository.findById(id); return userMapper.toDTO(user); } }
|
生成的实现类会自动加上 Spring 的 @Component 注解:
1 2
| @Component public class UserMapperImpl implements UserMapper { ... }
|
方式三:CDI(Jakarta EE 项目)
1 2 3 4
| @Mapper(componentModel = "cdi") public interface UserMapper { }
|
💡 最佳实践:SpringBoot 项目统一用 componentModel = "spring",不要混用工厂模式和注入模式。
2.4 多个源对象映射到一个目标对象
非常常见的场景:比如返回的 DTO 需要同时从 User 和 Address 里取字段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Data public class Address { private Long addressId; private String province; private String city; private String detail; }
@Data public class UserDetailDTO { private Long userId; private String userName; private String fullAddress; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Mapper public interface UserDetailMapper { UserDetailMapper INSTANCE = Mappers.getMapper(UserDetailMapper.class);
@Mapping(source = "user.id", target = "userId") @Mapping(source = "user.username", target = "userName") @Mapping( target = "fullAddress", expression = "java( address.getProvince() + address.getCity() + address.getDetail() )" ) UserDetailDTO toDetailDTO(User user, Address address); }
|
使用:
1 2 3 4 5
| User user = new User(1L, "zhangsan", "123", "z@x.com", 25, null); Address addr = new Address(10L, "浙江省", "杭州市", "西湖区文三路 1 号");
UserDetailDTO dto = UserDetailMapper.INSTANCE.toDetailDTO(user, addr);
|
技巧:当方法有多个参数时,所有 @Mapping 的 source 建议都写成 “参数名.字段名” 的形式,避免歧义。
2.5 嵌套对象的”扁平化”映射(点号路径)
源对象里嵌套了另一个对象,想把嵌套字段”拍平”到目标对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Data public class Employee { private Long empId; private String empName; private Department department; }
@Data public class Department { private Long deptId; private String deptName; private String deptCode; }
@Data public class EmployeeDTO { private Long empId; private String empName; private Long deptId; private String deptName; }
|
1 2 3 4 5 6 7 8
| @Mapper public interface EmployeeMapper { EmployeeMapper INSTANCE = Mappers.getMapper(EmployeeMapper.class);
@Mapping(source = "department.deptId", target = "deptId") @Mapping(source = "department.deptName", target = "deptName") EmployeeDTO toDTO(Employee emp); }
|
生成的代码会自动做 null 检查:
1 2 3 4
| if (emp.getDepartment() != null) { employeeDTO.setDeptId(emp.getDepartment().getDeptId()); employeeDTO.setDeptName(emp.getDepartment().getDeptName()); }
|
完美!不会因为 department == null 而 NPE。
3)中级篇:项目里一定会遇到的进阶用法
3.1 自定义类型转换:@Qualifier + 自定义方法
当 dateFormat / numberFormat 搞不定时(比如枚举转中文、字典翻译、复杂计算),可以写自定义转换方法,通过 @Qualifier 或 @Named 精准指定。
场景示例:性别枚举 → 中文描述 + 字典编码翻译
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public enum Gender { MALE, FEMALE, UNKNOWN }
@Data public class Person { private Long id; private String name; private Gender gender; private String status; }
@Data public class PersonDTO { private Long id; private String name; private String genderDesc; private String statusDesc; }
|
方式一:用 @Named 给方法起名字,最清晰
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import org.mapstruct.Named; import org.mapstruct.Qualifier;
public class GenderAndStatusConverter {
@Named("genderToDesc") public static String genderToDesc(Gender gender) { if (gender == null) return "未知"; return switch (gender) { case MALE -> "男"; case FEMALE -> "女"; default -> "未知"; }; }
@Named("statusToDesc") public static String statusToDesc(String status) { return "1".equals(status) ? "启用" : "禁用"; } }
|
在 Mapper 里 uses = ... 引入转换器,然后用 qualifiedByName 指定:
1 2 3 4 5 6 7 8
| @Mapper(uses = GenderAndStatusConverter.class) public interface PersonMapper { PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);
@Mapping(source = "gender", target = "genderDesc", qualifiedByName = "genderToDesc") @Mapping(source = "status", target = "statusDesc", qualifiedByName = "statusToDesc") PersonDTO toDTO(Person person); }
|
方式二:直接在 Mapper 里写 default 方法(更内聚,小项目推荐)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Mapper public interface PersonMapper { PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);
@Mapping(source = "gender", target = "genderDesc", qualifiedByName = "genderToDesc") @Mapping(source = "status", target = "statusDesc", qualifiedByName = "statusToDesc") PersonDTO toDTO(Person person);
@Named("genderToDesc") default String genderToDesc(Gender gender) { if (gender == null) return "未知"; return switch (gender) { case MALE -> "男"; case FEMALE -> "女"; default -> "未知"; }; }
@Named("statusToDesc") default String statusToDesc(String status) { return "1".equals(status) ? "启用" : "禁用"; } }
|
两种方式生成的代码一致:
1 2
| personDTO.setGenderDesc(genderToDesc(person.getGender())); personDTO.setStatusDesc(statusToDesc(person.getStatus()));
|
3.2 全局类型转换:@Mapper + 抽象类 Mapper
有时候你希望所有用到 LocalDateTime → String 的地方都用同一种格式,不想每个字段都写 dateFormat。这时可以用抽象类 Mapper + 自定义通用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Mapper(componentModel = "spring") public abstract class BaseMapper {
public String localDateTimeToString(LocalDateTime time) { return time == null ? null : time.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); }
public LocalDateTime stringToLocalDateTime(String str) { return str == null ? null : LocalDateTime.parse(str, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); }
public String bigDecimalToString(BigDecimal value) { return value == null ? null : value.setScale(2, RoundingMode.HALF_UP).toPlainString(); } }
|
然后你的业务 Mapper 继承这个 BaseMapper,就自动获得了这些全局转换能力:
1 2 3 4 5
| @Mapper(componentModel = "spring") public abstract class OrderMapper extends BaseMapper { public abstract OrderDTO toDTO(Order order); }
|
💡 继承 vs uses 的选择:
- 继承 BaseMapper:适合”全局通用的转换”(日期、数字格式化等)
uses = XxxConverter.class:适合”特定场景才用的转换”(字典翻译、特殊枚举等)
3.3 反向映射(DTO → Entity)与 @InheritInverseConfiguration
实际项目中,你往往需要双向映射:Entity → DTO(查询返回)和 DTO → Entity(保存/更新)。
MapStruct 提供了 @InheritInverseConfiguration 让你不用重复写两遍 @Mapping:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Mapper(componentModel = "spring") public interface UserMapper {
@Mapping(source = "username", target = "name") @Mapping(source = "createTime", target = "createTimeStr", dateFormat = "yyyy-MM-dd HH:mm:ss") UserDTO toDTO(User user);
@InheritInverseConfiguration(name = "toDTO") @Mapping(target = "password", ignore = true) User toEntity(UserDTO dto); }
|
name = "toDTO" 表示”继承 toDTO 方法上的 @Mapping 配置并反向”。如果只有一个正向方法,name 可以省略。
3.4 映射继承:@InheritConfiguration
当你有多个方法需要复用同一套 @Mapping 配置(比如 toDTO 和 updateDTO 都需要同样的字段映射),用 @InheritConfiguration:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Mapper(componentModel = "spring") public interface UserMapper {
@Mapping(source = "username", target = "name") @Mapping(source = "createTime", target = "createTimeStr", dateFormat = "yyyy-MM-dd HH:mm:ss") UserDTO toDTO(User user);
@InheritConfiguration(name = "toDTO") @Mapping(target = "createTimeStr", ignore = true) UserDTO toSimpleDTO(User user);
@InheritConfiguration(name = "toDTO") @Mapping(target = "id", ignore = true) void updateEntity(UserDTO dto, @MappingTarget User entity); }
|
3.5 更新已有对象(@MappingTarget)
不是所有映射都要 new 一个新对象。场景:从 DTO 接收参数,更新数据库查出来的 Entity(而不是重新 new):
1 2 3 4 5 6 7 8 9 10
| @Mapper(componentModel = "spring") public interface UserMapper {
@Mapping(source = "username", target = "name") void updateEntity(UserDTO dto, @MappingTarget User entity); }
|
生成的代码(关键点:没有 new User!):
1 2 3 4 5 6 7 8 9
| @Override public void updateEntity(UserDTO dto, User entity) { if (dto == null) return;
entity.setUsername(dto.getName()); entity.setEmail(dto.getEmail()); entity.setAge(dto.getAge()); }
|
实战用法:
1 2 3 4 5 6 7 8
| User dbUser = userRepository.findById(1L).orElseThrow();
userMapper.updateEntity(userDTO, dbUser);
userRepository.save(dbUser);
|
3.6 null 值策略:nullValueMappingStrategy 等
MapStruct 提供了好几个控制”null 怎么处理”的配置,一次讲透:
| 配置注解 / 参数 | 作用 | 可选值 |
|---|
@Mapper(nullValueMappingStrategy = ...) | 源为 null 时怎么映射到目标 | RETURN_NULL(默认,返回 null) / SET_TO_DEFAULT(new 空对象/空集合) |
@Mapping(nullValuePropertyMappingStrategy = ...) | 源属性为 null 时,目标属性要不要设 null | SET_TO_NULL(默认) / IGNORE(保持目标原值) |
@Mapping(nullValueCheckStrategy = ...) | 什么时候做 null 检查 | ON_IMPLICIT_CONVERSION(默认,只有需要转换才检查) / ALWAYS / NEVER |
最常用场景:更新时不要把 null 刷到数据库
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Mapper(componentModel = "spring") public interface UserMapper {
@Mapping( target = "id", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE ) void updateEntity(UserDTO dto, @MappingTarget User entity); }
|
测试效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| User dbUser = new User(); dbUser.setId(1L); dbUser.setUsername("zhangsan"); dbUser.setEmail("old@example.com"); dbUser.setAge(25);
UserDTO dto = new UserDTO(); dto.setName("张三_新名字");
userMapper.updateEntity(dto, dbUser);
|
💡 如果想全局生效这个策略(所有映射都是 null 忽略),直接放在 @Mapper 上:
1 2 3 4
| @Mapper( componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE )
|
4)高级篇:复杂场景 & 性能优化
4.1 @Context:在映射方法间传递”上下文参数”
有时候你需要传递一些不属于源对象,但会影响映射结果的动态参数。比如:不同用户看到的金额精度不同、翻译时要带当前语言等。用 @Context 可以优雅解决:
1 2 3 4 5
| @Data public class OrderContext { private String currency; private boolean includeTax; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Mapper(componentModel = "spring") public interface OrderMapper {
@Mapping( target = "amountWithCurrency", expression = "java( formatAmount(order.getAmount(), context) )" ) OrderDTO toDTO(Order order, @Context OrderContext context);
default String formatAmount(BigDecimal amount, @Context OrderContext context) { if (amount == null) return null; BigDecimal value = context.isIncludeTax() ? amount.multiply(new BigDecimal("1.13")) : amount; return value.setScale(2, RoundingMode.HALF_UP) + " " + context.getCurrency(); } }
|
使用:
1 2 3 4 5 6
| OrderContext ctx = new OrderContext(); ctx.setCurrency("CNY"); ctx.setIncludeTax(true);
OrderDTO dto = orderMapper.toDTO(order, ctx);
|
生成的代码中 context 会一路透传下去,不会作为映射的源。
4.2 @BeforeMapping / @AfterMapping:映射前后钩子
如果需要在映射开始前或结束后做一些额外处理(比如前置校验、后置字段计算、Spring Bean 调用等),用这两个注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Mapper(componentModel = "spring") public abstract class UserMapper {
@BeforeMapping protected void beforeMapping(User user) { if (user.getUsername() == null || user.getUsername().isBlank()) { user.setUsername("默认用户名"); } }
@AfterMapping protected void afterMapping(User user, @MappingTarget UserDTO dto) { if (dto.getAge() != null && dto.getAge() < 18) { dto.setAgeTag("未成年"); } else if (dto.getAge() != null && dto.getAge() < 35) { dto.setAgeTag("青年"); } else { dto.setAgeTag("中年及以上"); } }
public abstract UserDTO toDTO(User user); }
|
注意:钩子方法必须写在抽象类里(接口里的 default 方法也行,但 @BeforeMapping 写在抽象类更常见),参数类型要能匹配到具体映射方法的源/目标参数。
4.3 在 Mapper 中调用 Spring Bean(@Autowired 注入)
当你用 componentModel = "spring" 时,生成的类是 @Component,因此你可以在抽象类 Mapper 里 @Autowired 任何 Spring Bean(比如字典服务、用户上下文等):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Mapper(componentModel = "spring") public abstract class OrderMapper {
@Autowired protected DictService dictService;
@Autowired protected UserContext userContext;
@Mapping( target = "statusDesc", expression = "java( dictService.translate(\"ORDER_STATUS\", order.getStatus()) )" ) @Mapping( target = "amountStr", expression = "java( formatByUserCurrency(order.getAmount()) )" ) public abstract OrderDTO toDTO(Order order);
protected String formatByUserCurrency(BigDecimal amount) { String currency = userContext.getCurrentUserCurrency(); return amount.setScale(2, RoundingMode.HALF_UP) + " " + currency; } }
|
只能用抽象类才能注入 Bean(接口没有字段),这是为什么复杂场景推荐抽象类而不是接口的原因。
4.4 @BeanMapping:单个方法级别的细粒度控制
@BeanMapping 放在映射方法上,专门控制”这一个方法”的行为,优先级比 @Mapper 上的全局配置高:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Mapper(componentModel = "spring") public interface UserMapper {
@BeanMapping(ignoreByDefault = true) @Mapping(source = "id", target = "id") @Mapping(source = "username", target = "name") UserDTO toSimpleDTO(User user); }
|
ignoreByDefault = true 非常适合”只暴露部分字段”的场景(比如给第三方系统的精简 DTO,确保不会误传敏感字段)。
4.5 映射枚举:@ValueMapping
专门处理枚举之间映射的注解,支持默认值和异常抛出:
1 2 3 4 5 6 7 8
| public enum OrderStatus { CREATED, PAID, SHIPPED, DELIVERED, CANCELLED }
public enum ExternalOrderStatus { NEW, PAYED, IN_TRANSIT, COMPLETED, ABORTED, UNKNOWN }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Mapper public interface OrderStatusMapper { OrderStatusMapper INSTANCE = Mappers.getMapper(OrderStatusMapper.class);
@ValueMapping(source = "CREATED", target = "NEW") @ValueMapping(source = "PAID", target = "PAYED") @ValueMapping(source = "SHIPPED", target = "IN_TRANSIT") @ValueMapping(source = "DELIVERED", target = "COMPLETED") @ValueMapping(source = "CANCELLED", target = "ABORTED") ExternalOrderStatus toExternal(OrderStatus status);
@InheritInverseConfiguration @ValueMapping(source = MappingConstants.ANY_REMAINING, target = MappingConstants.NULL) OrderStatus fromExternal(ExternalOrderStatus status); }
|
4.6 批量工厂 + 通用基类 Mapper(大型项目架构)
真实项目里往往有几十上百个 Mapper,我们可以做一层统一基类减少重复:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
public interface BaseMapper<E, D, Q> {
D toDTO(E entity);
E toEntity(D dto);
List<D> toDTOList(List<E> entityList);
List<E> toEntityList(List<D> dtoList);
void updateEntity(D dto, @MappingTarget E entity); }
|
具体业务 Mapper 直接继承即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) public interface UserMapper extends BaseMapper<User, UserDTO, UserQuery> {
@Override @Mapping(source = "username", target = "name") @Mapping(source = "createTime", target = "createTimeStr", dateFormat = "yyyy-MM-dd HH:mm:ss") UserDTO toDTO(User user);
@Override @InheritInverseConfiguration(name = "toDTO") @Mapping(target = "password", ignore = true) User toEntity(UserDTO dto);
}
|
这样整个项目的 Mapper 结构统一、策略统一,代码审查也轻松。
4.7 查看 & 调试生成的代码
MapStruct 生成的代码路径:
- Maven:
target/generated-sources/annotations/你的包路径/XxxMapperImpl.java - Gradle:
build/generated/sources/annotationProcessor/java/main/你的包路径/XxxMapperImpl.java
💡 IDE 调试小技巧:直接在生成的 XxxMapperImpl.java 里打断点,调试时可以一步步看到每个字段是怎么赋值的,排错神器。
5)常见坑 & 排障指南
5.1 ❌ 报错:”Can’t find mapper implementation” 或 NPE
场景:UserMapper.INSTANCE.toDTO(...) 空指针,或者 Spring 注入时 NoSuchBeanDefinitionException。
排查清单:
- 是不是只加了
mapstruct 依赖,没加 mapstruct-processor(注解处理器)? - Maven 里是不是忘了在
maven-compiler-plugin 的 <annotationProcessorPaths> 里配置? - 有没有执行过
mvn compile?(target 目录下没生成实现类当然找不到) - Spring 模式是不是忘了加
componentModel = "spring"?
5.2 ❌ 报错:”Unknown property ‘xxx’ in return type” / “No property named ‘xxx’”
场景:编译时就报找不到字段。
99% 的原因:
- Lombok 和 MapStruct 同时用,但没加
lombok-mapstruct-binding。 - 字段名拼写错了(注意大小写,或者是不是把 source/target 写反了)。
- 用了 Java Record 但 Lombok 版本太老。
解决:参考 1.1 节的依赖配置,把三个 annotationProcessorPath 都配齐。
5.3 ❌ 明明忽略了字段,IDE 还警告”未映射的 target property”
这个警告是 MapStruct 提醒你”目标对象有字段没配置映射”。两个解决办法:
方法一(针对单个字段):显式写 @Mapping(target = "xxx", ignore = true)。
方法二(全局消除警告,不推荐新手用):在 @Mapper 上加:
1 2 3 4 5
| @Mapper( componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE, // 目标有未映射字段不警告 unmappedSourcePolicy = ReportingPolicy.WARN // 源有未映射字段建议保留警告,防止漏字段 )
|
⚠️ 新手建议保留默认的 WARN,等对映射流程完全熟练了再调。
5.4 ❌ 日期/数字格式化不生效
LocalDateTime 要用 dateFormat,但要确保源是 LocalDateTime,目标是 String(反过来也一样)。- 如果你用了全局转换方法(比如 BaseMapper 里的),它的优先级比
dateFormat 高。 - Java 8 时间类型(
LocalDate/Time)确保项目里有 jsr310 相关支持(JDK 8+ 自带)。
这不是 MapStruct 的问题,而是团队成员习惯不统一。建议:项目里统一用 MapStruct,禁止再出现任何 BeanUtils.copyProperties。原因:
- 反射 BeanUtils 不会调用 MapStruct 的自定义转换,映射出来的数据不一致。
- 性能退化且没有类型安全保障。
- 出现问题排查链路不清晰。
6)实战:一个完整的”User 增删改查”场景
把前面的知识点串起来,做一个 CRUD 全流程。
6.1 实体与 DTO 定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| @Data @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String username; private String password; private String email; private Integer age; private Gender gender; private String status; private LocalDateTime createTime; private LocalDateTime updateTime; }
@Data public class UserCreateDTO { @NotBlank private String username; @NotBlank private String password; private String email; private Integer age; private Gender gender; }
@Data public class UserUpdateDTO { @NotNull private Long id; private String email; private Integer age; private Gender gender; private String status; }
@Data public class UserVO { private Long id; private String username; private String email; private Integer age; private String genderDesc; private String statusDesc; private String createTimeStr; }
|
6.2 Mapper 定义(抽象类 + Spring Bean)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| @Mapper( componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE ) public abstract class UserMapper {
public abstract User toEntity(UserCreateDTO dto);
public abstract void updateEntity(UserUpdateDTO dto, @MappingTarget User user);
@Mapping(source = "gender", target = "genderDesc", qualifiedByName = "genderToDesc") @Mapping(source = "status", target = "statusDesc", qualifiedByName = "statusToDesc") @Mapping(source = "createTime", target = "createTimeStr", dateFormat = "yyyy-MM-dd HH:mm:ss") public abstract UserVO toVO(User user);
public abstract List<UserVO> toVOList(List<User> list);
@Named("genderToDesc") String genderToDesc(Gender gender) { if (gender == null) return "未知"; return switch (gender) { case MALE -> "男"; case FEMALE -> "女"; }; }
@Named("statusToDesc") String statusToDesc(String status) { return "1".equals(status) ? "启用" : "禁用"; } }
|
6.3 Service 层调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| @Service @RequiredArgsConstructor public class UserService {
private final UserRepository userRepository; private final UserMapper userMapper; private final PasswordEncoder passwordEncoder;
@Transactional public UserVO create(UserCreateDTO dto) { User user = userMapper.toEntity(dto); user.setPassword(passwordEncoder.encode(dto.getPassword())); user.setStatus("1"); user.setCreateTime(LocalDateTime.now()); User saved = userRepository.save(user); return userMapper.toVO(saved); }
@Transactional public UserVO update(UserUpdateDTO dto) { User user = userRepository.findById(dto.getId()) .orElseThrow(() -> new RuntimeException("用户不存在")); userMapper.updateEntity(dto, user); user.setUpdateTime(LocalDateTime.now()); return userMapper.toVO(userRepository.save(user)); }
public UserVO getById(Long id) { User user = userRepository.findById(id) .orElseThrow(() -> new RuntimeException("用户不存在")); return userMapper.toVO(user); }
public List<UserVO> list() { return userMapper.toVOList(userRepository.findAll()); } }
|
整个流程零手写 getter/setter,所有字段转换逻辑全部集中在 UserMapper,清晰可维护。
7)总结:MapStruct 学习路线 & 心智模型
最后,给一张”从 0 到熟练”的学习阶梯图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ✅ 第 1 层(入门):同名自动映射 + @Mapping 改名 └─ 能应对 50% 的简单场景
✅ 第 2 层(基础):集合映射 + List/Set + componentModel=spring └─ 能应对 70% 的日常开发
✅ 第 3 层(熟练):多源对象 + @MappingTarget 更新 + @InheritInverseConfiguration └─ 能应对 90% 的 CRUD 项目
✅ 第 4 层(进阶):@Named/@Qualifier 自定义转换 + @Context + 空值策略 └─ 能搞定复杂业务规则
✅ 第 5 层(专家):@Before/@AfterMapping + Spring Bean 注入 + 抽象基类架构 └─ 能主导大型项目的映射层设计
|
一句话心法
MapStruct 本质上就是一个 “帮你手写 getter/setter 的代码模板引擎”。你越理解”它生成的代码和你手写的一样”,用起来就越轻松。遇到任何问题,第一反应去看生成的 XxxMapperImpl.java,90% 的疑问都能秒解。
如果这篇教程对你有帮助,建议收藏 + 在项目里立刻练起来。任何问题欢迎在评论区交流 👇