MapStruct,降低无用代码的神器
在学习《告别BeanUtils,Mapstruct从入门到精通》后,我发觉MapStruct确实是一个提升系统性能,降低无用代码的神器。然而,在实践这篇文章过程中,我遇到了些问题,并由此对MapStruct框架有了更深入的理解,以下将我的学习收获分享给大家。
增加了不同环境下Maven引入的注意事项(见“引入”章) 增加了一对多字段(例如Json字段)互转的代码(见“高级转换”第一节) 增加了子类字段互转的代码(见“高级转换”第二节) 增加了利用Spring进行依赖注入的代码(见“高级转换”第三节)
MapStruct is a code generator that greatly simplifies the implementation of mappings between Java bean types based on a convention over configuration approach.——https://mapstruct.org/
从官方定义来看,MapStruct类似于我们熟悉的BeanUtils, 是一个Bean的转换框架。
In contrast to other mapping frameworks MapStruct generates bean mappings at compile-time which ensures a high performance, allows for fast developer feedback and thorough error checking.——https://mapstruct.org/
他与BeanUtils最大的不同之处在于,其并不是在程序运行过程中通过反射进行字段复制的,而是在编译期生成用于字段复制的代码(类似于Lombok生成get()和set()方法),这种特性使得该框架在运行时相比于BeanUtils有很大的性能提升。
▐ Maven
由于MapStruct和Lombok都会在编译期生成代码,如果配置不当,则会产生冲突,因此在工程中同时使用这两个包时,应该按照以下方案导入:
当POM中不包含Lombok时
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.2.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.2.Final</version>
</dependency>
当POM中包含Lombok且不包含<annotationProcessorPaths>时
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.2.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.2.Final</version>
</dependency>
注意:引入时,mapstruct-processor必须lombok后面。
当POM中包含Lombok且包含<annotationProcessorPaths>时
<properties>
<org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source> <!-- depending on your project -->
<target>1.8</target> <!-- depending on your project -->
<annotationProcessorPaths>
<properties>
<org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source> <!-- depending on your project -->
<target>1.8</target> <!-- depending on your project -->
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<!-- other annotation processors -->
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<!-- other annotation processors -->
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
▐ Idea Plugin
搜索MapStruct Support安装即可,可以在使用MapStruct时获得更加丰富代码提示。
▐ 字段完全一致
待转换的类
@Data
@Builder
public class Source {
private Long id;
private Long age;
private String userNick;
}
转换目标类
@Data
public class Target {
private Long id;
private Long age;
private String userNick;
}
转换器
注意:Mapper是Mapstruct的注解。
@Mapper
public abstract class Converter {
public static Converter INSTANT = Mappers.getMapper(Converter.class);
public abstract Target convert(Source source);
}
使用示例
final Source source = Source.builder()
.id(1L)
.age(18L)
.userNick("Nick")
.build();
final Target target = Converter.INSTANT.convert(source);
System.out.println(target);
输出:
Target(id=1, age=18, userNick=Nick)
▐ 一一对应的字段名不一致、类型不一致
待转换的类
@Data
@Builder
public class Source {
private Long id;
private Long age;
private String userNick;
}
转换目标类
@Data
public class Target {
private Long id;
private Integer age;
private String nick;
}
转换目标类修改了age字段的类型,和userNick字段的名字,这两个类的字段仍然是一一对应的。
转换器
@Mapper
public abstract class Converter {
public static Converter INSTANT = Mappers.getMapper(Converter.class);
// 字段类型映射修改
@Mapping(source = "age", target = "age", resultType = Integer.class)
// 字段名映射修改
@Mapping(source = "userNick", target = "nick")
public abstract Target convert(Source source);
}
使用示例
final Source source = Source.builder()
.id(1L)
.age(18L)
.userNick("Nick")
.build();
final Target target = Converter.INSTANT.convert(source);
System.out.println(target);
输出:
Target(id=1, age=18, nick=Nick)
源码解析
▐ 一对多字段互转
互相转换的类
@Data
@Builder
public class VO {
private Long id;
private Long age;
private String userNick;
}
@Data
public class DTO {
private Long id;
private String extra;
}
多个字段转换为一个字段
常用于将多个字段转为JSON字段,在以下示例中,为了避免引入第三方包(如FastJson),仅使用字符串拼接两个字段,Json方式同理。
@Mapper
public abstract class Converter {
public static Converter INSTANT = Mappers.getMapper(Converter.class);
@Mapping(target = "extra", source = "vo", qualifiedByName = "convertToExtra")
public abstract DTO convert(VO vo);
@Named("convertToExtra")
public String convertToExtra(VO vo) {
return String.format("%s,%s", vo.getAge(), vo.getUserNick());
}
}
将多个字段转换为一个字段,需要以下几个步骤:
创建自定义转换方法(本例为convertToExtra()):
方法入参类型为被转换的类(本例为VO),出参为转换好的字段(本例为extra);
为方法加上@Named注解,并自定义该方法在mapStruct中的名字(本例中为convertToExtra)。
在转换方法上增加Mapping注解,其中:
source字段必须与转换方法入参名字相同(本例中均为vo);
target字段为目标字段(本例中为extra);
qualifiedByName字段为上述自定义的方法名字。
将一个字段转换为多个字段
该方法常用于从JSON字段中取出数据。
原理与上述方法类似,定义两个自定义转换方法,用于转换extra字段。
@Mapper
public abstract class Converter {
public static Converter INSTANT = Mappers.getMapper(Converter.class);
@Mapping(target = "age", source = "extra", qualifiedByName = "extractAge")
@Mapping(target = "userNick", source = "extra", qualifiedByName = "extractUserNick")
public abstract VO convertToVO(DTO dto);
@Named("extractAge")
public Long extractAge(String extra) {
// 从extra中提取第一个值
return Long.valueOf(extra.split(",")[0]);
}
@Named("extractUserNick")
public String extractUserNick(String extra) {
// 从extra中提取第二个值
return extra.split(",")[1];
}
}
使用示例
final VO vo = VO.builder()
.id(1L)
.age(18L)
.userNick("Nick")
.build();
// 转为DTO
final DTO dto = Converter.INSTANT.convertToDTO(vo);
System.out.println(dto);
// 转回VO
final VO newVo = Converter.INSTANT.convertToVO(dto);
System.out.println(newVo);
DTO(id=1, extra=18,Nick)
VO(id=1, age=18, userNick=18)
为转换加缓存
@Mapper
public abstract class Converter {
public static Converter INSTANT = Mappers.getMapper(Converter.class);
/**
* extra字段解析后的buffer,避免多次重复解析
*/
private final ThreadLocal<String[]> extraFieldBufferLocal = new ThreadLocal<>();
@Mapping(target = "age", source = "extra", qualifiedByName = "extractAge")
@Mapping(target = "userNick", source = "extra", qualifiedByName = "extractUserNick")
public abstract VO convertToVO(DTO dto);
@Named("extractAge")
public Long extractAge(String extra) {
if (extraFieldBufferLocal.get() == null) {
extraFieldBufferLocal.set(extractExtraField(extra));
}
return Long.valueOf(extraFieldBufferLocal.get()[0]);
}
@Named("extractUserNick")
public String extractUserNick(String extra) {
if (extraFieldBufferLocal.get() == null) {
extraFieldBufferLocal.set(extractExtraField(extra));
}
return extraFieldBufferLocal.get()[1];
}
/**
* 提取extra字段
*
* @param extra extra字段
* @return extra字段的提取结果
*/
public String[] extractExtraField(final String extra) {
return extra.split(",");
}
}
▐ 子类字段互转
常用于平铺类和嵌套类之间的转换,例如,前端需要将类中的所有字段打平,就可以参考以下示例代码。
互相转换的类
VO:
@Data
@Builder
public class VO {
private Long id;
private Date gmtCreate;
private Long age;
private String userNick;
}
@Data
public class DTO {
private Long id;
private Date gmtCreate;
private Config config;
@Data
public static class Config{
private String age;
private String userNick;
}
}
子类字段互转
@Mapper
public abstract class Converter {
public static Converter INSTANT = Mappers.getMapper(Converter.class);
@Mapping(target = "config.age", source = "age")
@Mapping(target = "config.userNick", source = "userNick")
abstract DTO convertToDTO(VO source);
@Mapping(target = "age", source = "config.age")
@Mapping(target = "userNick", source = "config.userNick")
abstract VO convertToVO(DTO dto);
}
使用示例
final VO vo = VO.builder()
.id(1L)
.age(10L)
.gmtCreate(new Date())
.userNick("nick")
.build();
final DTO dto = Converter.INSTANT.convertToDTO(vo);
System.out.println(dto);
final VO newVo = Converter.INSTANT.convertToVO(dto);
System.out.println(newVo);
输出:
DTO(id=1, gmtCreate=Fri Sep 16 00:09:05 CST 2022, config=DTO.Config(age=10, userNick=nick))
VO(id=1, gmtCreate=Fri Sep 16 00:09:05 CST 2022, age=10, userNick=nick)
▐ 利用Spring进行依赖注入
转换器
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public abstract class Converter {
public abstract Target convert(Source source);
}
public static Converter INSTANT = Mappers.getMapper(Converter.class);
并修改了Mapper注解为:
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
使用示例
在Spring上下文中,可以直接使用依赖注入注解(Autowired、Resource)获得对应的Converter
@Controller
public class MainController {
@Resource
private Converter convert;
@GetMapping("/")
@ResponseBody
public boolean test() {
final Source source = Source.builder()
.id(1L)
.age(18L)
.userNick("nick")
.build();
final Target result = convert.convert(source);
System.out.println(result);
return true;
}
}
输出:
Target(id=1, age=18, userNick=Nick)
本文在第一章提到,引入MapStruct时,必须要注意Lombok包与MapStruct包的顺序,关于这一点,网上很少有相关文章提及。
▐ 问题来源
在复现《告别BeanUtils,Mapstruct入门到精通》代码时,文中提到的引入顺序是这样的:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.0.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.0.Final</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
实践发现,在一个空工程中,如果按照上述写法引入MapStruct,其并不能正常工作。
而当修改引入顺序为以下方案时,则MapStruct可以正常使用。
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.0.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.0.Final</version>
</dependency>
▐ MapStruct基本原理
为了探究上述问题产生的原因,我们首先要理解MapStruct的基本原理。
MapStruct与其他Bean映射库最大的不同就是,其在编译期间生成转换代码,而不是在运行时通过反射生成代码。
为了更直观的理解这一点,可以从target中找到MapStruct自动生成的对应的ConveterImpl类,如下图所示:
即MapStruct为我们编写的Convert抽象类自动生成了一个实现。
而Lombok也是在编译时自动生成代码,那么问题大概率就出现在这里了。
▐ MapStruct是如何与Lombok共存的?
查阅MapStruct官方文档可以发现这样一段内容:
▐ MapStruct官方推荐的导入流程
在进一步查看MapStruct官网时发现,其并没有将MapStruct-processor放在dependencies中,而是放在了annotationProcessorPaths层级下:
https://mapstruct.org/documentation/installation/
...
<properties>
<org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
</properties>
...
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source> <!-- depending on your project -->
<target>1.8</target> <!-- depending on your project -->
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<!-- other annotation processors -->
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
<annotationProcessorPaths>地址:https://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html#annotationProcessorPaths
<annotationProcessorPaths>地址:https://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html#annotationProcessorPaths
mvn dependency:build-classpath -Dmdep.outputFile=classPath.txt
从导出内容可以看出,classPath中的Jar包顺序就是与dependencies中导入的顺序是相同的。
自此,关于MapStruct导入顺序的所有问题均已经被解决,总结如下:
在POM中没有annotationProcessorPaths时,Maven使用的classPath作为注解处理器执行的顺序,而classPath的顺序正是dependencies中导入的顺序。
当MapStruct依赖在Lombok依赖前面时,在执行注解处理器期间, 由于Lombok还未生成get、set代码,因此在MapStruct看来,这些类并没有公开的成员变量,也就无从生成用于转换的方法。
在使用annotationProcessorPaths后,其强制规定了注解处理器的顺序,dependencies中的顺序就被忽略了,Maven一定会先运行Lombok再运行MapStruct,代码即可正常运行。
大淘宝技术-用户平台技术团队
用户平台技术团队是一支集研发、数据、算法一体的团队,负责淘宝天猫的用户增长,游戏互动,平台会员和私域运营等消费者核心业务。在对用户争夺进入白热化的时期,团队正承担着捍卫电商主板块增长的重要使命,是阿里核心电商战场的参与者,用持续的技术创新来驱动阿里电商引擎的稳步前行。
这是一支年轻开放的团队,在这里你将收获超大规模高并发场景的架构设计能力,洞悉用户增长最前沿的实践方法,在数字化时代收获最核心的竞争力。团队技术氛围浓厚,倡导创新和工程师文化,鼓励用数据和代码发现解决问题。团队研发流程规范,代码质量高,学习成长速度快。