偶然间看到MapStructPlus项目,不用说,类比关系:
- MyBatis-Plus之于MyBatis;
- MinIO-Plus之于MinIO;
- ...
对MapStruct不熟悉,可参考:
概述
基于原生MapStruct,只做增强不做修改,目标是让对象映射更简单、更优雅:
- 零接口定义:无需手动编写
@Mapper注解接口,一行@AutoMapper注解搞定映射关系,少写N行转换代码; - 自动生成实现:编译期自动生成原生Java转换代码,性能和手写几乎无差别;
- 多环境适配:支持SpringBoot、非SpringBoot环境,依赖配置极简;
- 功能强化:原生MapStruct需复杂配置的场景(嵌套对象、枚举映射、多类转换),MapStructPlus用注解就能轻松实现。
如果只是如上列举,可能有些空洞。以我现在维护的项目来说,一张表对应一个DO(Domain Object,有些公司或团队,也称之为PO,Persistence Object),也就是说对应地,一张表至少需要手写一个转换器类:

之所以说是【至少】,就得看项目组或团队采纳的代码架构和风格。如果是DDD这种架构风格,则可能会定义DO,BO,DTO,VO,PO...,很有【可能】,每两个O之间的转换都需要写一个Converter类转换器。
想想就很可怕,虽然有【EasyCode】这一类IDEA插件,可用于辅助生成代码。但生成的代码,它就是会存在于代码库里啊,会增加代码库的体量和行数,随之而来的就是代码复杂度以及维护难度。
关于这些O的概念,可参考Java对象拷贝。
入门
直接上代码。pom.xml文件配置如下:
xml
<dependency>
<groupId>io.github.linpeilie</groupId>
<artifactId>mapstruct-plus-spring-boot-starter</artifactId>
<version>1.4.8</version>
</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.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
<path>
<!-- Lombok与MapStruct绑定器(Lombok 1.18.16+版本需加上) -->
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
用注解标记映射关系,通过Converter工具类实现转换。
java
// 在源类添加@AutoMapper注解,指定目标类
@Data
@AutoMapper(target = UserDto.class)
public class User {
private String username;
private int age;
private boolean young;
@AutoMapping(dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date birthday;
}
@Data
public class UserDto {
// 省略其他字段
private Boolean young;
private String birthday;
}
// SpringBoot环境,自动注入Converter
@SpringBootTest
public class QuickStartTest {
@Autowired
private Converter converter;
@Test
public void testConvert() {
User user = new User();
user.setUsername("jack");
user.setAge(23);
user.setYoung(false);
user.setBirthday(new Date());
// 实体转DTO
UserDto userDto = converter.convert(user, UserDto.class);
// DTO转实体(支持自动反向转换)
User newUser = converter.convert(userDto, User.class);
assert user.getUsername().equals(newUser.getUsername());
}
}
进阶
介绍几个稍微进阶的用法。
字段映射自定义
当源类与目标类字段名、类型不一致时,用@AutoMapping注解精准配置,支持格式转换、忽略字段等。
java
@Data
@AutoMapper(target = UserDto.class)
public class User {
// 字段名不一致映射
@AutoMapping(target = "userAge") // 目标字段名:userAge
private int age;
@AutoMapping(defaultValue = "false")// 为null时,转换到目标类后的默认值
private boolean young;
// 数字格式:保留两位小数,前缀加¥
@AutoMapping(target = "assetsStr", numberFormat = "¥0.00")
private double assets;
// 自定义日期字符串格式
@AutoMapping(target = "birthdayStr", dateFormat = "yyyy-MM-dd")
private Date birthday;
// 忽略密码字段,不参与映射
@AutoMapping(target = "password", ignore = true)
private String password;
}
多类映射
即一个类对应多个目标类,当一个实体需要转换为多个DTO/VO时,用@AutoMappers注解批量配置,支持不同目标类的差异化规则。
java
@Data
@AutoMappers({
@AutoMapper(target = UserDto.class),
@AutoMapper(target = UserVo.class)
})
public class User {
// 对UserDto单独配置:生日转字符串格式
@AutoMapping(targetClass = UserDto.class, target = "birthday", dateFormat = "yyyy-MM-dd")
// 对UserVo单独配置:忽略生日字段
@AutoMapping(targetClass = UserVo.class, target = "birthday", ignore = true)
private Date birthday;
// 未指定 targetClass,对所有目标类生效
@AutoMapping(target = "userName", source = "username")
private String username;
}
提示:targetClass支持父类,子类会自动继承映射规则。
嵌套对象映射
即支持多层级转换,当类中包含嵌套对象,MapStructPlus自动支持嵌套转换,无需额外配置,复杂场景可自定义嵌套规则。
java
@Data
@AutoMapper(target = OrderDto.class)
public class Order {
private Long orderId;
}
@Data
@AutoMapper(target = UserDto.class)
public class User {
private String username;
private List<Order> orderList;
}
@Data
public class UserDto {
private String username;
private List<OrderDto> orderList;
}
@Test
public void testNestedConvert() {
User user = new User();
user.setUsername("jack");
Order order = new Order();
order.setOrderId(1L);
order.setAmount(new BigDecimal("100.00"));
user.setOrderList(Collections.singletonList(order));
UserDto dto = converter.convert(user, UserDto.class); System.out.println(dto.getOrderList().get(0).getOrderId());
}
枚举映射
枚举与基本类型转换,用@AutoEnumMapper注解快速配置。
- 基础枚举转换
java
@Getter
@AllArgsConstructor
@AutoEnumMapper("state") // 标记枚举,指定映射字段
public enum GoodsStateEnum {
ENABLED(1),
DISABLED(0);
private final Integer state;
}
@Data
@AutoMapper(target = GoodsVo.class)
public class Goods {
private GoodsStateEnum state;
}
@Data
public class GoodsVo {
private Integer state;
}
@Test
public void testEnumConvert() {
Goods goods = new Goods();
goods.setState(GoodsStateEnum.ENABLED);
GoodsVo vo = converter.convert(goods, GoodsVo.class);
assert vo.getState().equals(1);
Goods newGoods = converter.convert(vo, Goods.class);
assert newGoods.getState().equals(GoodsStateEnum.ENABLED);
}
注意事项:枚举类上添加@AutoEnumMapper注解,必须要确保该枚举类有一个可保证唯一的字段,一般都是private final int code。
- 跨模块枚举映射:枚举与使用类不在同一模块时,在
@AutoMapper中通过useEnums指定依赖枚举:
java
@Data
@AutoMapper(target = GoodsVo.class, useEnums = {GoodsStateEnum.class})
public class Goods {
private GoodsStateEnum state;
}
自定义类型转换器
java
// String(逗号分隔)→ List<String>
@Component
public class StrToStrListConverter {
public List<String> strToList(String str) {
return StrUtil.split(str);
}
}
// 应用
@Data
@AutoMapper(target = UserDto.class, uses = StrToStrListConverter.class)
public class User {
private String username;
// 源字段:String(逗号分隔),目标字段:List<String>
@AutoMapping(target = "hobbyList")
private String hobbies;
}
@Data
public class UserDto {
private String username;
private List<String> hobbyList;
}
提示:当自定的类型转换器中有多个方法时,可通过@AutoMapping的qualifiedByName来指定具体的转换方法。
对象循环嵌套场景
类循环嵌套是指两个类互相引用,存在这种情况时,直接进行转换时,会导致栈溢出的问题(StackOverflowException)。
java
@Data
public class TreeNode {
private TreeNode parent;
private List<TreeNode> children;
}
@Data
public class TreeNodeDto {
private TreeNodeDto parent;
private List<TreeNodeDto> children;
}
其中,parent属性可以是其他类型的,可能跨越一个更长的属性链形成的嵌套循环。
为适配这种情况,AutoMapper注解中增加cycleAvoiding属性,用于标识,是否需要避免循环嵌套问题,默认为false。如果需要避免循环嵌套,需将该属性设置为true。当配置为true时,在整个对象的转换过程链路中,会传递一个CycleAvoidingMappingContext对象,临时保存转换生成的对象;在转换链路中,如果发现需要生成的对象已经存在,会直接返回该类型,从而避免栈溢出问题。
修改配置:
java
@Data
@AutoMapper(target = TreeNodeDto.class, cycleAvoiding = true)
public class TreeNode {
private TreeNode parent;
private List<TreeNode> children;
}
@Data
@AutoMapper(target = TreeNode.class, cycleAvoiding = true)
public class TreeNodeDto {
private TreeNodeDto parent;
private List<TreeNodeDto> children;
}
编译生成的转换逻辑如下:
java
public TreeNodeDto convert(TreeNode arg0, CycleAvoidingMappingContext arg1) {
TreeNodeDto target = arg1.getMappedInstance(arg0, TreeNodeDto.class);
if (target != null) {
return target;
}
if (arg0 == null) {
return null;
}
TreeNodeDto treeNodeDto = new TreeNodeDto();
arg1.storeMappedInstance(arg0, treeNodeDto);
treeNodeDto.setParent(demoConvertMapperAdapterForCycleAvoiding.iglm_TreeNodeToTreeNodeDto(arg0.getParent(), arg1));
treeNodeDto.setChildren(demoConvertMapperAdapterForCycleAvoiding.iglm_TreeNodeToTreeNodeDto(arg0.getChildren(), arg1));
return treeNodeDto;
}
解读:
AutoMapping、ReverseAutoMapping支持qualifiedByName、conditionQualifiedByName和dependsOn属性;AutoMappings支持配置在方法上面。
表达式
直接看例子。一个很常见的枚举类:
java
@Getter
@AllArgsConstructor
public enum SexEnum {
MALE(1, "男"),
FEMALE(2, "女"),
private final Integer code;
private final String desc;
public static String getDescByCode(Integer code) {
for (SexEnum item : SexEnum.values()) {
if (item.getCode().equals(code)) {
return item.getDesc();
}
}
return "";
}
}
使用:
java
@Data
@AutoMapper(target = StudentVo.class, uses = CourseConverter.class, imports = SexEnum.class)
public class Student {
private String name;
@AutoMapping(qualifiedByName = "courseToList")
private String courses;
@AutoMapping(expression = "java(SexEnum.getDescByCode(source.getSex()))")
private Integer sex;
}
其他
其他未提到的注解:
- @AutoMapMapper:
- @Named
componentModel,指定Mapper模式,即实现类添加指定的注解类型,可选值有:
default:默认情况,可通过Mappers.getMapper(Class)方式获取实例对象;cdi:生成的实现类上面会添加@ApplicationScoped注解,可通过 @Inject 注解获取实例对象;spring:生成的实现类上面会添加@Component注解,可通过@Autowired或@Resource注解获取实例对象jsr330:生成的实现类上面会添加@javax.inject.Named和@Singleton注解,可通过@Inject注解获取实例对象;jakarta:生成的实现类上面会添加@RequestScoped注解,可通过@Inject注解获取实例对象。
最佳实践
- 编译期报错优先排查:MapStructPlus编译期生成代码,报错直接定位问题,常见原因:字段名拼写错误、类型不匹配且未配置转换器。
- 版本:建议使用
1.4.0+版本,支持更多高级功能; - 反向转换:默认支持反向转换,若无需反向转换,在
@AutoMapper中设置reverseConvertGenerate = false减少代码生成; - 当需要使用
Map与对象转换时,需额外引入hutool-core包。