MapStruct转换实体

前几天在代码中看到有人用UnitChangeConvert.INSTANCE.dtoToBO(processVo),感觉非常帅。这只是一个接口,并没有给出实现类,就给实体转换了,省去了我们自己写转换类的时间,今天摸鱼中了解了下:MapStruct 是一个专注于「Java Bean 之间属性映射」的代码生成器,与 Spring 原生转换、MyBatis-Plus 类型转换器是完全不同的工具,解决的是「对象与对象之间的属性拷贝」问题(如 DTO ↔ POJO、VO ↔ Entity),而非参数 / 数据库字段的类型转换。

下面从 核心定位、与其他转换工具的区别、使用场景、快速上手、高级特性 五个维度,帮你理清 MapStruct 的核心价值和用法:

一、核心定位:Java Bean 映射的 "代码生成神器"

日常开发中,我们经常需要在不同层级的 Java Bean 之间拷贝属性(如:

  • Controller 接收前端 UserDTO → 服务层转换为 UserEntity 存入数据库;
  • 数据库查询出 UserEntity → 转换为 UserVO 返回给前端)。

传统做法是手动写 setter 拷贝(繁琐易出错),或用 BeanUtils.copyProperties(反射效率低、字段名不一致需手动处理、类型转换不灵活)。

MapStruct 的核心解决思路:

  • 基于「注解 + 接口」定义映射规则;
  • 编译期自动生成 类型安全、无反射、高效 的映射实现类(纯 Java 代码,不是运行时动态处理);
  • 支持字段名映射、类型自动转换、自定义转换逻辑、依赖注入等。

二、与其他 "转换工具" 的核心区别

你之前接触的工具和 MapStruct 适用场景完全不同,用表格清晰对比:

工具 / 框架 核心用途 适用场景 底层实现
MapStruct Java Bean ↔ Java Bean(属性拷贝) DTO↔Entity、VO↔POJO(业务层) 编译期生成 Java 代码(无反射)
Spring 原生 Converter 基础类型 / 简单对象转换(如 String→Integer) Web 参数转换、配置注入(Web / 配置层) 接口实现 + Spring 容器管理
MyBatis-Plus 转换器 数据库字段 ↔ Java 实体(ORM 层) 数据库存储类型→Java 类型 MyBatis 类型处理器接口
BeanUtils(Spring/Commons) 快速属性拷贝 简单场景(字段名完全一致) 运行时反射(效率低)

一句话区分:

  • 要拷贝两个 Java Bean 的属性 → 用 MapStruct;
  • 要把 String 转 Integer / 枚举(非 Bean 拷贝) → 用 Spring Converter
  • 要把数据库字段转 Java 实体 → 用 MyBatis-Plus 转换器。

三、MapStruct 快速上手(Spring Boot 环境)

步骤 1:引入依赖(Maven)

xml

xml 复制代码
<!-- MapStruct 核心依赖 -->
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.5.Final</version> <!-- 推荐稳定版 -->
</dependency>
​
<!-- 编译期代码生成依赖(必须,否则不会生成实现类) -->
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.5.Final</version>
    <scope>provided</scope>
</dependency>
​
<!-- 若用 Lombok,需确保 MapStruct 能识别 Lombok 生成的 getter/setter -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.30</version>
    <scope>provided</scope>
</dependency>
​
<!-- Lombok 与 MapStruct 兼容依赖(Java 11+ 可能需要) -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok-mapstruct-binding</artifactId>
    <version>0.2.0</version>
    <scope>provided</scope>
</dependency>

步骤 2:定义待映射的 Java Bean

假设存在「用户实体」和「用户 DTO」(字段名部分不一致,类型不同):

