在 Spring Boot 项目开发中,对象映射是高频场景,比如 DTO 与实体类、VO 与 DTO 之间的转换。传统映射方式存在诸多痛点,而 MapStruct 作为一款编译期对象映射工具, 能完美解决这些问题。本文将逐步讲解 MapStruct 与 Spring Boot 的整合及全方位使用。
一、为什么需要 MapStruct?
在日常开发中,我们经常需要在不同层的对象之间进行属性拷贝,比如将数据库查询得到的实体类(Entity)转换为对外提供接口的DTO(Data Transfer Object)。传统方式主要有以下几种,均存在明显问题。
1.1 手动编写 set/get 方法
手动映射属性逻辑繁琐,代码冗余,当对象属性较多或属性名发生变化时,维护成本极高,且易出现漏写、错写的情况。
示例代码
// 实体类
@Data
public class UserEntity {
private Long id;
private String username;
private String password;
private Integer age;
private LocalDateTime createTime;
}
// DTO类
@Data
public class UserDto {
private Long id;
private String userName; // 与实体类属性名不一致(username vs userName)
private Integer age;
private String createTimeStr; // 类型不一致(LocalDateTime vs String)
}
// 手动映射工具类
public class UserConvertor {
public static UserDto entityToDto(UserEntity entity) {
if (entity == null) {
return null;
}
UserDto dto = new UserDto();
dto.setId(entity.getId());
// 属性名不一致,需手动对应
dto.setUserName(entity.getUsername());
dto.setAge(entity.getAge());
// 类型转换,需手动处理格式
dto.setCreateTimeStr(entity.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
return dto;
}
}
1.2 使用 BeanUtils 等反射工具
Spring 的 BeanUtils、Apache 的 BeanUtils 等工具基于反射实现自动映射,虽减少了代码量,但存在以下问题:
- 反射性能较差,尤其在循环映射大量对象时,性能损耗明显;
- 无法处理属性名不一致、类型不匹配的场景,需额外手动补充;
- 不支持复杂映射逻辑(如表达式、自定义转换);
- 编译期无法校验映射正确性,运行时才可能暴露问题。
二、MapStruct 如何解决问题?
MapStruct 是一款基于编译期生成代码的对象映射工具,核心原理是在编译时根据注解生成对应的映射实现类,底层采用原生 set/get 方法,而非反射,兼具性能与灵活性。 其优势如下:
- 编译期生成代码,性能与手动编写一致;
- 支持属性名映射、类型自动转换,支持自定义转换逻辑;
- 编译期校验映射合法性,提前暴露问题;
- 注解驱动,配置简洁,易维护;
- 支持复杂场景(多源映射、表达式、集合映射等)。
示例代码(MapStruct 解决方案)
// 1. 引入依赖后,定义映射接口
@Mapper
public interface UserMapper {
// 单例实例(非Spring环境使用)
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
// 映射方法,通过注解配置特殊规则
@Mapping(target = "userName", source = "username") // 解决属性名不一致
@Mapping(target = "createTimeStr", source = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss") // 日期类型转换
UserDto entityToDto(UserEntity entity);
}
编译后,MapStruct 会自动生成 UserMapperImpl 实现类,核心代码如下(无需手动编写):
@Component
public class UserMapperImpl implements UserMapper {
@Override
public UserDto entityToDto(UserEntity entity) {
if (entity == null) {
return null;
}
UserDto userDto = new UserDto();
userDto.setId(entity.getId());
userDto.setUserName(entity.getUsername()); // 对应@Mapping配置
userDto.setAge(entity.getAge());
if (entity.getCreateTime() != null) {
// 对应dateFormat配置,自动格式化日期
userDto.setCreateTimeStr(entity.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
return userDto;
}
}
在 Spring 环境中,直接注入使用即可:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public UserDto getUserById(Long id) {
UserEntity entity = userRepository.findById(id).orElseThrow();
return userMapper.entityToDto(entity); // 直接调用映射方法
}
}
三、MapStruct 安装配置
3.1 依赖配置(Maven)
-Amapstruct.defaultComponentModel=spring 这个很重要,目的是让生成类注入spring容器
<properties>
<org.mapstruct.version>1.6.3</org.mapstruct.version>
</properties>
<!-- MapStruct 核心依赖 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.6.3</version> <!-- 建议使用最新稳定版 -->
</dependency><!-- 注解处理器,编译期生成代码 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.6.3</version>
<scope>provided</scope>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>
<!-- 注入spring容器 -->
-Amapstruct.defaultComponentModel=spring
</arg>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
...
3.2 插件安装(IDE)
IntelliJ IDEA
- 打开
File > Settings > Plugins; - 搜索
MapStruct Support,点击安装并重启 IDE; - 确保注解处理器启用:
File > Settings > Build, Execution, Deployment > Compiler > Annotation Processors,勾选Enable annotation processing。
Eclipse
- 打开
Help > Eclipse Marketplace; - 搜索
MapStruct Eclipse Plugin,点击安装并重启 IDE; - 配置注解处理器:右键项目 >
Properties > Java Compiler > Annotation Processing,勾选Enable annotation processing; - 在
Annotation Processing > Factory Path中,添加mapstruct-processor-${version}.jar依赖。
四、MapStruct 详细使用
4.1 @Mapping 详细使用
@Mapping 是 MapStruct 核心注解,用于配置单个属性的映射规则,支持多种复杂场景。
4.1.1 基础属性映射
@Mapper
public interface UserMapper {
// 属性名不一致
@Mapping(target = "userName", source = "username")
// 日期格式化
@Mapping(target = "createTimeStr", source = "createTime", dateFormat = "yyyy-MM-dd")
// 数字格式化
@Mapping(target = "priceStr", source = "price", numberFormat = "#,##0.00")
//嵌套源bean映射,可使用“.”作为目标。这将指示MapStruct将源bean的所有属性映射到目标对象
@Mapping( target = ".", source = "account" )
UserDto entityToDto(UserEntity entity);
}
4.1.2 添加自定义方法
当自动映射无法满足需求时,可在映射接口中定义默认方法或抽象类方法,实现自定义映射逻辑。当参数类型与返回类型匹配时,自动使用该方法
// 方式1:接口默认方法(Java 8+)
@Mapper
public interface UserMapper {
@Mapping(target = "fullName", source = "username")
UserDto entityToDto(UserEntity entity);
// 自定义方法,处理复杂逻辑
default String encryptPassword(String password) {
// 模拟密码加密逻辑
return DigestUtils.md5DigestAsHex(password.getBytes(StandardCharsets.UTF_8));
}
}
// 方式2:抽象类(支持定义字段和复杂逻辑)
@Mapper
public abstract class UserAbstractMapper {
// 可定义字段
@Autowired
private PasswordEncoder passwordEncoder;
@Mapping(target = "fullName", source = "username")
@Mapping(target = "password", expression = "java(encryptPassword(entity.getPassword()))")
public abstract UserDto entityToDto(UserEntity entity);
// 自定义映射方法,可依赖Spring组件
protected String encryptPassword(String password) {
return passwordEncoder.encode(password);
}
}
4.1.3 多源参数映射
支持将多个源对象的属性映射到同一个目标对象,适用于组合数据场景。
// 源对象1
@Data
public class UserEntity {
private Long id;
private String username;
}
// 源对象2
@Data
public class UserExtEntity {
private Integer age;
private String email;
}
// 目标DTO
@Data
public class UserFullDto {
private Long id;
private String username;
private Integer age;
private String email;
}
// 映射接口
@Mapper
public interface UserFullMapper {
@Mapping(target = "id", source = "user.id")
@Mapping(target = "username", source = "user.username")
@Mapping(target = "age", source = "userExt.age")
@Mapping(target = "email", source = "userExt.email")
UserFullDto combineToDto(UserEntity user, UserExtEntity userExt);
}
4.1.4 直接引用源参数
支持将源参数本身(非属性)直接映射到目标属性,适用于简单参数映射场景。
@Mapper
public interface UserMapper {
// 将参数hn(Integer类型)直接映射到target的houseNumber属性
@Mapping(target = "houseNumber", source = "hn")
UserAddressDto buildAddressDto(UserEntity user, Integer hn);
}
4.1.5 Map 转 Bean
支持将 Map<String, ?> 映射到实体类,通过键名匹配属性名。
@Mapper
public interface MapToBeanMapper {
@Mapping(target = "username", source = "userName") // Map键名是userName,目标属性是username
UserEntity mapToEntity(Map<String, Object> map);
}
4.1.6 构造函数映射
MapStruct 支持通过构造函数实例化目标对象,优先选择单参数构造函数,或通过@Default 注解指定构造函数。
// 目标对象(无无参构造,有带参构造)
@Data
public class UserDto {
private Long id;
private String username;
// 带参构造
public UserDto(Long id, String username) {
this.id = id;
this.username = username;
}
}
// 映射接口
@Mapper
public interface UserMapper {
// 自动匹配构造函数参数
UserDto entityToDto(UserEntity entity);
}
4.1.7 @Mapping.expression 使用
支持通过 Java 表达式实现复杂映射逻辑,适用于动态计算属性值场景。
@Mapper(imports = {StringUtils.class}) // 导入需要的工具类
public interface UserMapper {
@Mapping(target = "fullName", expression = "java(user.getFirstName() + \" \" + user.getLastName())")
@Mapping(target = "isAdult", expression = "java(user.getAge() >= 18)")
@Mapping(target = "email", expression = "java(StringUtils.lowerCase(user.getEmail()))")
UserDto entityToDto(UserEntity user);
}
4.2 @Mapper.uses 使用
@Mapper(uses = ...) 用于引入外部映射器或自定义转换类,实现映射逻辑复用。
// 自定义日期转换类
public class DateConvertor {
//参数类型和返回类型匹配的字段都会调用该方法
public String localDateTimeToString(LocalDateTime dateTime) {
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
}
// 映射接口,引入外部转换类
@Mapper(uses = {DateConvertor.class})
public interface UserMapper {
// 自动使用DateConvertor中的方法进行类型转换
UserDto entityToDto(UserEntity entity);
}
4.3 @Context 使用
@Context 用于传递上下文信息(如用户信息、配置参数),上下文参数会贯穿整个映射过程,支持在自定义方法、生命周期方法中使用。
// 上下文类
@Data
public class MappingContext {
private Long operatorId; // 操作人ID
private LocalDateTime operateTime; // 操作时间
}
// 映射接口
@Mapper
public interface UserMapper {
// 上下文参数添加@Context注解
@Mapping(target = "operatorId", source = "context.operatorId")
@Mapping(target = "operateTime", source = "context.operateTime")
UserDto entityToDto(UserEntity entity, @Context MappingContext context);
// 自定义方法中使用上下文
default String buildRemark(Sring remark,@Context MappingContext context) {
return "操作人:" + context.getOperatorId() + ",操作时间:" + context.getOperateTime() + "," + remark;
}
}
4.4 @Named 使用
@Named 用于给映射方法标记别名,当存在多个同类型映射方法时,通过别名指定使用哪个方法,解决歧义。
@Mapper
public interface UserMapper {
// 标记别名
@Named("encryptPassword")
default String encryptPassword(String password) {
return DigestUtils.md5DigestAsHex(password.getBytes());
}
@Named("plainPassword")
default String plainPassword(String password) {
return password;
}
// 通过qualifiedByName指定使用的方法
@Mapping(target = "password", source = "password", qualifiedByName = "encryptPassword")
UserDto entityToDtoWithEncrypt(UserEntity entity);
@Mapping(target = "password", source = "password", qualifiedByName = "plainPassword")
UserDto entityToDtoWithPlain(UserEntity entity);
}
//使用单独映射器类
@Named("TitleTranslator")
public class Titles {
@Named("EnglishToGerman")
public String translateTitleEG(String title) {
// some mapping logic
}
@Named("GermanToEnglish")
public String translateTitleGE(String title) {
// some mapping logic
}
}
@Mapper( uses = Titles.class )
public interface MovieMapper {
@Mapping( target = "title", qualifiedByName = { "TitleTranslator", "EnglishToGerman" } )
GermanRelease toGerman( OriginalRelease movies );
}
4.5 @MapMapping 使用
@MapMapping 用于配置 Map 类型的映射规则,支持键/值的格式化、类型转换。
@Mapper
public interface MapMapper {
// 配置值的日期格式化
@MapMapping(valueDateFormat = "yyyy-MM-dd")
Map<String, String> longDateMapToStringMap(Map<Long, LocalDate> sourceMap);
}
4.6 使用条件映射
- @Condition 用于定义条件映射规则,只有某个属性满足条件时才进行属性映射,支持自定义条件逻辑。
- @SourceParameterCondition:参数级别的条件过滤,能精准控制哪些参数参与映射过程。(比如多源映射,过滤哪些源参与映射)
@Mapper
public interface UserMapper {
// 自定义条件:字符串非空且非空白才映射
@Condition
default boolean isNonBlank(String value) {
return StringUtils.isNotBlank(value);
}
// 只有满足isNonBlank条件时,才映射username属性
@Mapping(target = "username", source = "username")
UserDto entityToDto(UserEntity entity);
//entity有效时才映射,否则返回null
@SourceParameterCondition
default boolean hasEntity(UserEntity entity) {
return entity != null && entity.getId() != null;
}
}
4.7 更新对象
某些情况下不想生成目标类型新实例、而是更新现有实例的映射。此类映射可通过为目标对象添加参数并标注@MappingTarget实现
@Mapperpublic interface CarMapper {
//使用给定 CarDto 对象的属性更新传入的 Car 实例
void updateCarFromDto(CarDto carDto, @MappingTarget Car car);
// 可以返回目标实例
Car updateCarFromDtoAndReturn(CarDto carDto, @MappingTarget Car car);
}
五、常用注解说明
| 注解 | 作用 | 关键属性 |
|---|---|---|
| @Mapper | 标记接口/抽象类为映射器,编译期生成实现类 | componentModel(组件模型,如spring)、uses(引入外部映射器)、imports(导入类) |
| @Mapping | 配置单个属性的映射规则 | target(目标属性)、source(源属性)、dateFormat(日期格式)、numberFormat(数字格式)、expression(表达式)、ignore(是否忽略) |
| @IterableMapping | 配置集合类型的映射规则 | elementTargetType(元素目标类型)、dateFormat、numberFormat |
| @Context | 标记上下文参数,用于传递映射过程中的附加信息 | |
| @Named | 给映射方法标记别名,解决多方法歧义 | value(别名) |
| @MapMapping | 配置Map类型的映射规则 | keyDateFormat、valueDateFormat、keyTargetType、valueTargetType |
| @ValueMapping | 配置枚举值的映射规则 | source(源枚举值)、target(目标枚举值) |
| @EnumMapping | 配置枚举类型的整体映射策略 | nameTransformationStrategy(名称转换策略)、mappingStrategy(映射策略) |
| @InheritConfiguration | 继承其他映射方法的配置 | name(要继承的方法名) |
| @InheritInverseConfiguration | 继承反向映射方法的配置(如DTO转Entity继承Entity转DTO的规则) | name(要继承的反向方法名) |
| @SubclassMapping | 配置子类的映射规则,支持多态映射 | source(源子类)、target(目标子类) |
| @BeanMapping | 配置Bean级别的映射规则 | resultType(结果类型)、nullValueMappingStrategy(空值映射策略) |
| @SourcePropertyName | 在条件方法(@Condition)中获取源属性名 | |
| @TargetPropertyName | 在条件方法(@Condition)中获取目标属性名 | |
| @SourceParameterCondition | 标记源参数的条件方法,控制是否映射该参数 | |
| @MappingTarget | 标记目标对象,用于更新现有对象(而非创建新对象) | |
| @MapperConfig | 定义共享映射配置,供多个@Mapper继承 | 与@Mapper属性一致,支持全局配置 |
| @DecoratedWith | 给映射器添加装饰器,自定义映射逻辑 | value(装饰器类) |
| @BeforeMapping | 标记映射前执行的生命周期方法 | |
| @AfterMapping | 标记映射后执行的生命周期方法 |
六、MapStruct SPI 简单介绍
MapStruct 提供 SPI(Service Provider Interface)扩展机制,允许开发者自定义映射行为,适配特殊场景。以下是常用 SPI 及说明:
6.1 org.mapstruct.ap.spi.AccessorNamingStrategy
自定义属性访问器命名策略,适用于非标准 JavaBean 命名规则的场景(如 Lombok 的 fluent 风格)。
使用示例(适配 Fluent 风格)
// 自定义访问器策略
public class FluentAccessorNamingStrategy implements AccessorNamingStrategy {
@Override
public boolean isGetter(ExecutableElement method) {
// Fluent风格getter无前缀,返回当前对象类型
return !method.getParameters().isEmpty() == false
&& method.getReturnType().equals(method.getEnclosingElement().asType());
}
@Override
public String getPropertyName(ExecutableElement getterOrSetterMethod) {
// 直接使用方法名作为属性名
return getterOrSetterMethod.getSimpleName().toString();
}
// 其他方法实现...
}
// 配置SPI:在META-INF/services下创建文件org.mapstruct.ap.spi.AccessorNamingStrategy
// 文件内容:com.example.mapstruct.spi.FluentAccessorNamingStrategy
6.2 org.mapstruct.ap.spi.MappingExclusionProvider
自定义排除映射的属性,用于过滤不需要映射的属性(如特定前缀、注解标记的属性)。
6.3 org.mapstruct.ap.spi.BuilderProvider
自定义构建器(Builder)探测策略,支持非标准 Builder 模式的对象映射(如自定义 Builder 方法名)。
6.4 org.mapstruct.ap.spi.EnumMappingStrategy
自定义枚举映射策略,控制枚举值的映射规则(如按名称、按序数)。
6.5 org.mapstruct.ap.spi.EnumTransformationStrategy
自定义枚举名称转换策略,支持枚举值名称的自动转换(如大小写、下划线转驼峰)。
6.6 org.mapstruct.ap.spi.AdditionalSupportedOptionsProvider
扩展 MapStruct 支持的自定义选项,允许添加自定义配置参数。
七、与 Lombok 协同
MapStruct 与 Lombok 协同使用时,需配置正确的注解处理器顺序,否则可能出现编译异常(如 Lombok 生成的 get/set 方法无法被 MapStruct 识别)。
7.1 依赖配置(Maven)
<properties>
<org.mapstruct.version>1.6.3</org.mapstruct.version>
<org.projectlombok.version>1.18.30</org.projectlombok.version>
</properties>
<dependencies>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
<scope>provided</scope>
</dependency><!-- MapStruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths><!-- 必须按此顺序配置:lombok → lombok-mapstruct-binding → mapstruct-processor -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</path>
<!-- Lombok 1.18.16+ 必须添加此绑定包 -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
7.2 注意事项
- 注解处理器顺序不可颠倒,必须先执行 Lombok 生成 get/set 方法,再执行 MapStruct 生成映射代码;
- Lombok 版本需与 mapstruct-processor 版本兼容,建议使用最新稳定版;
- 若使用 Lombok 的
@Builder注解,MapStruct 可自动探测 Builder 并生成对应映射代码。
总结
MapStruct 作为编译期对象映射工具,在 Spring Boot 项目中能高效解决对象转换问题,兼具性能、灵活性与可维护性。通过本文的配置与示例,可快速上手 MapStruct 并应用于实际开发,大幅减少重复代码,提升开发效率。