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@Builderpublic class Source { private Long id; private Long age; private String userNick;}转换目标类
@Datapublic class Target { private Long id; private Long age; private String userNick;}转换器
注意:Mapper是Mapstruct的注解。
@Mapperpublic 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@Builderpublic class Source { private Long id; private Long age; private String userNick;}转换目标类
@Datapublic class Target { private Long id; private Integer age; private String nick;}转换目标类修改了age字段的类型,和userNick字段的名字,这两个类的字段仍然是一一对应的。
转换器
@Mapperpublic 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@Builderpublic class VO { private Long id; private Long age; private String userNick;}@Datapublic class DTO { private Long id; private String extra;}多个字段转换为一个字段
常用于将多个字段转为JSON字段,在以下示例中,为了避免引入第三方包(如FastJson),仅使用字符串拼接两个字段,Json方式同理。
@Mapperpublic 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字段。
@Mapperpublic 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();
// 转为DTOfinal DTO dto = Converter.INSTANT.convertToDTO(vo);System.out.println(dto);
// 转回VOfinal VO newVo = Converter.INSTANT.convertToVO(dto);System.out.println(newVo);DTO(id=1, extra=18,Nick)VO(id=1, age=18, userNick=18)为转换加缓存
@Mapperpublic 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@Builderpublic class VO { private Long id; private Date gmtCreate; private Long age; private String userNick;}@Datapublic class DTO { private Long id; private Date gmtCreate; private Config config;
@Data public static class Config{ private String age; private String userNick; }}子类字段互转
@Mapperpublic 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
@Controllerpublic 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,代码即可正常运行。
大淘宝技术-用户平台技术团队
用户平台技术团队是一支集研发、数据、算法一体的团队,负责淘宝天猫的用户增长,游戏互动,平台会员和私域运营等消费者核心业务。在对用户争夺进入白热化的时期,团队正承担着捍卫电商主板块增长的重要使命,是阿里核心电商战场的参与者,用持续的技术创新来驱动阿里电商引擎的稳步前行。
这是一支年轻开放的团队,在这里你将收获超大规模高并发场景的架构设计能力,洞悉用户增长最前沿的实践方法,在数字化时代收获最核心的竞争力。团队技术氛围浓厚,倡导创新和工程师文化,鼓励用数据和代码发现解决问题。团队研发流程规范,代码质量高,学习成长速度快。