🧭 MapStruct 教程笔记
—— 高效、安全的 Java Bean 映射工具
一、为什么选择 MapStruct?
- 性能优势
MapStruct 在编译时生成纯 Java 方法调用的映射代码,避免了反射带来的性能损耗。例如,在处理大量对象映射时,MapStruct 的性能显著优于使用反射的工具,如 Spring 的BeanUtils
。 - 类型安全
编译时检查映射字段的类型和名称,避免运行时错误。例如,字段名或类型不匹配时,编译阶段会报错。 - 灵活性
支持复杂映射逻辑,如多源映射、嵌套映射、自定义类型转换等,覆盖大多数业务场景。
二、基础配置
1. Maven 依赖配置
在 pom.xml
中添加以下依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.bingbaihanji</groupId>
<artifactId>MapStruct</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<org.mapstruct.version>1.6.3</org.mapstruct.version>
</properties>
<dependencies>
<!-- MapStruct 核心库 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<!-- MapStruct 注解处理器 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
<scope>provided</scope>
</dependency>
<!-- Lombok(可选) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
</dependency>
<!-- Lombok 与 MapStruct 集成(可选) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven 编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
</path>
<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>
</project>
注意:若使用 Lombok,需确保 mapstruct-processor
在 Lombok 之后声明。
2. 与 Spring 集成
在 @Mapper
注解中指定 componentModel = "spring"
,生成的映射器将自动注册为 Spring Bean:
@Mapper(componentModel = "spring")
public interface UserMapper {
UserDto toDto(User user);
}
三、核心用法
1. 基础映射
同名字段自动映射:若源对象与目标对象字段名相同,无需额外配置。
定义两个实体类
package com.bingbaihanji.entity;
import lombok.Data;
@Data
public class SimpleSource {
private String name;
private String description;
}
//--------------------------------------------------------------------//
package com.bingbaihanji.entity;
import lombok.Data;
@Data
public class SimpleDestination {
private String name;
private String description;
}
写mapper接口
package com.bingbaihanji.structmapper;
import com.bingbaihanji.entity.SimpleDestination;
import com.bingbaihanji.entity.SimpleSource;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface SimpleSourceDestinationMapper {
// 使用 Mappers.getMapper(SimpleSourceDestinationMapper.class) 获取实例
SimpleSourceDestinationMapper INSTANCE = Mappers.getMapper(SimpleSourceDestinationMapper.class);
SimpleDestination sourceToDestination(SimpleSource source);
SimpleSource destinationToSource(SimpleDestination destination);
}
然后执行 mvn clean install 触发 MapStruct 插件自动生成代码,生成后的实现类在 /target/generated-sources/annotations/
目录下
package com.bingbaihanji.structmapper;
import com.bingbaihanji.entity.SimpleDestination;
import com.bingbaihanji.entity.SimpleSource;
import javax.annotation.processing.Generated;
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2025-05-21T12:00:31+0800",
comments = "version: 1.6.3, compiler: javac, environment: Java 21.0.7 (Eclipse Adoptium)"
)
public class SimpleSourceDestinationMapperImpl implements SimpleSourceDestinationMapper {
@Override
public SimpleDestination sourceToDestination(SimpleSource source) {
if ( source == null ) {
return null;
}
SimpleDestination simpleDestination = new SimpleDestination();
simpleDestination.setName( source.getName() );
simpleDestination.setDescription( source.getDescription() );
return simpleDestination;
}
@Override
public SimpleSource destinationToSource(SimpleDestination destination) {
if ( destination == null ) {
return null;
}
SimpleSource simpleSource = new SimpleSource();
simpleSource.setName( destination.getName() );
simpleSource.setDescription( destination.getDescription() );
return simpleSource;
}
}
测试用例
package com.bingbaihanji;
import com.bingbaihanji.entity.SimpleDestination;
import com.bingbaihanji.entity.SimpleSource;
import com.bingbaihanji.structmapper.SimpleSourceDestinationMapper;
public class Main {
public static void main(String[] args) {
// 创建源对象并赋值
SimpleSource simpleSource = new SimpleSource();
simpleSource.setName("SourceName");
simpleSource.setDescription("SourceDescription");
// 使用SimpleSourceDestinationMapper将源对象转换为目标对象
SimpleDestination simpleDestination = SimpleSourceDestinationMapper.INSTANCE.sourceToDestination(simpleSource);
System.out.println(simpleSource);
System.out.println("----------------------");
System.out.println(simpleDestination);
}
}
输出结果:
SimpleSource(name=SourceName, description=SourceDescription)
----------------------
SimpleDestination(name=SourceName, description=SourceDescription)
字段名不同:使用 @Mapping
注解指定映射关系。
Studnet类的age和name与StudentVO类的ageVO和nameVO对应不上时
package com.bingbaihanji.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author bingb
* @date 2025-05-21 12:05:56
* @description //TODO
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private String name;
private Integer age;
}
//==========================================================//
package com.bingbaihanji.entity.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class StudentVO {
// 换名字
private String nameVO;
// 将age换成String类型
private String age;
}
mappper
package com.bingbaihanji.structmapper;
import com.bingbaihanji.entity.Student;
import com.bingbaihanji.entity.vo.StudentVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
@Mapper
public interface StudentMapper {
StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
// 使用 @Mappings 注解进行更复杂的映射
// 在Mapper类中加入@Mapping的注解指定原对象的字段名和要被对应上的字段名。
// 其中@Mappings表示多个字段需要对应,如果只是一个可以使用@Mappin
@Mappings({
@Mapping(source = "name", target = "nameVO"),
@Mapping(source = "age", target = "age")
})
StudentVO studentToStudentVO(Student student);
}
测试
package com.bingbaihanji;
import com.bingbaihanji.entity.Student;
import com.bingbaihanji.entity.vo.StudentVO;
import com.bingbaihanji.structmapper.StudentMapper;
public class Main {
public static void main(String[] args) {
Student student = new Student("张三", 14);
StudentVO studentVO = StudentMapper.INSTANCE.studentToStudentVO(student);
System.out.println(student);
System.out.println("----------------------");
System.out.println(studentVO);
}
}
结果
Student(name=张三, age=14)
----------------------
StudentVO(nameVO=张三, age=14)
2. 多源映射
支持从多个源对象合并字段到目标对象:
某些时候,我们的源不是一个,例如从数据库中查询出来了学生和老师,我们需要将老师的名字给VO的name字段,学生的年龄给VO的age字段时可以使用多参数源的映射方式。
package com.bingbaihanji.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author bingb
* @date 2025-05-21 14:09:04
* @description //TODO
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Teacher {
private String teacherName;
private String teacherAge;
}
mapper
package com.bingbaihanji.structmapper;
import com.bingbaihanji.entity.Student;
import com.bingbaihanji.entity.Teacher;
import com.bingbaihanji.entity.vo.StudentVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
@Mapper
public interface StudentMapper {
StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
// 老师的名字给VO的name字段,学生的年龄给VO的age字段
@Mappings({
@Mapping(source = "teacher.teacherName", target = "nameVO"),
@Mapping(source = "student.age", target = "age")
})
StudentVO studentToStudentVO(Student student, Teacher teacher);
}
}
测试:
package com.bingbaihanji;
import com.bingbaihanji.entity.Student;
import com.bingbaihanji.entity.Teacher;
import com.bingbaihanji.entity.vo.StudentVO;
import com.bingbaihanji.structmapper.StudentMapper;
public class Main {
public static void main(String[] args) {
Student student = new Student("猴子", 14);
Teacher teacher = new Teacher("菩提祖师", 18);
StudentVO studentVO = StudentMapper.INSTANCE.studentToStudentVO(student, teacher);
System.out.println(student);
System.out.println(teacher);
System.out.println("----------------------");
System.out.println(studentVO);
}
}
输出结果:
Student(name=猴子, age=14)
Teacher(teacherName=菩提祖师, teacherAge=18)
----------------------
StudentVO(nameVO=菩提祖师, age=14)
3. 嵌套映射
通过 .
操作符访问嵌套对象的字段:
实体类:
package com.bingbaihanji.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author bingb
* @date 2025-05-21 14:09:04
* @description //TODO
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Teacher {
private String teacherName;
private Integer teacherAge;
// 爱好
private Hobby hobby;
}
//=======================================//
package com.bingbaihanji.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author bingb
* @date 2025-05-21 14:19:59
* @description //TODO
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Hobby {
private String playingBasketball;
private String readingBooks;
private String swimming;
}
// ========================================//
package com.bingbaihanji.entity.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class StudentVO {
// 换名字
private String nameVO;
// 将age换成String类型
private String age;
// 爱好
private String hobby;
}
mapper
package com.bingbaihanji.structmapper;
import com.bingbaihanji.entity.Student;
import com.bingbaihanji.entity.Teacher;
import com.bingbaihanji.entity.vo.StudentVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
@Mapper
public interface StudentMapper {
StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
@Mappings({
@Mapping(source = "teacher.hobby.swimming", target = "hobby"),
@Mapping(source = "teacher.teacherAge", target = "nameVO"),
@Mapping(source = "student.age", target = "age")
})
StudentVO studentToStudentVO(Student student, Teacher teacher);
}
测试以及结果
package com.bingbaihanji;
import com.bingbaihanji.entity.Hobby;
import com.bingbaihanji.entity.Student;
import com.bingbaihanji.entity.Teacher;
import com.bingbaihanji.entity.vo.StudentVO;
import com.bingbaihanji.structmapper.StudentMapper;
public class Main {
public static void main(String[] args) {
Student student = new Student("猴子", 14);
Teacher teacher = new Teacher("菩提祖师", 18, new Hobby("篮球", "看书", "游泳"));
StudentVO studentVO = StudentMapper.INSTANCE.studentToStudentVO(student, teacher);
System.out.println(student);
System.out.println(teacher);
System.out.println("----------------------");
System.out.println(studentVO);
}
}
结果
Student(name=猴子, age=14)
Teacher(teacherName=菩提祖师, teacherAge=18, hobby=Hobby(playingBasketball=篮球, readingBooks=看书, swimming=游泳))
----------------------
StudentVO(nameVO=18, age=14, hobby=游泳)
4. 集合映射
自动映射集合类型(如 List
、Set
):
List<CarDto> carsToCarDtos(List<Car> cars);
四、高级功能
1. 数据类型转换
-
隐式转换:自动处理基本类型与包装类、
String
与Date
等常见类型的转换。 -
显式格式化:使用
dateFormat
或numberFormat
指定格式。@Mapping(target = "createTime", source = "createDate", dateFormat = "yyyy-MM-dd") UserDto userToUserDto(User user);
2. 自定义映射方法
通过 expression
或 @AfterMapping
注入自定义逻辑:
@Mapping(target = "fullName", expression = "java(user.getFirstName() + ' ' + user.getLastName())")
UserDto userToUserDto(User user);
@AfterMapping
default void fillExtInfo(User user, @MappingTarget UserDto dto) {
dto.setExtInfo(calculateExtInfo(user));
}
3. 逆向映射
使用 @InheritInverseConfiguration
自动生成反向映射:
@Mapping(target = "name", source = "userName")
UserDto userToUserDto(User user);
@InheritInverseConfiguration
User userDtoToUser(UserDto dto);
4. 更新现有对象
通过 @MappingTarget
更新目标对象而非创建新实例:
void updateUser(User user, @MappingTarget UserDto dto);
5. 使用抽象类自定义映射器
一些特殊场景 @Mapping 无法满足时,需要定制化开发,同时希望保留MapStruct自动生成代码的能力。
1 定义实体类
public class Transaction {
private Long id;
private String uuid = UUID.randomUUID().toString();
private BigDecimal total;
//standard getters
}
DTO类:
public class TransactionDTO {
private String uuid;
private Long totalInCents;
// standard getters and setters
}
将 BigDecimal
类型的total美元金额,转换为 Long totalInCents
(以美分表示的总金额),这部分我们将使用自定义方法实现。
2. Mapper 定义
@Mapper
abstract class TransactionMapper {
public TransactionDTO toTransactionDTO(Transaction transaction) {
TransactionDTO transactionDTO = new TransactionDTO();
transactionDTO.setUuid(transaction.getUuid());
transactionDTO.setTotalInCents(transaction.getTotal()
.multiply(new BigDecimal("100")).longValue());
return transactionDTO;
}
public abstract List<TransactionDTO> toTransactionDTO(
Collection<Transaction> transactions);
}
6 @BeforeMapping 和 @AfterMapping 注解
可以通过 @BeforeMapping 和 @AfterMapping 注解定制化需求
@MappingTarget 是一个参数注释,在@BeforeMapping的情况下,在执行映射逻辑之前填充目标映射DTO,在@AfterMapping注释方法的情况下在执行之后填充。
@Mapper
public abstract class CarsMapper {
@BeforeMapping
protected void enrichDTOWithFuelType(Car car, @MappingTarget CarDTO carDto) {
if (car instanceof ElectricCar) {
carDto.setFuelType(FuelType.ELECTRIC);
}
if (car instanceof BioDieselCar) {
carDto.setFuelType(FuelType.BIO_DIESEL);
}
}
@AfterMapping
protected void convertNameToUpperCase(@MappingTarget CarDTO carDto) {
carDto.setName(carDto.getName().toUpperCase());
}
public abstract CarDTO toCarDto(Car car);
}
五、整合spring框架
前面实例通过 Mappers.getMapper(YourClass.class) 获取 MapStruct Mapper实例。
对于Spring 应用,修改@Mapper注解,设置componentModel属性值为spring,如果是 CDI,则将其值设置为 cdi。
注入到spring容器
@Mapper(componentModel = "spring")
public interface SimpleSourceDestinationMapper
注入 Spring 组件到 Mapper 中
我们需要在mapper中引用Spring容器中的组件,需要改用抽象类而非接口了:
@Mapper(componentModel = "spring")
public abstract class SimpleDestinationMapperUsingInjectedService
然后添加 @Autowired 注入依赖:
@Mapper(componentModel = "spring")
public abstract class SimpleDestinationMapperUsingInjectedService {
@Autowired
protected SimpleService simpleService;
@Mapping(target = "name", expression = "java(simpleService.enrichName(source.getName()))")
public abstract SimpleDestination sourceToDestination(SimpleSource source);
}
切记不要将注入的 bean 设置为private ,因为 MapStruct 需要在生成的实现类中访问该对象
六、常见问题与优化
1. Lombok 兼容性
Maven 配置中需确保 Lombok 处理器在 MapStruct 之前。
2. 枚举映射
使用 @ValueMapping
处理枚举值不一致的情况:
@ValueMapping(source = "MALE", target = "MAN")
GenderDto genderToGenderDto(Gender gender);
3. 空值处理
配置全局策略忽略未映射字段或空值:
@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface UserMapper { ... }
六、总结
MapStruct 通过编译时代码生成实现了高效、安全的 Bean 映射,适用于复杂业务场景。其核心优势在于:
- 性能:基于直接方法调用,无反射开销。
- 可维护性:编译时错误检查减少运行时问题。
- 灵活性:支持自定义逻辑、多源映射等高级功能。
推荐场景:DTO 与 Entity 转换、微服务间模型适配、前后端数据封装等。