image.png
官网地址: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>

<!-- MapStruct 依赖 -->
<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;
// constructor, getters, setters
}

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;

// constructor, getters, setters
}

定义 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);
}

三个关键点:

  1. @Mapper 注解标记该接口为映射入口,是编译时 MapStruct 处理器的识别标记
  2. @Mapping 注解处理字段名不一致的情况;对于类型不同的字段(如枚举→字符串),MapStruct 会自动进行隐式转换
  3. INSTANCE 成员变量按惯例声明,客户端可通过 CarMapper.INSTANCE.carToCarDto(car) 直接调用

componentModel 属性

@MappercomponentModel 属性决定生成实现类的组件模式:

模式 说明 获取方式
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
// 设置组件模型为 Spring
@Mapper(componentModel = "spring")
public interface CarMapper {

CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);

@Mapping(source = "numberOfSeats", target = "seatCount")
CarDto carToCarDto(Car car);
}

编译与查看生成结果

由于 MapStruct 在编译期生成代码(IDE 自动编译通常不触发),需手动执行:

1
mvn compile

编译后在 target 目录下会多出 CarMapperImpl.class —— 这就是自动生成的实现类。用 IDE 反编译功能可以查看其源码:字段名不同(seatCountnumberOfSeats)的映射由 @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 内置了大量自动类型转换能力。以下是开箱即用的转换规则:

转换类别 示例 备注
基本类型 ↔ 包装类 intInteger, booleanBoolean 双向自动转换
基本类型之间 intlong, byteint 大→小可能损失精度或值域
基本类型 / 包装类 ↔ String intString, booleanString 分别调用 valueOf() / parseInt()
日期 ↔ String DateString 通过 dateFormatnumberFormat 指定格式
BigDecimal / BigIntegerString 同上

大类型转小类型(如 longint)可能导致精度丢失。可通过 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>
<!-- 先执行 Lombok -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<!-- 再执行 MapStruct -->
<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>