前几天在代码中看到有人用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);
五、注意事项
-
编译期生成代码:
- 必须引入
mapstruct-processor依赖,否则不会生成实现类,运行时会报「找不到实现类」错误; - 若用 IDEA,需开启「Annotation Processing」(Settings → Build → Compiler → Annotation Processors),否则 IDEA 可能不识别生成的类。
- 必须引入
-
与 Lombok 兼容:
- 确保 Lombok 版本 ≥ 1.18.16,MapStruct 版本 ≥ 1.4.2;
- 若字段无 getter/setter(如用
@Data生成),MapStruct 无法识别字段,需确保 Lombok 正确生成访问器。
-
字段名匹配规则:
- 默认按「字段名相同」映射(忽略大小写?不,严格匹配);
- 若字段名是
userName(实体)和user_name(DTO),可开启unmappedTargetPolicy = ReportingPolicy.IGNORE忽略未映射字段,或手动指定@Mapping。
总结
MapStruct 是 Java Bean 映射的最优解,核心优势:
- 类型安全:编译期检查字段名、类型是否匹配,避免运行时错误;
- 效率高:无反射,编译期生成原生 Java 代码,性能远超
BeanUtils; - 灵活:支持字段映射、类型转换、自定义逻辑、依赖注入;
- 简化代码:无需手动写
setter拷贝,注解驱动开发。