MapStruct 就是用来做对象转换的。
Entity → DTO / VO
DTO → Entitytext手写的话基本都是:
vo.setId(entity.getId());
vo.setName(entity.getName());
vo.setAvatar(entity.getAvatar());java字段多了以后,这些代码没什么价值。
MapStruct 在编译时把这些代码生成出来。
基本用法#
@Mapper(componentModel = "spring")
public interface UserMapper {
UserVO toVO(User user);
}java字段名、类型都对应时,直接映射。
例如:
class User {
private Long id;
private String name;
}javaclass UserVO {
private Long id;
private String name;
}java编译后大致就是:
@Component
public class UserMapperImpl implements UserMapper {
@Override
public UserVO toVO(User user) {
if (user == null) {
return null;
}
UserVO vo = new UserVO();
vo.setId(user.getId());
vo.setName(user.getName());
return vo;
}
}java生成代码可以在:
target/generated-sources/annotations/text找到。
所以 MapStruct 不是运行时反射。
它做的是:
编译期生成代码
↓
运行时执行普通 Java 代码text字段名不一样#
用 @Mapping。
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(source = "phone", target = "phoneNumber")
UserVO toVO(User user);
}javasource:源对象
target:目标对象text集合#
定义好单对象转换后,集合直接写:
UserVO toVO(User user);
List<UserVO> toVOList(List<User> users);javaMapStruct 会生成遍历代码。
更新对象#
void update(
UserUpdateDTO dto,
@MappingTarget User user
);java直接修改传进来的 user,不会重新创建。
什么时候适合用#
MapStruct 适合做比较纯粹的对象转换:
Entity → VO
Entity → DTO
DTO → Entity
字段改名
简单类型转换
集合转换
嵌套对象转换text例如:
@Mapping(source = "phone", target = "phoneNumber")
UserVO toVO(User user);java这种事情交给 MapStruct 就行。
什么时候别用#
这里比记 API 更重要。
要查数据库#
例如:
User
↓
根据 departmentId 查 Department
↓
组装 UserVOtext这不是 Mapper 的职责。
放 Service。
Service
↓
查询数据
↓
组装
↓
Mappertext有业务计算#
例如:
VIP 八折
普通用户原价
活动期间再减免text这是业务逻辑。
别写成一堆:
expression = "java(...)"java塞进 Mapper。
放 Service / Domain。
要调用外部服务#
例如:
订单
↓
用户服务
↓
物流服务
↓
优惠券服务
↓
OrderVOtext这也不是 Mapper 应该管的。
Mapper 已经开始很复杂#
如果一个 Mapper 里面出现:
大量 expression
大量条件判断
查询数据库
调用其他服务
复杂业务方法text基本就该拆。
一个简单的判断标准:
只是把 A 的数据搬到 B,用 MapStruct。
需要决定业务怎么做,不要用 MapStruct。
Entity 和 VO 为什么分开#
Entity 和 VO 是两种不同的模型。
Entity 面向数据库:
class User {
private Long id;
private String password;
private Integer isDeleted;
private String avatar;
}javaVO 面向接口:
class UserVO {
private Long id;
private String avatar;
}java这样 Entity 增加字段时,不会自动把字段暴露给前端。
另外,VO 可以有接口层自己的序列化规则。
例如数据库里存:
avatar/abc.jpgtextVO:
@JsonSerialize(using = CdnUrlSerializer.class)
private String avatar;java流程就是:
Entity
↓
MapStruct
↓
VO
↓
Jackson
↓
CdnUrlSerializer
↓
JSONtextMapStruct 只负责把 avatar 搬过去。
CDN URL 怎么生成,是序列化器的事情。
性能#
MapStruct 官方没有给一个固定的“快多少倍”的数字。
官方能确定的是:
-
编译期生成实现类
-
生成代码使用普通方法调用
-
不依赖运行时反射
-
编译期做映射检查
因此它的执行路径基本就是手写 Mapper 那套 getter / setter。
第三方 JMH 测试也能看到类似结果。例如一组公开基准中:
| 实现 | 单次映射 |
|---|---|
| 手写 | 12.94 ns |
| MapStruct | 13.50 ns |
| Orika | 174.78 ns |
| ModelMapper | 4074.94 ns |
MapStruct 和手写代码非常接近。
不过这只是特定测试条件下的结果,不能理解成 MapStruct 在所有场景下都固定快这么多。
实际项目里也没必要为了这点转换性能做过度优化。数据库、网络、Redis、序列化这些通常更值得关注。
AI 时代怎么用#
现在写:
@Mapping(source = "phone", target = "phoneNumber")java这种代码,AI 基本可以直接生成。
所以没必要把 MapStruct 的所有 API 背下来。
记住几个常用的:
@Mapper
@Mapping
@Named
@MappingTarget
@InheritInverseConfigurationjava不会的直接查。
真正需要自己判断的是:
为什么要做这个转换?
哪些字段应该暴露?
这个逻辑应该放 Mapper 还是 Service?
有没有漏字段?
AI 有没有塞进去一堆 expression?text代码可以让 AI 写。
分层和职责还是自己定。
项目里的常用写法#
@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR
)
public interface UserMapper {
@Mapping(source = "phone", target = "phoneNumber")
UserVO toVO(User user);
List<UserVO> toVOList(List<User> users);
}javaunmappedTargetPolicy = ReportingPolicy.ERROR 比较实用。
VO 新增字段但 Mapper 没处理时,直接编译报错。
总结#
MapStruct 没什么好复杂的。
MapStruct
↓
对象转换
Service / Domain
↓
业务逻辑
Jackson
↓
JSONtext记住:
能不能用 MapStruct,不看代码能不能写,而看这件事是不是“对象映射”。
只是:
A → Btext用 MapStruct。
如果已经变成:
查数据
算业务
调服务
做流程text就别往 Mapper 里塞。
AI 负责生成具体代码。
程序员需要判断的是对象边界和职责。