前言
MapStruct 是一个代码生成工具,它基于 约定优于配置 的原则,极大地简化了 Java Bean 类型之间映射的实现。生成的映射代码使用简单的方法调用,因此具有速度快、类型安全且易于理解的特点。
Mapstruct Plus 是 Mapstruct 的增强工具,在 Mapstruct 的基础上,实现了自动生成 Mapper 接口的功能,并强化了部分功能,使 Java 类型转换更加便捷、优雅。
MapStruct Plus 内嵌 MapStruct,和 MapStruct 完全兼容,如果之前已经使用 MapStruct,可以无缝替换依赖。
参考网站:
一、Why MapStruct?
多层架构的应用程序通常需要在不同的对象模型之间进行映射(例如,实体类和DTO)。编写这种映射代码是一项繁琐且容易出错的任务。MapStruct 的目标是通过尽可能地自动化这一过程来简化这项工作。
与其他映射框架不同,MapStruct 在编译时生成 Bean 映射代码,这确保了高性能,同时为开发者提供了快速的反馈和全面的错误检查。
比如一个 User 对象需要转换为 UserVo 对象:
@Data
public class User {
private String username;
private int age;
private boolean young;
}
@Data
public class UserDTO {
private String username;
private int age;
private boolean young;
}
常规的有两种方式:
使用 getter 和 setter 方法进行赋值,但是这个方法有着大量枯燥且重复的工作,一旦出错也不易于发现,可读性差。
使用 spring 提供的 BeanUtils 工具类进行对象之间的转换,如下代码块所示,但是因为内部采用反射实现,性能低下,出现问题时不容易调试。
// 创建一个 User 对象
User user = new User();
user.setUsername("jack");
user.setAge(23);
user.setYoung(false);
// 创建一个 UserDTO 对象
UserDTO userDTO = new UserDTO();
// 一行代码实现 user => userDTO
BeanUtils.copyProperties(user, userDTO);
所以 MapStruct 应运而生,这个框架是基于 Java 注释处理器,定义一个转换接口,在编译的时候会根据接口类和方法相关的注解,自动生成实现类,底层是基于 getter 和 setter 方法的,比 BeanUtils 的性能要高。然而美中不足的是,当需要转换的对象较多或者结构复杂的时候,需要定义较多的转换接口和转换方法。
此时,就可以使用 MapStruct Plus ,一个注解就可以生成两个类之间的转换接口,使 Java 类型转换更加便捷和优雅。
二、MapStruct Plus的快速开始
本文以 Spring Boot 项目为例,版本:
Spring Boot:2.7.6
JDK:8
Lombok:1.18.24
1. 引入依赖
引入 mapstruct-plus-spring-boot-starter 依赖
<mapstruct-plus.version>1.4.3</mapstruct-plus.version>
<dependency>
<groupId>io.github.linpeilie</groupId>
<artifactId>mapstruct-plus-spring-boot-starter</artifactId>
<version>${mapstruct-plus.version}</version>
</dependency>
引入 Maven 插件,配置项目的构建过程
<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>
<encoding>UTF-8</encoding>
<!-- MapStruct Plus练习 -->
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>io.github.linpeilie</groupId>
<artifactId>mapstruct-plus-processor</artifactId>
<version>${mapstruct-plus.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
最新版本依赖可以查看:MapStruct Plus 的 Maven 仓库地址
https://mvnrepository.com/artifact/io.github.linpeilie/mapstruct-plus
2. 指定对象映射关系
在 User 或者 UserDTO 上面增加注解 —— @AutoMapper,并设置 target 为对方类。
以下面代码举例,添加注解:@AutoMapper(target = UserDTO.class)
User 类
@Data
@AutoMapper(target = UserDTO.class)
public class User {
private String username;
private int age;
private boolean young;
}
UserDTO 类
@Data
public class UserDTO{
private String username;
private int age;
private boolean young;
}
3. 编写测试代码
@SpringBootTest
public class QuickStartTest {
@Autowired
private Converter converter;
@Test
void testMapStructPlus() {
// 创建一个 User 对象
User user = new User();
user.setUsername("jack");
user.setAge(23);
user.setYoung(false);
UserDTO userDTO = converter.convert(user, UserDTO.class);
System.out.println(userDTO); // UserDTO{username='jack', age=23, young=false}
assert user.getUsername().equals(userDTO.getUsername());
assert user.getAge() == userDTO.getAge();
assert user.isYoung() == userDTO.isYoung();
User newUser = converter.convert(userDTO, User.class);
System.out.println(newUser); // User{username='jack', age=23, young=false}
assert user.getUsername().equals(newUser.getUsername());
assert user.getAge() == newUser.getAge();
assert user.isYoung() == newUser.isYoung();
}
}
4. 运行结果
测试通过,输出:
5. 原理解析
通过以上示例可以看出,User 对象转化为 UserDTO 对象主要是UserDTO userDTO = converter.convert(user, UserDTO.class);这行代码,其底层也很简单,原理是通过 getter 和 setter 实现的:
public UserDTO convert(User arg0) {
if ( arg0 == null ) {
return null;
}
UserDTO userDTO = new UserDTO();
userDTO.setUsername( arg0.getUsername() );
userDTO.setAge( arg0.getAge() );
userDTO.setYoung( arg0.isYoung() );
return userDTO;
}
该代码被保存在 target 包中,具体路径:target/generated-sources/annotations/实体类存放路径
通过上图,可以看到,除此之外,MapStructPlus 会根据当前的默认规则,生成 UserDTO 转换为 User 的接口 UserDTOToUserMapper 及实现类 UserDTOToUserMapperImpl。如果不想生成该转换逻辑的话,可以通过注解的 reverseConvertGenerate 属性来配置。
三、自定义实体类中的属性转换
在上面的例子中,两个实体类中对应的属性都是同一种类型,那么想要自定义属性比如:后端存储的是字符串 String 类型的属性,想给前端返回一个 List 类型的属性,可以根据规则进行转换。
下面的举例是 String 属性和 List 属性之间的相互转化(String 《===》List)
有两种方式:
自定义一个类型转换器,通过 @AutoMapper 的 uses 属性引入
通过 @AutoMapping 中配置的 expression 表达式配置
1. 自定义一个类型转换器
首先定义两个类型转换器,一个是 String 转为 List,一个是 List 是 String。且两个类型转换器需要定义为 Spring 的 Bean,即使用 @Component 注解。
String 转为 List 的转换器:
@Component
public class StringToListConverter {
public List<String> stringToList(String str) {
if (str == null) {
return Collections.emptyList();
}
return Arrays.asList(str.split(","));
}
}
List 转为 String 的转换器:
@Component
public class ListToStringConverter {
public String listToString(List<String> list) {
if (list == null || list.isEmpty()) {
return null;
}
return String.join(",", list);
}
}
2. 使用类型转换器
第二步,使用该类型转换器,即在 @AutoMapper 注解中使用 uses,且给需要转化的属性加上 @AutoMapping 注解,target 指向另一个需要转化的属性。
User 类:
@Data
@AutoMapper(target = UserDTO.class, uses = StringToListConverter.class)
public class User {
private String username;
private int age;
private boolean young;
@AutoMapping(target = "tagList")
private String tags;
}
UserDTO 类:
@Data
@AutoMapper(target = User.class, uses = ListToStringConverter.class)
public class UserDTO {
private String username;
private int age;
private boolean young;
@AutoMapping(target = "tags")
private List<String> tagList;
}
3. 进行测试
第三步,进行测试。
@SpringBootTest
public class QuickStartTest {
@Autowired
private Converter converter;
@Test
void testMapStructPlus() {
// 创建一个对象
User user = new User();
user.setUsername("jack");
user.setAge(23);
user.setYoung(false);
user.setTags("Java,Python,C++");
// 转换
UserDTO userDTO = converter.convert(user, UserDTO.class);
System.out.println(userDTO);
assert userDTO.getTagList().size() == 3;
}
}
测试结果:
测试用例通过,User 类中的 String 类型的 tags 属性,成功转化为 UserDTO 类中的 List 类型的 tagList 属性。
还有一种方法是直接在注解中写表达式,但是博主觉得这种方式没有自定义转换器好,所以在本文中不列举
如果感兴趣,详情请参考:表达式自定义属性转换
https://www.mapstruct.plus/guide/class-convert.html#%E8%A1%A8%E8%BE%BE%E5%BC%8F
四、Map转为Object
MapStruct Plus 提供了 Map<String, Object> 转化为对象的功能。
转换逻辑:
针对目标类中的一个属性,首先会判断 Map 中是否存在该键,如果存在的话,首先判断类型,
如果类型相同,直接强转
若果类型不同,会使用 Hutool 提供的类型转换工具尝试转换为目标类型
MapStruct Plus 在 1.4.0+ 版本取消了内置 Hutool 框架,如果需要用到 Map 转化为对象的功能时,需要引入 hutool-core 这个依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.8.29</version>
</dependency>
1. 使用步骤
引入 hutool-core 依赖
在目标类上添加 @AutoMapMapper 注解
同时支持自定义类作为属性,需要在自定义类上增加 @AutoMapMapper 注解
2. 定义对象
定义两个对象:MapModelA 和 MapModelB
MapModelA 类:
@AutoMapMapper
@Data
public class MapModelA {
private String str;
private int i1;
private Long l2;
private MapModelB mapModelB;
}
MapModelB类:
@AutoMapMapper
@Data
public class MapModelB {
private Date date;
}
3. 转换测试
@SpringBootTest
public class MapToObjectTest {
@Autowired
private Converter converter;
@Test
void testMapStructPlus() {
Map<String, Object> mapModel1 = new HashMap<>();
mapModel1.put("str", "1jkf1ijkj3f");
mapModel1.put("i1", 111);
mapModel1.put("l2", 11231);
Map<String, Object> mapModel2 = new HashMap<>();
mapModel2.put("date", DateUtil.parse("2025-03-12 11:59:59"));
mapModel1.put("mapModelB", mapModel2);
final MapModelA mapModelA = converter.convert(mapModel1, MapModelA.class);
System.out.println(mapModelA); // MapModelA(str=1jkf1ijkj3f, i1=111, l2=11231, mapModelB=MapModelB(date=2023-02-23 01:03:23))
}
}
测试成功,Map 对象成功转化为 MapModelA对象:
五、枚举类型转换
枚举自动转换(当前特性从 1.2.2 开始支持)
当需要进行枚举转换时(例如枚举转换为编码值,或者由编码转换为枚举),可以在目标枚举添加 @AutoEnumMapper 注解, 增加该注解后,在任意类型中需要转换该枚举时都可以自动转换。
使用该注解需要注意:当前枚举必须有一个可以保证唯一的字段,并在使用当前注解时,将该字段名,添加到注解提供的 value 属性中。
还有就是枚举和使用枚举的类,要在同一个模块中。
1. 定义一个枚举类
@Getter
@AllArgsConstructor
@AutoEnumMapper("state")
public enum GoodsStateEnum {
ENABLED(1, "启用"),
DISABLED(0, "禁用");
private final Integer state;
private final String desc;
}
2. 定义要转换的对象
Goods类:
@Data
@AutoMapper(target = GoodsVo.class)
public class Goods {
private GoodsStateEnum state;
}
GoodsVO类:
@Data
public class GoodsVO {
private Integer state;
}
3. 转换测试
@SpringBootTest
public class EnumToValueTest {
@Autowired
private Converter converter;
@Test
void testMapStructPlus() {
Goods goods = new Goods();
goods.setState(GoodsStateEnum.ENABLED);
final GoodsVO goodsVO = converter.convert(goods, GoodsVO.class);
System.out.println(goodsVO);
Assert.equals(goodsVO.getState(), goods.getState().getState());
final Goods goods2 = converter.convert(goodsVO, Goods.class);
System.out.println(goods2);
Assert.equals(goods2.getState(), GoodsStateEnum.ENABLED);
}
}
测试成功,Enum 可以转化为整形,整形也可以转化为 Enum:
4. 跨模块支持
当枚举与要使用的类型,不在同一个模块(module)中时,并不能自动转换,需要指定依赖关系。
在 AutoMapper 注解中,可以通过属性 useEnums 来指定,当前转换关系,需要依赖的枚举类列表。这些枚举需要被 AutoEnumMapper注解。
该特性从 1.4.2 开始支持
需要注意的是,当两个类在同一个模块(module)中,无需指定,可以自动转换。当前特性主要解决跨模块之间不能自动转换的问题。
六、一个类与多个类之间的转换
MapStruct Plus 还支持一个类和多个类进行转换,可以通过 @AutoMappers 来配置,该注解支持配置多个 @AutoMapper。
在配置多个类进行转化的时候,多个类可能有相同的属性,为了解决属性冲突的问题,可以使用 @AutoMappings 指定多个转换规则,并且在使用 @AutoMapping 注解时,配置 targetClass 属性,指定当前规则的目标转化类。
如果配置 @AutoMapping 注解时,没有指定 targetClass,那么当前规则就会应用所有类转换。
1. 定义对象
MapStructPlus 除了支持一个类与单个目标类型进行转换,还支持一个类与多个目标类型进行转换。
配置多个类转换
当想要配置一个类与多个类进行转换时,可以通过 @AutoMappers 来配置,该注解支持配置多个 @AutoMapper
定义一个User类,一个Course类,一个UserDTO类。其中UserDTO类将与User类和Course类互相映射。User类和Course类都有username属性,但是只将User类中的username属性映射。
User类:
@Data
@AutoMapper(target = UserDTO.class, uses = StringToListConverter.class)
public class User {
private String username;
private int age;
private boolean young;
@AutoMapping(target = "tagList")
private String tags;
}
Course类:
@Data
@AutoMapper(target = UserDTO.class)
public class Course {
@AutoMapping(targetClass = UserDTO.class, ignore = true) // 忽略 UserDTO 中的 username 属性
private String username;
private String teacher;
}
UserDTO类:
@Data
@AutoMappers({
@AutoMapper(target = User.class, uses = ListToStringConverter.class),
@AutoMapper(target = Course.class)
})
public class UserDTO {
@AutoMappings({
@AutoMapping(targetClass = User.class),
@AutoMapping(targetClass = Course.class, ignore = true)
})
private String username;
private int age;
private boolean young;
@AutoMapping(targetClass = User.class, target = "tags")
private List<String> tagList;
private String teacher;
}
2. 转换测试
@SpringBootTest
public class OneToOthersTest {
@Autowired
private Converter converter;
@Test
void testOneToMany() {
// 创建 User 对象
User user = new User();
user.setUsername("jack");
user.setAge(25);
user.setYoung(false);
user.setTags("Java,Python,Go,C++");
// 创建 Course 对象
Course course = new Course();
course.setUsername("Java 开发");
course.setTeacher("教 Java 的老师");
// 转换(User 对象和 Course 对象)为 UserDTO 对象
UserDTO userDTO = converter.convert(user, UserDTO.class);
userDTO = converter.convert(course, userDTO);
System.out.println(userDTO);
// 转换 UserDTO 对象为(User 对象和 Course 对象)
user = converter.convert(userDTO, User.class);
course = converter.convert(userDTO, Course.class);
System.out.println(user);
System.out.println(course);
}
}