
官网地址:http://mapstruct.org/
MapStruct 对象转换用法
MapStruct 是一个编译时代码生成器,专门用于简化 Java Bean 之间的对象映射。在实际开发中,DAO 层实体和 DTO(Data Transfer Object)之间大部分字段相同、只有少数不同——手写 getter/setter 既繁琐又容易出错,而 MapStruct 只需通过注解约定就能自动生成高效、类型安全的映射代码。
它作为 Java 编译器插件工作(基于注解处理器),可在命令行或 IDE 中使用。
一、开发环境搭建(Maven)
MapStruct 由两部分组成:
| 坐标 |
作用 |
org.mapstruct:mapstruct-jdk8 |
提供 @Mapper、@Mapping 等必要注解。JDK ≥ 1.8 时推荐使用此坐标以利用 Java 8 特性 |
org.mapstruct:mapstruct-processor |
注解处理器,在编译时根据接口定义自动生成实现类 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <org.mapstruct.version>1.3.0.Final</org.mapstruct.version>
<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-jdk8</artifactId> <version>${org.mapstruct.version}</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> <scope>provided</scope> </dependency>
|
二、2 分钟入门
假设有两个类:源对象 Car 和目标对象 CarDto。两者大部分字段相同,但”座位数”字段名不同(numberOfSeats vs seatCount),且车型一个是枚举、一个是字符串。
源对象与目标对象
Car.java:
1 2 3 4 5 6 7 8 9 10
| public class Car { private String make; private int numberOfSeats; private CarType type; }
enum CarType { SEDAN }
|
CarDto.java:
1 2 3 4 5 6 7 8
| public class CarDto {
private String make; private int seatCount; private String type;
}
|
定义 Mapper 接口
1 2 3 4 5 6 7 8
| @Mapper public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
@Mapping(source = "numberOfSeats", target = "seatCount") CarDto carToCarDto(Car car); }
|
三个关键点:
@Mapper 注解标记该接口为映射入口,是编译时 MapStruct 处理器的识别标记
@Mapping 注解处理字段名不一致的情况;对于类型不同的字段(如枚举→字符串),MapStruct 会自动进行隐式转换
INSTANCE 成员变量按惯例声明,客户端可通过 CarMapper.INSTANCE.carToCarDto(car) 直接调用
componentModel 属性
@Mapper 的 componentModel 属性决定生成实现类的组件模式:
| 模式 |
说明 |
获取方式 |
default |
不使用任何组件模型 |
Mappers.getMapper(CarMapper.class) |
cdi |
生成 CDI 应用级 Bean |
@Inject |
spring |
自动添加 @Component |
Spring @Autowired 注入 |
jsr330 |
添加 @Named + @Singleton |
JSR-330 @Inject |
Spring 项目中最常用的写法:
1 2 3 4 5 6 7 8 9
| @Mapper(componentModel = "spring") public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
@Mapping(source = "numberOfSeats", target = "seatCount") CarDto carToCarDto(Car car); }
|
编译与查看生成结果
由于 MapStruct 在编译期生成代码(IDE 自动编译通常不触发),需手动执行:
编译后在 target 目录下会多出 CarMapperImpl.class —— 这就是自动生成的实现类。用 IDE 反编译功能可以查看其源码:字段名不同(seatCount ↔ numberOfSeats)的映射由 @Mapping 控制,枚举到字符串的类型转换由 MapStruct 默认机制完成。
三、多个属性的映射
支持将多个源参数组合映射到一个目标对象:
1 2 3
| @Mapping(source = "person.description", target = "description") @Mapping(source = "address.houseNo", target = "houseNumber") DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
|
四、更新现有 Bean 实例
场景:不创建新对象,而是把 DTO 的属性值更新到已有对象的同名字段上。使用 @MappingTarget 标注目标参数:
1
| void updateCarFromDto(CarDto carDto, @MappingTarget Car car);
|
五、反向配置继承
当已存在 A → B 的映射方法,现在需要 B → A 的反向映射时,无需重复编写 @Mapping 规则,直接继承即可:
1 2 3 4 5 6 7
| @Mapping(source = "make", target = "manufacturer") @Mapping(source = "numberOfSeats", target = "seatCount") CarDto carToCarDto(Car car);
@InheritInverseConfiguration(name = "carToCarDto") Car carDTOTocar(CarDto carDto);
|
六、隐式类型转换
MapStruct 内置了大量自动类型转换能力。以下是开箱即用的转换规则:
| 转换类别 |
示例 |
备注 |
| 基本类型 ↔ 包装类 |
int ↔ Integer, boolean ↔ Boolean |
双向自动转换 |
| 基本类型之间 |
int ↔ long, byte ↔ int |
大→小可能损失精度或值域 |
基本类型 / 包装类 ↔ String |
int ↔ String, boolean ↔ String |
分别调用 valueOf() / parseInt() 等 |
日期 ↔ String |
Date ↔ String |
通过 dateFormat 或 numberFormat 指定格式 |
BigDecimal / BigInteger ↔ String |
— |
同上 |
大类型转小类型(如 long → int)可能导致精度丢失。可通过 Mapper/MapperConfig 的 typeConversionPolicy 属性控制警告级别(默认 ReportingPolicy.IGNORE)。
6.1 数字格式化(int → String)
1 2 3 4 5 6 7 8 9
| @Mapper public interface CarMapper {
@Mapping(source = "price", numberFormat = "$#.00") CarDto carToCarDto(Car car);
@IterableMapping(numberFormat = "$#.00") List<String> prices(List<Integer> prices); }
|
6.2 科学计数法格式化(BigDecimal → String)
1 2 3 4 5 6
| @Mapper public interface CarMapper {
@Mapping(source = "power", numberFormat = "#.##E0") CarDto carToCarDto(Car car); }
|
6.3 日期格式化(Date → String)
1 2 3 4 5 6 7 8 9
| @Mapper public interface CarMapper {
@Mapping(source = "manufacturingDate", dateFormat = "dd.MM.yyyy") CarDto carToCarDto(Car car);
@IterableMapping(dateFormat = "dd.MM.yyyy") List<String> stringListToDateList(List<Date> dates); }
|
七、默认值与常量
通过 @Mapping 注解的两个属性灵活设置目标字段的默认行为:
| 属性 |
触发条件 |
行为 |
defaultValue |
source 字段值为 null 时生效 |
用指定值替代 null |
constant |
无条件生效,忽略 source |
始终注入固定常量值 |
1 2 3 4 5 6 7 8
| @Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined") @Mapping(target = "longProperty", source = "longProp", defaultValue = "-1") @Mapping(target = "stringConstant", constant = "Constant Value") @Mapping(target = "integerConstant", constant = "14") @Mapping(target = "longWrapperConstant", constant = "3001") @Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014") @Mapping(target = "stringListConstants", constant = "jack-jill-tom") Target sourceToTarget(Source s);
|
效果说明:
- 当
s.getStringProp() == null 时 → stringProperty 被设为 "undefined",否则使用原值
- 当
s.getLongProperty() == null 时 → longProperty 被设为 -1
stringConstant 始终为 "Constant Value"(constant 无视 source)
"jack-jill-tom" 会被按破折号解析并映射为 List<String>
八、踩坑记录
与 Lombok 同时使用的冲突问题
现象:同时使用 MapStruct 和 Lombok 时,编译后生成的实现类中缺少 set 方法。
原因:两个注解处理器的执行顺序冲突——Lombok 还没来得及生成 getter/setter,MapStruct 就已经去读取了。
解决:在 pom.xml 中显式指定注解处理器路径及顺序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <encoding>UTF-8</encoding> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </path> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> <configuration> <useSystemClassLoader>false</useSystemClassLoader> <skipTests>true</skipTests> </configuration> </plugin> </plugins> </build>
|