3220 字
约 10 分钟
11
MapStruct 入门教程:告别手写 setXXX,让对象转换更优雅

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@MappingcomponentModel、集合转换、自定义转换等高级功能,就会顺畅很多。

MapStruct 入门教程:告别手写 setXXX,让对象转换更优雅
http://clxhxhhr.top/posts/374/
作者
clxstart
发布于
2026-08-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。