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 将多个原实体类中的属性拷贝到目标实体类中(原实体类中有同名属性)

想象一下,你正在为机场地勤人员或航空公司运营中心开发一个内部系统。这个系统最重要的一个页面就是“实时航班动态看板”。这个看板需要在一个界面上,清晰地展示一个航班的全方位信息


这些信息来源于机场内不同的子系统:

  1. 航班基础信息系统:提供航班的固定信息,如航班号、机型、计划起飞/到达时间、出发/到达机场
  2. 实时运行状态系统:提供航班的动态信息,如实际起飞/到达时间、当前状态(延误、登机、起飞中)、登机口
  3. 机组人员系统:提供当班机长和乘务长的姓名

我们的任务就是从这三个独立的系统中获取数据,然后将它们无缝地合并成一个统一的“航班动态信息”对象,最终呈现在看板上


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日"

}

我们的目标是将 UserDOregistrationTime 字段,通过自定义逻辑,转换为 UserDTOregistrationDateDisplay 字段

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 官网

Logo

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

更多推荐