外观
MapStruct 对象转换
2026-04-01
版本: 1.6.3 官网: mapstruct.org
MapStruct 在编译期根据接口定义自动生成对象转换代码,相比 BeanUtils.copyProperties 零反射、零性能损耗,且类型不匹配会在编译期报错,安全可靠。
Nebula 规范:所有 DO ↔ VO / DTO 的转换必须使用 MapStruct,禁止在 Service 中手动 new VO 再逐字段赋值。
引入依赖
// build.gradle
dependencies {
implementation 'org.mapstruct:mapstruct'
annotationProcessor 'org.mapstruct:mapstruct-processor'
// Lombok + MapStruct 同时使用时,Lombok 必须在 MapStruct 之前处理
annotationProcessor 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
}基础用法
定义 Converter 接口
在 Nebula 项目中,每个模块的 Convert 接口放在 convert/ 目录下:
package com.huida.nebula.system.core.user.convert;
import com.huida.nebula.core.convert.BaseMapperConfig;
import com.huida.nebula.system.core.user.entity.SysUserDO;
import com.huida.nebula.system.core.user.vo.param.UserSaveParamVO;
import com.huida.nebula.system.core.user.vo.resp.UserDetailVO;
import com.huida.nebula.system.core.user.vo.resp.UserRespVO;
import com.huida.nebula.system.dto.user.CurrentUserVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;
// config = BaseMapperConfig.class 是 Nebula 框架统一配置(nullValuePropertyMappingStrategy = IGNORE 等)
@Mapper(config = BaseMapperConfig.class)
public interface SysUserConvert {
// 通过 Mappers.getMapper() 获取单例(无 Spring 容器时也可用)
SysUserConvert INSTANCE = Mappers.getMapper(SysUserConvert.class);
/** 实体 → 列表响应 VO */
UserRespVO doToRespVO(SysUserDO userDO);
/** 实体 → 详情响应 VO */
UserDetailVO doToDetailVO(SysUserDO userDO);
/** 新增参数 → 实体 */
SysUserDO saveParamToDo(UserSaveParamVO saveParam);
/**
* 修改参数合并到现有实体(@MappingTarget:覆盖已有对象而非新建)
* 框架配置了 IGNORE 策略,saveParam 中为 null 的字段不会覆盖 DO
*/
void saveParamMergeToDo(UserSaveParamVO saveParam, @MappingTarget SysUserDO userDO);
}在 Service 中调用
// 新增场景
SysUserDO userDO = SysUserConvert.INSTANCE.saveParamToDo(saveParam);
sysUserMapper.insert(userDO);
// 修改场景(先查后改,避免覆盖无关字段)
SysUserDO userDO = sysUserMapper.selectById(saveParam.getId());
SysUserConvert.INSTANCE.saveParamMergeToDo(saveParam, userDO);
sysUserMapper.updateById(userDO);
// 查询结果转换
SysUserDO userDO = sysUserMapper.selectById(id);
return SysUserConvert.INSTANCE.doToDetailVO(userDO);
// 批量转换(list → list)
List<UserRespVO> voList = userDOList.stream()
.map(SysUserConvert.INSTANCE::doToRespVO)
.collect(Collectors.toList());字段名不一致时的映射
使用 @Mapping 注解处理源字段名与目标字段名不一致的情况:
@Mapper(config = BaseMapperConfig.class)
public interface SysUserConvert {
// source 是来源字段,target 是目标字段
@Mapping(target = "userId", source = "id")
@Mapping(target = "displayName", source = "username")
CurrentUserVO doToCurrentVO(SysUserDO userDO);
// 忽略某个字段(不做映射)
@Mapping(target = "password", ignore = true)
UserRespVO doToRespVO(SysUserDO userDO);
}多源合并映射
将多个对象的字段合并到一个目标对象:
@Mapper(config = BaseMapperConfig.class)
public interface OrderConvert {
// 同时接收两个来源对象
@Mapping(target = "orderId", source = "order.id")
@Mapping(target = "username", source = "user.username")
@Mapping(target = "productName", source = "product.name")
OrderDetailVO toDetailVO(OrderDO order, SysUserDO user, ProductDO product);
}自定义转换逻辑
当字段类型差异较大时,使用 @Named 定义转换方法:
@Mapper(config = BaseMapperConfig.class)
public interface RoleConvert {
@Mapping(target = "statusText", source = "status", qualifiedByName = "statusToText")
RoleRespVO doToRespVO(SysRoleDO roleDO);
// 自定义转换方法:整数状态码 → 文本描述
@Named("statusToText")
default String statusToText(Integer status) {
if (status == null) return "未知";
return status == 0 ? "正常" : "禁用";
}
}BaseMapperConfig 说明
Nebula 框架在 nebula-boot-core 中定义了统一的 MapStruct 配置:
// 框架内部代码,业务侧通过 config = BaseMapperConfig.class 继承
@MapperConfig(
// null 值字段不覆盖目标对象已有值(对 @MappingTarget 场景很重要)
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
// 未映射的目标字段默认忽略(不报错)
unmappedTargetPolicy = ReportingPolicy.IGNORE
)
public interface BaseMapperConfig {}关键点:
IGNORE策略:修改场景下只更新有值的字段,null字段不会清空数据库中的原有值unmappedTargetPolicy = IGNORE:目标 VO 中有 DO 没有的计算字段时不会报编译错误,在 Service 中单独赋值即可
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
编译报错:No property named "xxx" | 字段名拼写错误或 getter 不存在 | 检查 @Mapping(source=...) 字段名 |
| Lombok + MapStruct 生成代码为空 | annotationProcessor 顺序错误 | 确保 Lombok 在 MapStruct 之前,并加 lombok-mapstruct-binding |
| 修改时 null 覆盖了数据库值 | BaseMapperConfig 未配置 IGNORE | 检查 @Mapper(config = BaseMapperConfig.class) 是否配置 |
| 自定义转换方法未生效 | @Named 名称不匹配 | qualifiedByName 与 @Named 值必须完全一致 |
