MapStructPlus:简介与实战

偶然间看到MapStructPlus项目,不用说,类比关系:

  • MyBatis-Plus之于MyBatis;
  • MinIO-Plus之于MinIO;
  • ...

对MapStruct不熟悉,可参考:

概述

官网GitHub官方文档

基于原生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注解快速配置。

  1. 基础枚举转换
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

  1. 跨模块枚举映射:枚举与使用类不在同一模块时,在@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;
}

解读:

  • AutoMappingReverseAutoMapping支持qualifiedByNameconditionQualifiedByNamedependsOn属性;
  • 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注解获取实例对象。

最佳实践

  1. 编译期报错优先排查:MapStructPlus编译期生成代码,报错直接定位问题,常见原因:字段名拼写错误、类型不匹配且未配置转换器。
  2. 版本:建议使用1.4.0+版本,支持更多高级功能;
  3. 反向转换:默认支持反向转换,若无需反向转换,在@AutoMapper中设置reverseConvertGenerate = false减少代码生成;
  4. 当需要使用Map与对象转换时,需额外引入hutool-core包。

参考

相关推荐
linksinke3 个月前
Mapstruct引发的 Caused by: java.lang.NumberFormatException: For input string: ““
java·开发语言·exception·mapstruct·numberformat·不能为空
穿条秋裤到处跑7 个月前
MapStruct类型转换接口未自动注入到spring容器中
java·spring·mapstruct
我是我最后的目击者2 年前
MapStruct - 注解汇总
mapstruct
青塬科技2 年前
MapStruct的一些常规用法
java·springboot·mapstruct
Caseythekiwi132 年前
【教程】微服务使用Feign接口进行远程调用的步骤
java·微服务·maven·mybatis·springboot·feign·mapstruct
子沫20202 年前
mapstruct自定义转换,怎样将String转化为List
java·maven·mapstruct
Naylor2 年前
SpringBoot对象拷贝
springboot·mapstruct·cglibbeancopier