typescript 复制代码
// 数据库实体(POJO)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserEntity {
    private Long id;          // 主键
    private String userName;  // 用户名(字段名:userName)
    private Integer age;      // 年龄(int 类型)
    private LocalDate birth;  // 生日(LocalDate 类型)
    private String phone;     // 手机号
}
​
// 前端传输对象(DTO)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {
    private Long userId;      // 主键(字段名:userId,与实体 id 对应)
    private String name;      // 用户名(字段名:name,与实体 userName 对应)
    private String age;       // 年龄(String 类型,与实体 int 对应)
    private String birth;     // 生日(String 类型,格式:yyyy-MM-dd)
    private String phoneNum;  // 手机号(字段名:phoneNum,与实体 phone 对应)
}

步骤 3:定义 MapStruct 映射接口

@Mapper 注解(MapStruct 的注解,非 MyBatis 的 @Mapper!)定义映射规则,componentModel = "spring" 表示让 Spring 管理映射器实例(支持依赖注入):

less 复制代码
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
​
// 关键注解:componentModel="spring" → Spring 管理该映射器
@Mapper(componentModel = "spring", imports = {LocalDate.class, DateTimeFormatter.class})
public interface UserConvert {
​
    // 方式 1:Spring 依赖注入用(无需手动创建实例)
    // 方式 2:非 Spring 环境用(手动获取实例)
    // UserConvert INSTANCE = Mappers.getMapper(UserConvert.class);
​
    /**
     * DTO → 实体(核心映射方法)
     * @Mapping:指定字段映射规则(字段名不一致、类型转换、格式转换)
     */
    @Mappings({
        // DTO 的 userId → 实体的 id(字段名不一致)
        @Mapping(source = "userId", target = "id"),
        // DTO 的 name → 实体的 userName(字段名不一致)
        @Mapping(source = "name", target = "userName"),
        // DTO 的 age(String)→ 实体的 age(int):自动类型转换(MapStruct 内置支持)
        @Mapping(source = "age", target = "age"),
        // DTO 的 birth(String)→ 实体的 birth(LocalDate):自定义格式转换
        @Mapping(
            source = "birth",
            target = "birth",
            dateFormat = "yyyy-MM-dd" // 指定日期格式
        ),
        // DTO 的 phoneNum → 实体的 phone(字段名不一致)
        @Mapping(source = "phoneNum", target = "phone")
    })
    UserEntity dtoToEntity(UserDTO userDTO);
​
    /**
     * 实体 → DTO(反向映射,字段名对应规则与正向一致)
     * 可复用正向映射规则,用 @InheritInverseConfiguration 简化
     */
    @InheritInverseConfiguration(name = "dtoToEntity")
    @Mapping(
        source = "birth",
        target = "birth",
        dateFormat = "yyyy-MM-dd" // 反向转换也需指定日期格式
    )
    UserDTO entityToDto(UserEntity userEntity);
​
    /**
     * 自定义转换逻辑(如特殊格式处理、复杂计算)
     * 若 MapStruct 内置转换不满足,可定义默认方法(default)
     */
    default Integer stringToAge(String ageStr) {
        if (ageStr == null || ageStr.trim().isEmpty()) {
            return 0; // 空值默认 0
        }
        try {
            return Integer.parseInt(ageStr.trim());
        } catch (NumberFormatException e) {
            return 0; // 格式错误默认 0
        }
    }
​
    // 反向转换:Integer → String
    default String ageToString(Integer age) {
        return age == null ? "" : String.valueOf(age);
    }
}

步骤 4:编译项目,查看生成的实现类

MapStruct 会在 编译期 生成 UserConvertImpl 实现类(无需手动写),路径在 target/generated-sources/annotations/ 下,核心逻辑如下(自动生成的纯 Java 代码):

