MapStruct快速入门(什么是MapStruct、MapStruct与BeanUtils.copyProperties方法有什么区别,MapStruct进阶用法,MapStruct的实现原理)
文章目录
1. 什么是MapStruct
我们先来看一下 MapStruct 官网上的简介
https://mapstruct.org/documentation/stable/reference/html/#introduction

看到 MapStruct 是一个 Java 注释处理器 ,用于生成类型安全的 bean 映射类。 这句话时是不是有点懵,什么是注释处理器,什么又是 bean 映射类
其实,我们不用管这些官方定义,我们只需要知道 MapStruct 主要是用来干什么的。简单来说,MapStruct 的作用就是将一个类的属性拷贝到另一个类中,就像我们使用过的 BeanUtils.copyProperties 方法一样,例如将 Entity 实体类中的属性拷贝到 DTO 中
2. MapStruct与Spring提供的BeanUtils.copyProperties方法有什么区别
2.1 实现原理
-
MapStruct 在编译阶段根据接口定义生成具体的 Java 实现代码,调用时直接执行生成的 setter/getter 方法,无反射开销
-
BeanUtils.copyProperties 使用 Java 反射机制在运行时动态获取目标对象的属性并进行赋值,每次调用都涉及反射查找,效率较低
MapStruct 是“编译时增强”,它生成的代码和手写代码几乎没有区别,性能最优。而 BeanUtils 是“运行时反射”,每次都要查找 Field 并赋值,性能损耗明显,尤其在循环或高频调用场景下
2.2 性能表现
- MapStruct 性能极高,接近原生代码调用速度,无反射开销,适合高频调用场景
- BeanUtils.copyProperties 性能较差,反射开销大,大量使用时会影响系统性能,不适合高并发或循环场景
2.3 类型安全
- MapStruct 在编译时检查,类型不匹配会直接报错,提前发现问题
- BeanUtils.copyProperties 在运行时检查,类型不匹配时可能静默失败或抛出异常,问题发现较晚
MapStruct 在编译阶段就能发现映射错误,有助于提升代码健壮性。BeanUtils 的错误往往在运行时暴露,不利于线上稳定性
2.4 字段映射
- MapStruct 支持不同字段名映射、自定义映射逻辑,灵活性极高
- BeanUtils.copyProperties 仅支持相同字段名映射,无法自定义映射规则,功能受限
MapStruct 能处理如 mobileNumber→ cellphoneNumber这种字段名不一致的情况,而 BeanUtils 无法做到,因为 BeanUtils 要求原实体类和目标实体类的字段名完全一致
2.5 复杂对象映射
- MapStruct 支持嵌套对象、集合、枚举等复杂映射,可处理企业级数据转换需求
- BeanUtils.copyProperties 不支持嵌套对象、集合、枚举等复杂映射,仅支持简单属性复制,无法处理复杂结构
2.6 自定义转换逻辑
- MapStruct 支持通过 @Mapping 注解或自定义方法实现复杂的映射逻辑
- BeanUtils.copyProperties 不支持,无法添加自定义映射逻辑,功能受限
MapStruct 允许开发者编写自定义转换逻辑,如字段格式化、条件判断等,灵活性极高。BeanUtils 则完全依赖默认规则,无法扩展
3. MapStruct快速入门
3.1 引入Maven依赖
3.1.1 dependency
properties 标签
<org.mapstruct.version>1.6.3</org.mapstruct.version>
dependency 标签
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>

3.1.2 plugin
plugin 标签(放在 artifactId 为 maven-compiler-plugin 的 annotationProcessorPaths 标签下)
与 mapstruct 相关的依赖最好都放在 lombok 依赖后面
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>

3.1.3 项目中使用了lombok后需要额外引入的依赖
与 mapstruct 相关的依赖最好都放在 lombok 依赖后面
注意:如果项目使用了 lombok,plugin 标签中还需要引入 lombok-mapstruct-binding,具体的原因可以参考 mapstruct 官网给出的解释
https://mapstruct.org/documentation/stable/reference/html/#lombok
https://mapstruct.org/documentation/stable/reference/html/#lombok

<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>

