第二章. MapStruct:字段级映射策略与数据处理
第二章. MapStruct:字段级映射策略与数据处理
Prorise第二章. MapStruct:字段级映射策略与数据处理
摘要:在完成了环境搭建与基础映射后,本章我们将进入 MapStruct 的深水区。面对真实业务中复杂的“脏数据”和严格的格式要求,我们将深入掌握空值防御、常量注入、Java 表达式嵌入以及高精度的数值/日期格式化。同时,我们将构建真实的 Web 接口,通过 Postman 实测来验证反向映射与敏感字段脱敏策略,彻底解决“DTO 到 Entity”的数据回流难题。
本章学习路径
- 空值防御体系:通过
defaultValue、constant和expression构建三级防御机制,杜绝 NPE(空指针异常)。 - 高级表达式:在映射中嵌入 Java 代码与依赖导入,解决动态值生成问题。
- 格式化与隐式转换:掌握
BigDecimal货币精度控制与LocalDateTime的双向格式化,剖析框架内部的隐式转换表。 - 反向继承实战:利用
@InheritInverseConfiguration实现高效的“数据回流”,并配合ignore完成安全脱敏。 - 闭环验证:编写
Controller接口,结合 Postman 与源码断点,验证所有策略的运行时表现。
2.1. 默认值与常量控制策略
在上一章的实战中,我们留下了两个悬念:UserVO 中的 statusDesc 为 null,且我们需要为前端返回一个固定的业务来源标识。在企业级开发中,数据库中的数据往往是不完整的,或者需要根据业务规则强制覆盖某些字段。
MapStruct 提供了三个维度的控制属性,它们的优先级和触发时机各不相同,混用时极易产生误解。
2.1.1. 三级赋值策略详解
我们需要在 UserMapper 中通过具体的场景来区分这三个属性:
defaultValue(兜底策略):仅当 源字段为 null 时生效。- 场景:如果用户未设置邮箱,显示 “暂无邮箱”。
constant(强制策略):无视源字段,始终强制赋值。- 场景:API 接口版本号、固定的业务类型标识。
defaultExpression(动态兜底):当 源字段为 null 时,执行一段 Java 代码。- 场景:如果数据库中
trace_id为空,自动生成一个 UUID。
- 场景:如果数据库中
2.1.2. 增强实体类与 VO
为了演示这些特性,我们需要先对 UserEntity 和 UserVO 进行微调,增加测试字段。
修改文件:src/main/java/com/example/demo/entity/UserEntity.java
1 |
|
修改文件:src/main/java/com/example/demo/vo/UserVO.java
1 |
|
2.1.3. 编写进阶 Mapper 配置
文件路径:src/main/java/com/example/demo/convert/UserMapper.java
这里我们需要特别注意 expression 的写法。由于 MapStruct 生成代码时不知道类的包路径,如果我们在表达式中使用了 java.util.UUID 等类,要么写全限定名,要么在 @Mapper 注解中配置 imports。
1 | package com.example.demo.convert; |
2.1.4. 生成代码深度审计
执行 mvn compile,打开 target/generated-sources/.../UserMapperImpl.java。我们要验证 MapStruct 是否按照预期的逻辑生成了 if-else 代码。
1 |
|
从源码中可以清晰看到:constant 的优先级最高,完全忽略源数据;而 defaultValue 和 defaultExpression 则是互斥的,都是在 else 分支中生效。
2.2. 类型转换与高精度格式化
在金融或报表系统中,数据格式化是重灾区。前端需要 yyyy-MM-dd 格式的日期,或者带有两位小数的金额字符串。如果交给前端处理,可能会出现时区不一致或精度丢失问题,因此后端转换是最佳实践。
2.2.1. 隐式类型转换机制
MapStruct 强大之处在于其内置的“隐式转换表”。在未配置任何注解的情况下,以下转换会自动发生:
- 基本类型 <-> 包装类:
int<->Integer(自动判空与拆装箱)。 - 基本类型 <-> String:
long<->String(调用String.valueOf)。 - 枚举 <-> String:调用枚举的
name()方法。
但是,对于 Date、LocalDateTime 和 BigDecimal,我们需要显式控制格式。
2.2.2. 日期与数值格式化实战
我们在 UserEntity 中增加金额字段,并在 Mapper 中配置格式化规则。
修改 Entity:增加 BigDecimal balance。
修改 VO:增加 String balanceStr 和 String createTimeStr。
UserMapper.java 配置更新:
1 |
|
2.2.3. 源码验证
观察生成的代码,重点关注 DateTimeFormatter 和 DecimalFormat 的使用:
1 | // UserMapperImpl.java |
2.3. 反向映射与敏感数据脱敏
开发中常见的场景是:查询时 Entity 转 VO,保存时 VO 转 Entity。这两个过程通常是“镜像”的,但有两个核心区别:
- 反向:源和目标颠倒。
- 脱敏/忽略:前端传来的 VO 不应该包含
id(数据库自增)、createTime(自动生成)或password(不应明文传输),或者后端在转换回 Entity 时必须忽略这些字段以防止被恶意覆盖。
2.3.1. @InheritInverseConfiguration 继承配置
我们不需要把 @Mapping 注解再反着写一遍。MapStruct 提供了“继承反向配置”的功能。
UserMapper.java 新增方法:
1 | // ... 正向 toVO 方法 ... |
关键原理解析:
- 继承机制:
@InheritInverseConfiguration(name = "toVO")会自动查找toVO方法的配置。- 它发现
toVO中有emailAddress -> email,于是它自动生成email -> emailAddress。 - 它发现
toVO中有createTime的格式化,于是它本该自动生成String解析为LocalDateTime的代码。
- 它发现
- 覆盖机制:我们在
@Mappings中手动写了createTime ignore = true。- 规则:手动配置优先级 > 继承配置。
- 结果:MapStruct 放弃了自动生成的日期解析代码,转而直接忽略该字段。
常见报错警示:如果你看到 Target property "xxx" must not be mapped more than once,说明你在同一个 @Mappings 数组里对同一个字段写了两行配置(比如一行写格式化,一行写忽略),请务必删除其中一行,保留你最终想要的那一个。
2.4. Web 层闭环验证
光看代码不够,我们需要启动 Spring Boot,通过真实的 HTTP 请求来验证这一切。
2.4.1. 搭建测试 Controller
我们在 controller 包下创建一个测试控制器。
文件路径:src/main/java/com/example/demo/controller/MapStructTestController.java
1 | package com.example.demo.controller; |
2.4.2. 验证场景一:正向转换与格式化
启动项目,使用浏览器或 Postman 访问 GET http://localhost:8080/test/mapstruct/toVO。
预期响应结果:
1 | { |
分析:
balance原值1234.567被格式化为1234.57,符合#0.00的四舍五入规则。email原值为 null,正确回退到了默认值。traceId成功生成了随机串。
2.4.3. 验证场景二:反向转换与脱敏
使用 Postman 发送 POST http://localhost:8080/test/mapstruct/toEntity。
请求 Body (JSON):
1 | { |
预期响应结果:
1 | { |
分析:
- 即使前端恶意传递了
id: 9999,转换后的 Entity 中id依然为 null,保证了数据库自增 ID 的安全。 cityName成功被还原到了嵌套的Address对象中,证明了 MapStruct 在反向转换时的智能对象实例化能力。
2.5. 本章总结与进阶语法速查
本章我们攻克了 MapStruct 最硬核的“数据清洗”与“格式化”难题。为了方便大家在实际开发中直接 Copy 代码,我们将本章的核心技巧浓缩为一份 “场景化速查手册”。
2.5.1. 进阶语法速查手册
遇到以下业务需求时,请直接参考本表代码:
| 业务场景 | 核心方案 | 代码示例 |
|---|---|---|
| 强制赋值 (如:设置固定版本号) | constant | @Mapping(target = "version", constant = "v1.0")(注:完全忽略源字段) |
| 空值兜底 (如:为空时显示 “未知”) | defaultValue | @Mapping(source = "name", target = "name", defaultValue = "未知用户")(注:仅 source 为 null 时生效) |
| 动态生成 (如:为空时生成 UUID) | defaultExpression | @Mapping(source = "id", target = "id", defaultExpression = "java(java.util.UUID.randomUUID().toString())")(注:需配合 imports 或全限定名) |
| 日期格式化 (Date/Time ↔ String) | dateFormat | @Mapping(source = "createTime", target = "timeStr", dateFormat = "yyyy-MM-dd HH:mm") |
| 金额格式化 (BigDecimal ↔ String) | numberFormat | @Mapping(source = "price", target = "priceStr", numberFormat = "#0.00")(注:自动处理四舍五入) |
| 反向继承 (VO 转回 Entity) | @InheritInverseConfiguration | @InheritInverseConfiguration(name = "toVO")UserEntity toEntity(UserVO vo); |
| 安全过滤 (如:不修改密码/ID) | ignore = true | @Mapping(target = "password", ignore = true) |
| 引入依赖 (配合表达式使用) | imports | @Mapper(componentModel = "spring", imports = {UUID.class, LocalDateTime.class}) |
2.5.2. 核心避坑指南
在运用上述高级特性时,有三条 “铁律” 必须遵守,否则 Bug 极难排查:
- 优先级铁律:
constant>expression>source。- 如果你配置了
constant,MapStruct 会直接无视你的source属性,哪怕源字段有值也不会用。
- 如果你配置了
- 反向覆盖铁律:
@InheritInverseConfiguration是全量继承。- 如果正向转换有“日期格式化”,反向也会自动生成“日期解析”。
- 必须 手动添加
ignore = true来覆盖那些你不希望前端修改的敏感字段(如 ID、创建时间)。
- 表达式导包铁律:
- 在
expression或defaultExpression中写 Java 代码时,MapStruct 不会自动导包。 - 要么写全限定名(
java.util.UUID),要么在@Mapper(imports = {UUID.class})中显式声明。
- 在







