MapStruct 入门教程:告别手写 setXXX,让对象转换更优雅
前言
在 Java 后端开发中,我们经常会遇到这样的代码:
UserDO userDO = new UserDO();
userDO.setUsername(userVO.getUsername());
userDO.setAvatar(userVO.getAvatar());
userDO.setEmail(userVO.getEmail());
userDO.setPhone(userVO.getPhone());
userDO.setAddress(userVO.getAddress());
字段少的时候还好。
但如果一个对象有十几个、二十几个甚至几十个字段,那么这种代码就会变得非常痛苦。
不仅写起来麻烦,而且非常容易漏字段。
这时候,MapStruct 就派上用场了。
MapStruct 是一个 Java Bean 对象映射工具,它可以帮助我们自动完成:
VO → DO
DO → VO
DTO → DO
DO → DTO
DTO → VO
等对象之间的属性复制。
简单来说:
MapStruct 就是专门帮你自动生成
setXXX(getXXX())代码的工具。
一、为什么项目中会出现对象转换?
在学习 MapStruct 之前,我们必须先理解一个问题:
为什么不能一个 User 类从 Controller 一直用到数据库?
比如一个用户实体:
public class User {
private Long id;
private String username;
private String password;
private Integer status;
private LocalDateTime createTime;
}
如果 Controller 直接接收这个对象:
@PostMapping("/user/update")
public void update(@RequestBody User user) {
}
那么前端理论上就可以提交:
{
"username": "小明",
"password": "123456",
"status": 999
}
问题就来了。
本来你只想允许用户修改:
username
但是数据库实体里面可能还有:
password
status
deleted
createTime
这些内部字段。
所以在实际项目中,通常会把不同职责的数据对象拆开。
例如:
前端请求
↓
ReqVO
↓
Service
↓
DO
↓
数据库
二、VO、DTO、DO 分别是什么?
先简单理解。
1. VO
VO 可以理解成:
面向前端的数据对象。
例如:
public class UpdateUserReqVO {
private String username;
private String avatar;
}
这个对象代表:
用户修改资料时,前端允许提交哪些数据。
2. DO
DO 可以理解成:
数据库对象。
例如:
public class UserDO {
private Long id;
private String username;
private String avatar;
private String password;
private Integer status;
private LocalDateTime createTime;
}
这个对象主要对应数据库表。
3. DTO
DTO 一般表示:
Data Transfer Object,数据传输对象。
常见于:
Service 之间传递
微服务之间传递
RPC 调用
远程接口调用
例如:
public class UserDTO {
private Long id;
private String username;
}
三、问题来了:对象拆开以后怎么转换?
假设前端请求对象:
public class UpdateUserReqVO {
private String username;
private String avatar;
private String email;
}
数据库对象:
public class UserDO {
private Long id;
private String username;
private String avatar;
private String email;
private LocalDateTime createTime;
}
Service 收到:
UpdateUserReqVO
但是保存数据库需要:
UserDO
所以必须转换。
最原始的方式:
UserDO userDO = new UserDO();
userDO.setUsername(reqVO.getUsername());
userDO.setAvatar(reqVO.getAvatar());
userDO.setEmail(reqVO.getEmail());
字段少的时候没有问题。
但如果有:
username
avatar
email
phone
address
gender
birthday
province
city
introduction
那么代码会变成:
userDO.setUsername(reqVO.getUsername());
userDO.setAvatar(reqVO.getAvatar());
userDO.setEmail(reqVO.getEmail());
userDO.setPhone(reqVO.getPhone());
userDO.setAddress(reqVO.getAddress());
userDO.setGender(reqVO.getGender());
userDO.setBirthday(reqVO.getBirthday());
userDO.setProvince(reqVO.getProvince());
userDO.setCity(reqVO.getCity());
userDO.setIntroduction(reqVO.getIntroduction());
这类代码最大的问题不是不能运行。
而是:
重复、繁琐、容易漏字段、维护成本高。
于是 MapStruct 出现了。
四、什么是 MapStruct?
MapStruct 是一个 Java Bean 映射代码生成器。
它最大的特点是:
在编译阶段自动生成对象转换代码。
比如你写:
@Mapper
public interface UserConvert {
UserDO convert(UserReqVO source);
}
MapStruct 编译时会自动生成类似:
public class UserConvertImpl implements UserConvert {
@Override
public UserDO convert(UserReqVO source) {
if (source == null) {
return null;
}
UserDO target = new UserDO();
target.setUsername(source.getUsername());
target.setAvatar(source.getAvatar());
target.setEmail(source.getEmail());
return target;
}
}
所以 MapStruct 并不是什么“神秘的自动映射”。
本质上还是:
setXXX(getXXX())
只不过这部分代码不用你亲自写。
五、MapStruct 为什么值得使用?
1. 减少重复代码
原来:
userDO.setUsername(reqVO.getUsername());
userDO.setAvatar(reqVO.getAvatar());
userDO.setEmail(reqVO.getEmail());
使用 MapStruct:
UserDO userDO =
UserConvert.INSTANCE.convert(reqVO);
非常简洁。
2. 编译期检查
MapStruct 的代码是在编译阶段生成的。
这意味着:
如果字段类型不匹配或者映射配置有问题,很多错误可以直接在编译阶段发现。
比运行时才发现问题更加安全。
3. 性能高
MapStruct 生成的本质就是普通 Java 代码。
比如:
target.setName(source.getName());
不需要在运行时大量依赖反射。
所以性能非常好。
4. 业务代码更加干净
Service 应该关注:
业务流程
而不是:
setName()
setAvatar()
setEmail()
setPhone()
使用 MapStruct 后:
UserDO userDO =
UserConvert.INSTANCE.convert(reqVO);
userDO.setId(userId);
userMapper.updateById(userDO);
代码重点就变成了:
转换
设置业务字段
更新数据库
业务逻辑更加清晰。
六、Spring Boot 中添加 MapStruct
以 Maven 项目为例。
1. 添加依赖
<properties>
<mapstruct.version>1.5.5.Final</mapstruct.version>
</properties>
添加:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
然后添加 MapStruct Processor:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
七、mapstruct 和 mapstruct-processor 有什么区别?
这个地方初学者特别容易懵。
可以直接理解为:
mapstruct
=
给你提供 @Mapper、@Mapping 等 API
而:
mapstruct-processor
=
读取这些注解,并自动生成 Java 代码
整个流程就是:
你写 @Mapper
↓
Java 编译
↓
MapStruct Processor 发现 @Mapper
↓
分析源对象和目标对象
↓
自动生成实现类
↓
继续编译
↓
最终生成 class 文件
所以 MapStruct 是:
编译期代码生成。
八、第一个 MapStruct 转换器
假设有:
public class UpdateUserReqVO {
private String username;
private String avatar;
private String email;
}
以及:
public class UserDO {
private Long id;
private String username;
private String avatar;
private String email;
}
创建:
@Mapper
public interface UserConvert {
UserConvert INSTANCE =
Mappers.getMapper(UserConvert.class);
UserDO convertVO2DO(UpdateUserReqVO bean);
}
使用:
UserDO userDO =
UserConvert.INSTANCE
.convertVO2DO(reqVO);
这时候:
reqVO.username
↓
userDO.username
reqVO.avatar
↓
userDO.avatar
reqVO.email
↓
userDO.email
都会自动映射。
九、为什么只写接口,没有实现类也能运行?
第一次看到:
@Mapper
public interface UserConvert {
}
很多人会疑惑:
接口不是不能直接 new 吗?
确实不能。
但是 MapStruct 会自动生成:
UserConvertImpl
类似:
public class UserConvertImpl
implements UserConvert {
}
然后:
Mappers.getMapper(UserConvert.class);
会获得这个实现类。
所以:
UserConvert.INSTANCE.convertVO2DO(reqVO);
本质上可以理解成:
new UserConvertImpl()
.convertVO2DO(reqVO);
只不过实现类是 MapStruct 生成的。
十、字段名字相同的时候
这是 MapStruct 最简单的情况。
源对象:
private String username;
目标对象:
private String username;
名称一样。
MapStruct 会自动转换。
不需要任何配置。
十一、字段名字不同怎么办?
例如:
源对象:
private String username;
目标对象:
private String nickname;
MapStruct 默认不知道:
username
应该映射:
nickname
所以要告诉它。
@Mapper
public interface UserConvert {
@Mapping(
source = "username",
target = "nickname"
)
UserVO convert(UserDO userDO);
}
意思就是:
source.username
↓
target.nickname
生成的代码类似:
userVO.setNickname(
userDO.getUsername());
十二、忽略某个字段
有时候目标对象里面有字段不希望 MapStruct 自动处理。
例如:
private Long id;
那么可以:
@Mapping(
target = "id",
ignore = true
)
UserDO convert(UserReqVO vo);
意思:
id 不需要你映射。
然后业务代码自己处理:
UserDO userDO =
UserConvert.INSTANCE.convert(reqVO);
userDO.setId(userId);
十三、List 怎么转换?
这是 MapStruct 非常舒服的地方。
假设数据库查询:
List<UserDO> userDOList;
前端需要:
List<UserVO>
传统写法:
List<UserVO> result =
new ArrayList<>();
for (UserDO userDO : userDOList) {
UserVO userVO = new UserVO();
userVO.setId(userDO.getId());
userVO.setUsername(
userDO.getUsername());
result.add(userVO);
}
MapStruct 可以直接写:
List<UserVO> convertDOList2VOList(
List<UserDO> list);
使用:
List<UserVO> result =
UserConvert.INSTANCE
.convertDOList2VOList(
userDOList);
非常方便。
十四、MapStruct 和 Spring 一起使用
实际 Spring Boot 项目中,更推荐让 MapStruct 交给 Spring 管理。
可以写:
@Mapper(componentModel = "spring")
public interface UserConvert {
UserDO convert(UserReqVO vo);
}
然后 Service:
@Service
public class UserServiceImpl {
private final UserConvert userConvert;
public UserServiceImpl(
UserConvert userConvert) {
this.userConvert = userConvert;
}
}
使用:
UserDO userDO =
userConvert.convert(reqVO);
这样就不需要:
Mappers.getMapper(...)
了。
十五、为什么更推荐 componentModel = "spring"?
因为在 Spring Boot 项目中,本身就有:
IoC
DI
依赖注入
所以让 Spring 管理 Convert 会更加统一。
结构变成:
Controller
↓
Service
↓
UserConvert
↓
MapStruct 自动生成实现类
Service 只依赖接口:
UserConvert
而不关心:
UserConvertImpl
具体怎么实现。
这也是一种解耦。
十六、MapStruct 和 BeanUtils 有什么区别?
很多人以前会使用:
BeanUtils.copyProperties(
source,
target
);
看起来也很方便。
但是两者思想不完全一样。
MapStruct:
编译期生成 Java 代码
类似:
target.setName(source.getName());
而一些传统 Bean 映射工具更多依赖运行时机制完成属性复制。
MapStruct 的优势主要包括:
编译期检查
类型安全
高性能
映射规则清晰
容易处理复杂映射
所以在大型项目中,MapStruct 通常更适合承担正式的对象转换工作。
十七、什么时候应该使用 MapStruct?
不是所有项目都必须使用 MapStruct。
如果只是:
一个小 Demo
只有几个对象
一个对象只有两三个字段
偶尔转换一次
直接手写:
userDO.setName(reqVO.getName());
完全没问题。
但是如果项目中存在:
大量 VO
大量 DTO
大量 DO
频繁对象转换
每个对象十几个甚至几十个字段
多人协作
长期维护
那么 MapStruct 就非常适合。
可以记住一句:
对象转换频繁 + 字段多 + 项目分层明显,就非常适合使用 MapStruct。
十八、MapStruct 本质上解决的不是“少写几行代码”
很多人第一次学 MapStruct,会觉得:
不就是少写几个 setter 吗?
其实没有这么简单。
真正的原因是:
项目为了:
职责分离
安全性
解耦
可维护性
会产生:
VO
DTO
DO
这些不同的数据模型。
数据模型拆开以后,又出现:
VO → DO
DO → VO
DTO → DO
大量转换需求。
于是 MapStruct 才出现。
整个因果关系应该这样理解:
项目越来越复杂
↓
需要职责分离
↓
产生 VO、DTO、DO
↓
对象之间需要转换
↓
大量手写 setter
↓
代码重复、容易出错
↓
MapStruct
所以:
不是为了使用 MapStruct 才设计 VO、DTO、DO。
而是:
项目本身需要分层和解耦,于是产生对象转换需求,MapStruct 刚好用来解决这个问题。
十九、一个完整示例
假设前端提交:
{
"username": "犬小哈",
"avatar": "xxx.jpg",
"email": "test@example.com"
}
请求对象:
@Data
public class UpdateUserReqVO {
private String username;
private String avatar;
private String email;
}
数据库对象:
@Data
public class UserDO {
private Long id;
private String username;
private String avatar;
private String email;
private LocalDateTime updateTime;
}
Convert:
@Mapper(componentModel = "spring")
public interface UserConvert {
UserDO convertVO2DO(
UpdateUserReqVO reqVO);
}
Service:
@Service
public class UserServiceImpl {
private final UserConvert userConvert;
public UserServiceImpl(
UserConvert userConvert) {
this.userConvert = userConvert;
}
public void updateUser(
Long userId,
UpdateUserReqVO reqVO) {
UserDO userDO =
userConvert.convertVO2DO(
reqVO);
userDO.setId(userId);
userDO.setUpdateTime(
LocalDateTime.now());
// 更新数据库
userMapper.updateById(userDO);
}
}
整个流程:
前端
↓
UpdateUserReqVO
↓
MapStruct
↓
UserDO
↓
Service 补充业务字段
↓
Mapper
↓
数据库
这就是 MapStruct 在实际项目中的典型使用方式。
二十、总结
MapStruct 是一个非常实用的 Java 对象映射工具。
它主要解决:
VO → DO
DO → VO
DTO → DO
DO → DTO
这类对象转换过程中大量重复的属性复制代码。
它的核心特点是:
编译期生成代码
类型安全
性能高
减少重复代码
提高代码可读性
方便维护
但是要记住:
MapStruct 并不是任何时候都必须使用。
小项目、字段少、转换少的时候,手写代码完全没问题。
当项目开始出现:
大量分层对象
大量字段
频繁转换
复杂映射
MapStruct 的价值才会真正体现出来。
最后记住 MapStruct 最核心的一句话:
MapStruct 不是为了让你少写几个 setter,而是为了降低分层对象之间的数据转换成本。
当你理解这一点之后,再去学习 @Mapper、@Mapping、componentModel、集合转换、自定义转换等高级功能,就会顺畅很多。