3.2 准备工作
我们准备两个类,用于演示 MapStruct 的快速入门
User.java
import lombok.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
@EqualsAndHashCode(callSuper = false)
public class User {
private Integer id;
private String nickname;
private LocalDate birthday;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
UserVO.java
import lombok.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
@EqualsAndHashCode(callSuper = false)
public class UserVO {
private Integer id;
private String nickname;
private LocalDate birthday;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
3.3 MapStruct快速上手
我们需要编写一个接口,接口的名字自定义(建议用 XXXConverter 命名,例如 UserConverter),并在接口上添加 @Mapper 注解(注意:不是 Mybatis 中的@Mapper 注解)
接着在接口中定义一个方法,如果我们要将 User 类的属性拷贝到 UserVO 中,那么
- 方法接收的参数的类型(source)是 User
- 方法返回值的类型(target)是 UserVO
接下来为大家演示在不同的环境中如何使用 MapStruct 拷贝属性
3.3.1 非Spring环境

import cn.edu.scau.mapstruct.pojo.User;
import cn.edu.scau.mapstruct.vo.UserVO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface UserConverter {
UserConverter INSTANCE = Mappers.getMapper(UserConverter.class);
UserVO userToUserVO(User user);
}
如何使用 UserConverter

import cn.edu.scau.mapstruct.converter.UserConverter;
import cn.edu.scau.mapstruct.pojo.User;
import cn.edu.scau.mapstruct.vo.UserVO;
import java.time.LocalDate;
import java.time.LocalDateTime;
public class UserConverterTests {
public static void main(String[] args) {
User user = new User();
user.setId(1);
user.setNickname("张三");
user.setBirthday(LocalDate.now());
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
UserVO userVO = UserConverter.INSTANCE.userToUserVO(user);
System.out.println(user);
System.out.println(userVO);
}
}
3.3.2 Spring环境
注意:指定 @Mapper 注解的 componentModel 属性为 spring 之后需要去掉
UserConverter INSTANCE = Mappers.getMapper(UserConverter.class);代码
在 Spring 环境中使用 MapStruct 与非 Spring 环境类似,在 XXXConverter 类上指定 @Mapper 注解的 componentModel 属性为 spring 就可以了
指定 @Mapper 注解的 componentModel 属性为 spring 后,生成的实现类上会添加 @Component 注解

import cn.edu.scau.mapstruct.pojo.User;
import cn.edu.scau.mapstruct.vo.UserVO;
import org.mapstruct.Mapper;
@Mapper(componentModel = "spring") // 当 componentModel 设置为 spring 时,生成的实现类上会添加 @Component 注解
public interface UserConverter {
UserVO userToUserVO(User user);
}
如何使用 UserConverter(可以使用 @Autowired 注解注入 UserConverter,也可以使用构造器注入 UserConverter)

import cn.edu.scau.mapstruct.converter.UserConverter;
import cn.edu.scau.mapstruct.pojo.User;
import cn.edu.scau.mapstruct.vo.UserVO;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Component
public class UserConverterInSpringTests {
private final UserConverter userConverter;
public UserConverterInSpringTests(UserConverter userConverter) {
this.userConverter = userConverter;
}
public void testUserConverterInSpring() {
User user = new User();
user.setId(1);
user.setNickname("张三");
user.setBirthday(LocalDate.now());
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
UserVO userVO = userConverter.userToUserVO(user);
System.out.println(user);
System.out.println(userVO);
}
}
4. MapStruct进阶用法
4.1 原实体类(source)和目标实体类(target)的字段名称不一致
如果原实体类(source)和目标实体类(target)的字段名称不一致,需要额外处理
例如 User 类中手机号的字段名为 mobileNumber,UserVO 类中手机号的字段名为 cellphoneNumber,我们就需要在 UserConverter 接口的 userToUserVO 方法上添加 @Mapping 注解来自定义转换逻辑
@Mapping(source = "mobileNumber", target = "cellphoneNumber")

如果有多个字段名称不一致,可以在方法上使用多个 @Mapping 注解,也可以使用 @Mappings 注解将多个 @Mapping 注解包裹起来,
本文演示的是使用 @Mappings 注解将多个 @Mapping 注解包裹起来的方式

4.2 将多个原实体类中的属性拷贝到目标实体类中(原实体类中有同名属性)
想象一下,你正在为机场地勤人员或航空公司运营中心开发一个内部系统。这个系统最重要的一个页面就是“实时航班动态看板”。这个看板需要在一个界面上,清晰地展示一个航班的全方位信息
这些信息来源于机场内不同的子系统:
- 航班基础信息系统:提供航班的固定信息,如航班号、机型、计划起飞/到达时间、出发/到达机场
- 实时运行状态系统:提供航班的动态信息,如实际起飞/到达时间、当前状态(延误、登机、起飞中)、登机口
- 机组人员系统:提供当班机长和乘务长的姓名
我们的任务就是从这三个独立的系统中获取数据,然后将它们无缝地合并成一个统一的“航班动态信息”对象,最终呈现在看板上
4.2.1 定义源实体类和目标实体类
源实体类 : FlightInfo.java (航班基础信息)
import lombok.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class FlightStatus {
private String flightNumber; // 航班号,用于关联
private String actualDeparture; // 实际起飞时间
private String actualArrival; // 实际到达时间
private String currentStatus; // 当前状态,如 "Boarding", "Departed", "Delayed"
private String gate; // 登机口
}
源实体类 : FlightStatus.java (实时运行状态)
import lombok.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class FlightInfo {
private String flightNumber; // 航班号,如 "CA1234"
private String aircraftType; // 机型,如 "Boeing 737-800"
private String scheduledDeparture; // 计划起飞时间,如 "2023-10-04 08:00"
private String scheduledArrival; // 计划到达时间,如 "2023-10-04 10:30"
private String departureAirport; // 出发机场,如 "PEK"
private String arrivalAirport; // 到达机场,如 "SHA"
}
源实体类 : CrewInfo.java (机组人员信息)
import lombok.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class CrewInfo {
private String flightNumber; // 航班号,用于关联
private String captainName; // 机长姓名
private String purserName; // 乘务长姓名
}
目标实体类: FlightDashboardView.java (看板视图对象)
import lombok.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class FlightDashboardView {
// --- 来自 FlightInfo ---
private String flightNumber;
private String aircraftType;
private String scheduledDeparture;
private String scheduledArrival;
private String departureAirport;
private String arrivalAirport;
// --- 来自 FlightStatus ---
private String actualDeparture;
private String actualArrival;
private String currentStatus;
private String gate;
// --- 来自 CrewInfo ---
private String captainName;
private String purserName;
}
4.2.2 创建 Converter 接口
因为三个原实体类中都含有 flightNumber 字段,MapStruct 不知道要采用哪一个实体类的 flightNumber 字段,所以我们需要手动指定目标实体类中的 flightNumber 字段来源于哪个实体类
@Mapping(source = "flightInfo.flightNumber", target = "flightNumber")

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
public interface FlightDashboardViewConverter {
@Mapping(source = "flightInfo.flightNumber", target = "flightNumber")
FlightDashboardView toDashboardView(FlightInfo flightInfo, FlightStatus flightStatus, CrewInfo crewInfo);
}
4.3 自定义转换逻辑
4.3.1 场景设定
源实体 (UserDO): 代表从数据库查询出来的用户对象
registrationTime: 存储为 String 类型,值为 ISO 格式的日期时间,如 "2023-08-17T10:30:00"
import lombok.*;
/**
* 源实体:模拟从数据库中查询出的用户对象
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
@EqualsAndHashCode(callSuper = false)
public class UserDO {
private Long id;
private String username;
private String registrationTime; // "2023-08-17T10:30:00"
}
目标实体 (UserDTO): 代表要传递给前端展示的用户对象
registrationDateDisplay: 存储为 String 类型,希望显示为更友好的格式,如 "2023年08月17日"
import lombok.*;
/**
* 目标实体:模拟传递给前端的展示对象
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
@EqualsAndHashCode(callSuper = false)
public class UserDTO {
private Long id;
private String username;
private String registrationDateDisplay; // "2023年08月17日"
}
我们的目标是将 UserDO 的 registrationTime 字段,通过自定义逻辑,转换为 UserDTO 的 registrationDateDisplay 字段
4.3.2 如何自定义转换逻辑
4.3.2.1 在XXXConverter接口中自定义转换逻辑
可以直接在 XXXConverter 接口中自定义一个包含自定义转换逻辑的方法,并用 @Name 注解为方法起一个名字,在 @Mapping 注解中通过 qualifiedByName 属性引用方法

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Mapper(componentModel = "spring")
public interface CustomConverter {
@Mapping(source = "registrationTime", target = "registrationDateDisplay", qualifiedByName = "isoDateTimeToDisplay")
UserDTO userDOToUserDto(UserDO userDO);
/**
* 自定义转换方法: 将 ISO 日期时间字符串格式化为 "YYYY年MM月dd日"
* `@Named` 注解可以为方法起一个名字,以便在 @Mapping 中引用
*
* @param isoDateTime ISO 格式的日期时间字符串,例如 "2023-08-17T10:30:00"
* @return 格式化后的日期字符串,例如 "2023年08月17日"
*/
@Named("isoDateTimeToDisplay")
default String isoDateTimeToDisplay(String isoDateTime) {
if (isoDateTime == null || isoDateTime.isEmpty()) {
return "";
}
try {
// 1. 使用 LocalDateTime.parse() 来解析包含时间的完整字符串,它能正确处理 "2023-08-17T10:30:00" 这种格式
LocalDateTime dateTime = LocalDateTime.parse(isoDateTime);
// 2. 从 LocalDateTime 对象中获取 LocalDate 部分
LocalDate date = dateTime.toLocalDate();
// 3. 定义目标格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
// 4. 格式化输出
return date.format(formatter);
} catch (Exception e) {
// 如果解析失败(例如字符串格式不正确),返回空字符串或原始字符串
// 这里选择返回空字符串,避免前端显示错误数据
return "";
}
}
}
4.3.2.2 在新的类(工具类)中自定义转换逻辑(推荐使用)
虽然可以直接在 XXXConverter 接口中自定义一个包含自定义转换逻辑的方法,但是当接口中的自定义转换逻辑越来越多时,整个接口将变得十分臃肿
我们可以自定义转换逻辑的方法抽取到一个类(通常时工具类)中,通过 @Mapper 注解的 uses 属性引用类中的指定方法,能够达到同样的效果

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring", uses = {DateFormatterUtil.class})
public interface CustomConverter {
@Mapping(source = "registrationTime", target = "registrationDateDisplay", qualifiedByName = "isoDateTimeToDisplay")
UserDTO userDOToUserDto(UserDO userDO);
}
DateFormatterUtil.java

import org.mapstruct.Named;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 独立的日期格式化工具类
* 这个类可以被多个 Mapper 复用
*/
public class DateFormatterUtil {
/**
* 自定义转换方法: 将 ISO 日期时间字符串格式化为 "YYYY年MM月dd日"
* `@Named` 注解可以为方法起一个名字,以便在 @Mapping 中引用
*
* @param isoDateTime ISO 格式的日期时间字符串,例如 "2023-08-17T10:30:00"
* @return 格式化后的日期字符串,例如 "2023年08月17日"
*/
@Named("isoDateTimeToDisplay")
public static String isoDateTimeToDisplay(String isoDateTime) {
if (isoDateTime == null || isoDateTime.isEmpty()) {
return "";
}
try {
// 1. 使用 LocalDateTime.parse() 来解析包含时间的完整字符串,它能正确处理 "2023-08-17T10:30:00" 这种格式
LocalDateTime dateTime = LocalDateTime.parse(isoDateTime);
// 2. 从 LocalDateTime 对象中获取 LocalDate 部分
LocalDate date = dateTime.toLocalDate();
// 3. 定义目标格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
// 4. 格式化输出
return date.format(formatter);
} catch (Exception e) {
// 如果解析失败(例如字符串格式不正确),返回空字符串或原始字符串
// 这里选择返回空字符串,避免前端显示错误数据
return "";
}
}
}
5. MapStruct的实现原理
前面在介绍 MapStruct 与 Spring 提供的 BeanUtils.copyProperties 方法有什么区别 时,只是简单地介绍了一下 MapStruct 的实现原理,我们在此详细说一下 MapStruct 的实现原理
MapStruct 在编译阶段根据接口定义生成具体的实现类,在实现类中调用时直接执行生成的 setter/getter 方法,无反射开销(可以理解为手动将原实体类中的属性通过 getter 取出来,再手动通过 setter 将原实体类中的属性拷贝到目标类中)
怎么验证这一点呢,我们可以在项目生成的 target 目录下查看由 MapStruct 生成的实现类,实现类中的代码采用的正是最朴素的手动 get/set 属性的方式

6. IntelliJ IDEA 中与MapStruct相关的插件
在 IntelliJ IDEA 的插件市场中安装 MapStruct Support 插件
MapStruct Support

如果目标实体类中有某个属性没有被映射到,那么接口中的方法底部会出现黄色波浪线。将鼠标悬浮在方法名上,可以看到有哪些属性没有被映射到

鼠标点击 Add unmapped target property: 'cellphoneNumber' 可以自动生成对应的 @Mapping 注解

安装 MapStruct Support 插件后,按住 CTRL 键,鼠标左键 source 或 target 的字段名,可以跳转到对应的实体类

按住 CTRL 键后,鼠标左键 qualifiedByName 的方法名,也可以跳转到对应的自定义转换方法

7. MapStruct的更多用法
参考 MapStruct 官网
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)