typescript 复制代码
// 自动生成的实现类(Spring 管理)
@Component
public class UserConvertImpl implements UserConvert {
​
    @Override
    public UserEntity dtoToEntity(UserDTO userDTO) {
        if (userDTO == null) {
            return null;
        }
        UserEntity userEntity = new UserEntity();
        userEntity.setId(userDTO.getUserId());
        userEntity.setUserName(userDTO.getName());
        // 调用自定义的 stringToAge 方法转换类型
        userEntity.setAge(stringToAge(userDTO.getAge()));
        // 自动按指定格式转换 String → LocalDate
        if (userDTO.getBirth() != null) {
            userEntity.setBirth(LocalDate.parse(userDTO.getBirth(), DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        }
        userEntity.setPhone(userDTO.getPhoneNum());
        return userEntity;
    }
​
    @Override
    public UserDTO entityToDto(UserEntity userEntity) {
        // 类似正向转换逻辑,自动处理字段映射和类型转换
    }
​
    // 自动生成自定义方法的调用逻辑...
}

步骤 5:在 Spring 中使用映射器

通过 @Autowired 注入 UserConvert,直接调用映射方法:

java 复制代码
@Service
public class UserService {
​
    @Autowired
    private UserConvert userConvert; // 注入 MapStruct 映射器
​
    public void addUser(UserDTO userDTO) {
        // 1. DTO → 实体(自动完成字段映射和类型转换)
        UserEntity userEntity = userConvert.dtoToEntity(userDTO);
        
        // 2. 存入数据库(假设用 MyBatis 操作)
        // userMapper.insert(userEntity);
        
        System.out.println("转换后的实体:" + userEntity);
    }
​
    public UserDTO getUserById(Long id) {
        // 1. 从数据库查询实体
        UserEntity userEntity = new UserEntity(id, "张三", 25, LocalDate.of(2000, 1, 1), "13800138000");
        
        // 2. 实体 → DTO(反向转换)
        UserDTO userDTO = userConvert.entityToDto(userEntity);
        
        return userDTO;
    }
}

测试效果:

ini 复制代码
// 测试 DTO → 实体
UserDTO dto = new UserDTO(1L, "张三", "25", "2000-01-01", "13800138000");
UserEntity entity = userConvert.dtoToEntity(dto);
// 输出:UserEntity(id=1, userName=张三, age=25, birth=2000-01-01, phone=13800138000)
​
// 测试实体 → DTO
UserEntity entity = new UserEntity(1L, "张三", 25, LocalDate.of(2000, 1, 1), "13800138000");
UserDTO dto = userConvert.entityToDto(entity);
// 输出:UserDTO(userId=1, name=张三, age=25, birth=2000-01-01, phoneNum=13800138000)

四、MapStruct 核心注解与高级特性

1. 核心注解(常用)

注解 作用 示例
@Mapper 标识映射接口,指定组件模型(如 Spring) @Mapper(componentModel = "spring")
@Mapping 单个字段映射规则 @Mapping(source = "name", target = "userName")
@Mappings 多个 @Mapping 的集合 包裹多个 @Mapping 注解
@InheritInverseConfiguration 继承反向映射规则(避免重复写注解) @InheritInverseConfiguration(name = "dtoToEntity")
@MappingTarget 映射到已存在的对象(更新属性) void updateEntity(UserDTO dto, @MappingTarget UserEntity entity)
@Named 命名自定义转换方法(用于复杂映射) @Named("stringToAge") default Integer stringToAge(String s) {}

2. 高级特性

(1)集合映射(自动支持 List/Set/Array)

MapStruct 会自动为集合生成映射方法,无需手动定义:

scss 复制代码
@Mapper(componentModel = "spring")
public interface UserConvert {
    // 单个对象映射(已定义)
    UserEntity dtoToEntity(UserDTO dto);
    UserDTO entityToDto(UserEntity entity);
​
    // 集合映射(自动生成,无需手动写实现)
    List<UserEntity> dtoListToEntityList(List<UserDTO> dtoList);
    Set<UserDTO> entitySetToDtoSet(Set<UserEntity> entitySet);
}
(2)自定义转换逻辑(复杂场景)

若内置转换不满足(如枚举转换、对象嵌套),可定义 default 方法或单独的转换类:

kotlin 复制代码
// 示例:枚举转换
public enum GenderEnum {
    MALE(1, "男"), FEMALE(2, "女");
    // getter/setter/静态方法...
}
​
// DTO 中 gender 是 String 类型("男"/"女"),实体中是 GenderEnum
@Mapping(
    source = "gender",
    target = "gender",
    qualifiedByName = "genderStrToEnum" // 指定自定义转换方法
)
UserEntity dtoToEntity(UserDTO dto);
​
// 自定义枚举转换方法(用 @Named 标识)
@Named("genderStrToEnum")
default GenderEnum genderStrToEnum(String genderStr) {
    if ("男".equals(genderStr)) return GenderEnum.MALE;
    if ("女".equals(genderStr)) return GenderEnum.FEMALE;
    return null;
}
(3)依赖注入其他服务

componentModel = "spring",映射器可注入 Spring Bean(如业务服务、工具类):

kotlin 复制代码
@Mapper(componentModel = "spring")
public interface UserConvert {
​
    @Autowired
    UserService userService; // 注入 Spring 服务
​
    @Mapping(
        source = "userId",
        target = "deptId",
        qualifiedByName = "getDeptIdByUserId" // 调用注入的服务
    )
    UserEntity dtoToEntity(UserDTO dto);
​
    @Named("getDeptIdByUserId")
    default Long getDeptIdByUserId(Long userId) {
        // 调用业务服务获取部门ID(复杂逻辑)
        return userService.getDeptIdByUserId(userId);
    }
}
(4)日期 / 数字格式转换

通过 dateFormat/numberFormat 指定格式:

less 复制代码
// 日期格式
@Mapping(source = "birth", target = "birth", dateFormat = "yyyy-MM-dd HH:mm:ss")
// 数字格式(如保留2位小数)
@Mapping(source = "amount", target = "amount", numberFormat = "#.00")
UserEntity dtoToEntity(UserDTO dto);

五、注意事项

  1. 编译期生成代码

    • 必须引入 mapstruct-processor 依赖,否则不会生成实现类,运行时会报「找不到实现类」错误;
    • 若用 IDEA,需开启「Annotation Processing」(Settings → Build → Compiler → Annotation Processors),否则 IDEA 可能不识别生成的类。
  2. 与 Lombok 兼容

    • 确保 Lombok 版本 ≥ 1.18.16,MapStruct 版本 ≥ 1.4.2;
    • 若字段无 getter/setter(如用 @Data 生成),MapStruct 无法识别字段,需确保 Lombok 正确生成访问器。
  3. 字段名匹配规则

    • 默认按「字段名相同」映射(忽略大小写?不,严格匹配);
    • 若字段名是 userName(实体)和 user_name(DTO),可开启 unmappedTargetPolicy = ReportingPolicy.IGNORE 忽略未映射字段,或手动指定 @Mapping

总结

MapStruct 是 Java Bean 映射的最优解,核心优势:

  1. 类型安全:编译期检查字段名、类型是否匹配,避免运行时错误;
  2. 效率高:无反射,编译期生成原生 Java 代码,性能远超 BeanUtils
  3. 灵活:支持字段映射、类型转换、自定义逻辑、依赖注入;
  4. 简化代码:无需手动写 setter 拷贝,注解驱动开发。
相关推荐
00后程序员1 小时前
Objective-C 测试(OC 测试)指南 从单元测试到性能调优的多工具协同方法
后端
Boop_wu1 小时前
[Java 面试] 多线程1
java·开发语言
专注于大数据技术栈1 小时前
java学习--main方法
java·开发语言·学习
Asthenia04122 小时前
技术复盘:Solon-MCP 日志统一配置背后的技术架构分析
后端
2501_941802482 小时前
C++高性能并发编程实战:从多线程管理到内存优化与任务调度全流程解析
java·开发语言·c++
0***R5152 小时前
SpringBoot集成Elasticsearch实战
java·spring boot·elasticsearch
小希smallxi2 小时前
在 Spring Boot 项目中,如何在非 Web 层(如 AOP)中获取 Session 信息
前端·spring boot·后端
00后程序员2 小时前
iOS 商店上架全流程解析 从工程准备到审核通过的系统化实践指南
后端