设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间08 现在时间08 缓存数据 若岁月静好,那就颐养身心;若时光阴暗,那就多些历练。早安!

若岁月静好,那就颐养身心;若时光阴暗,那就多些历练。早安!

查看: 1418|回复: 4

AbstractProcessor扩展MapStruct自动生成实体映射工具类

[复制链接]

  离线 

TA的专栏

  • 打卡等级:热心大叔
  • 打卡总天数:176
  • 打卡月天数:0
  • 打卡总奖励:2692
  • 最近打卡:2023-08-27 08:33:59
等级头衔

等級:晓枫资讯-上等兵

在线时间
33 小时

积分成就
威望
0
贡献
430
主题
484
精华
0
金钱
4233
积分
932
注册时间
2022-12-25
最后登录
2023-8-27

发表于 2023-1-29 16:53:02 | 显示全部楼层 |阅读模式

日常开发过程中,尤其在 DDD 过程中,经常遇到 VO/MODEL/PO 等领域模型的相互转换。此时我们会一个字段一个字段进行 set|get 设置。要么使用工具类进行暴力的属性拷贝,在这个暴力属性拷贝过程中好的工具更能提高程序的运行效率,反之引起性能低下、隐藏细节设置 OOM 等极端情况出现。


2 现有技术
  • 直接 set|get 方法:字段少时还好,当字段非常大时工作量巨大,重复操作,费时费力。
  • 通过反射 + 内省的方式实现值映射实现:比如许多开源的 apache-common、spring、hutool 工具类都提供了此种实现工具。这种方法的缺点就是性能低、黑盒属性拷贝。不同工具类的处理又有区别:spring 的属性拷贝会忽略类型转换但不报错、hutool 会自动进行类型转、有些工具设置抛出异常等等。出现生产问题,定位比较困难。
  • mapstruct:使用前需要手动定义转换器接口,根据接口类注解和方法注解自动生成实现类,属性转换逻辑清晰,但是不同的领域对象转换还需要单独写一层转换接口或者添加一个转换方法。

3 扩展设计
3.1 mapstruct 介绍

本扩展组件基于 mapstruct 进行扩展,简单介绍 mapstruct 实现原理。

mapstruct 是基于 JSR 269 实现的,JSR 269 是 JDK 引进的一种规范。有了它,能够实现在编译期处理注解,并且读取、修改和添加抽象语法树中的内容。JSR 269 使用 Annotation Processor 在编译期间处理注解,Annotation Processor 相当于编译器的一种插件,因此又称为插入式注解处理。

我们知道,java 的类加载机制是需要通过编译期运行期。如下图所示

20230128090606056.jpg

mapstruct 正是在上面的编译期编译源码的过程中,通过修改语法树二次生成字节码,如下图所示

20230128090606057.jpg

以上大概可以概括如下几个步骤:

1、生成抽象语法树。Java 编译器对 Java 源码进行编译,生成抽象语法树(Abstract Syntax Tree,AST)。

2、调用实现了 JSR 269 API 的程序。只要程序实现了 JSR 269 API,就会在编译期间调用实现的注解处理器。

3、修改抽象语法树。在实现 JSR 269 API 的程序中,可以修改抽象语法树,插入自己的实现逻辑。

4、生成字节码。修改完抽象语法树后,Java 编译器会生成修改后的抽象语法树对应的字节码文件件。

从 mapstruct 实现原理来看,我们发现 mapstruct 属性转换逻辑清晰,具备良好的扩展性,问题是需要单独写一层转换接口或者添加一个转换方法。能否将转换接口或者方法做到自动扩展呢?


3.2 改进方案

上面所说 mapstruct 方案,有个弊端。就是如果有新的领域模型转换,我们不得不手动写一层转换接口,如果出现 A/B 两个模型互转,一般需定义四个方法:

20230128090606058.jpg

鉴于此,本方案通过将原 mapstruct 定义在转换接口类注解和转换方法的注解,通过映射,形成新包装注解。将此注解直接定义在模型的类或者字段上,然后根据模型上的自定义注解直接编译期生成转换接口,然后 mapstruct 根据自动生成的接口再次生成具体的转换实现类。

注意:自动生成的接口中类和方法的注解为原 mapstruct 的注解,所以 mapstruct 原有功能上没有丢失。详细调整如下图:

20230128090606059.jpg


4 实现
4.1 技术依赖
  • 编译期注解处理器 AbstractProcessor:Annotation Processor 相当于编译器的一种插件,因此又称为插入式注解处理。想要实现 JSR 269,主要有以下几个步骤。

1)继承 AbstractProcessor 类,并且重写 process 方法,在 process 方法中实现自己的注解处理逻辑。

2)在 META-INF/services 目录下创建 javax.annotation.processing.Processor 文件注册自己实现的

  • 谷歌 AutoService:AutoService 是 Google 开源的用来方便生成符合 ServiceLoader 规范的开源库,使用非常的简单。只需要增加注解,便可自动生成规范约束文件。

知识点: 使用 AutoService 的好处是帮助我们不需要手动维护 Annotation Processor 所需要的 META-INF 文件目录和文件内容。它会自动帮我们生产,使用方法也很简单,只需要在自定义的 Annotation Processor 类上加上以下的注解即可 @AutoService (Processor.class)

  • mapstruct:帮助实现自定义插件自动生成的转换接口,并注入到 spring 容器中 (现有方案中已做说明)。
  • javapoet:JavaPoet 是一个动态生成代码的开源库。帮助我们简单快速的生成 java 类文件,期主要特点如下:

JavaPoet 是一款可以自动生成 Java 文件的第三方依赖。

简洁易懂的 API,上手快。

让繁杂、重复的 Java 文件,自动化生成,提高工作效率,简化流程。


4.2 实现步骤
  • 第一步:自动生成转换接口类所需的枚举,分别为类注解 AlpacaMap 和字段注解 AlpacaMapField。

1) AlpacaMap:定义在类上,属性 target 指定所转换目标模型;属性 uses 指定雷专转换过程中所依赖的外部对象。

2)AlpacaMapField:原始 mapstruct 所支持的所有注解做一次别名包装,使用 spring 提供的 AliasFor 注解。

知识点: @AliasFor 是 Spring 框架的一个注解,用于声明注解属性的别名。它有两种不同的应用场景:

注解内的别名

元数据的别名

两者主要的区别在于是否在同一个注解内。

  • 第二步:AlpacaMapMapperDescriptor 实现。此类主要功能是加载使用第一步定义枚举的所有模型类,然后将类的信息和类 Field 信息保存起来方便后面直接使用,片段逻辑如下:

  1. AutoMapFieldDescriptor descriptor = new AutoMapFieldDescriptor();
  2.             descriptor.target = fillString(alpacaMapField.target());
  3.             descriptor.dateFormat = fillString(alpacaMapField.dateFormat());
  4.             descriptor.numberFormat = fillString(alpacaMapField.numberFormat());
  5.             descriptor.constant = fillString(alpacaMapField.constant());
  6.             descriptor.expression = fillString(alpacaMapField.expression());
  7.             descriptor.defaultExpression = fillString(alpacaMapField.defaultExpression());
  8.             descriptor.ignore = alpacaMapField.ignore();
  9.              ..........
复制代码
  • 第三步:AlpacaMapMapperGenerator 类主要是通过 JavaPoet 生成对应的类信息、类注解、类方法以及方法上的注解信息

生成类信息:TypeSpec createTypeSpec(AlpacaMapMapperDescriptor descriptor)

生成类注解信息 AnnotationSpec buildGeneratedMapperConfigAnnotationSpec(AlpacaMapMapperDescriptor descriptor) {

生成类方法信息: MethodSpec buildMappingMethods(AlpacaMapMapperDescriptor descriptor)

生成方法注解信息:List<AnnotationSpec> buildMethodMappingAnnotations(AlpacaMapMapperDescriptor descriptor){

在实现生成类信息过程中,需要指定生成类的接口类 AlpacaBaseAutoAssembler,此类主要定义四个方法如下:

  1. public interface AlpacaBaseAutoAssembler<S,T>{
  2.     T copy(S source);
  3.     default List<T> copyL(List<S> sources){
  4.         return sources.stream().map(c->copy(c)).collect(Collectors.toList());
  5.     }
  6.     @InheritInverseConfiguration(name = "copy")
  7.     S reverseCopy(T source);
  8.     default List<S> reverseCopyL(List<T> sources){
  9.         return sources.stream().map(c->reverseCopy(c)).collect(Collectors.toList());
  10.     }
  11. }
复制代码
  • 第四步:因为生成的类转换器是注入 spring 容器的。所以需要顶一个专门生成 mapstruct 注入 spring 容器的注解,此注解通过类 AlpacaMapSpringConfigGenerator 自动生成,核心代码如下

  1. private AnnotationSpec buildGeneratedMapperConfigAnnotationSpec() {
  2.         return AnnotationSpec.builder(ClassName.get("org.mapstruct", "MapperConfig"))
  3.                 .addMember("componentModel", "$S", "spring")
  4.                 .build();
  5.     }
复制代码
  • 第五步:通过以上步骤,我们定义好了相关类、相关类的方法、相关类的注解、相关类方法的注解。此时将他们串起来通过 Annotation Processor 生成类文件输出,核心方法如下

  1. private void writeAutoMapperClassFile(AlpacaMapMapperDescriptor descriptor){
  2.         System.out.println("开始生成接口:"+descriptor.sourcePackageName() + "."+ descriptor.mapperName());
  3.         try (final Writer outputWriter =
  4.                      processingEnv
  5.                              .getFiler()
  6.                              .createSourceFile(  descriptor.sourcePackageName() + "."+ descriptor.mapperName())
  7.                              .openWriter()) {
  8.             alpacaMapMapperGenerator.write(descriptor, outputWriter);
  9.         } catch (IOException e) {
  10.             processingEnv
  11.                     .getMessager()
  12.                     .printMessage( ERROR,   "Error while opening "+ descriptor.mapperName()  + " output file: " + e.getMessage());
  13.         }
  14.     }
复制代码

知识点: 在 javapoet 中核心类第一大概有一下几个类,可参考如下:

JavaFile 用于构造输出包含一个顶级类的 Java 文件,是对.java 文件的抽象定义

TypeSpec TypeSpec 是类 / 接口 / 枚举的抽象类型

MethodSpec MethodSpec 是方法 / 构造函数的抽象定义

FieldSpec FieldSpec 是成员变量 / 字段的抽象定义

ParameterSpec ParameterSpec 用于创建方法参数

AnnotationSpec AnnotationSpec 用于创建标记注解


5 实践

下面举例说明如何使用,在这里我们定义一个模型 Person 和模型 Student,其中涉及字段转换的普通字符串、枚举、时间格式化和复杂的类型换砖,具体运用如下步骤。


5.1 引入依赖

代码已上传代码库,如需特定需求可重新拉去分支打包使用

  1. <dependency>
  2.             <groupId>com.jdl</groupId>
  3.             <artifactId>alpaca-mapstruct-processor</artifactId>
  4.             <version>1.1-SNAPSHOT</version>
  5.         </dependency>
复制代码
5.2 对象定义

uses 方法必须为正常的 spring 容器中的 bean,此 bean 提供 @Named 注解的方法可供类字段注解 AlpacaMapField 中的 qualifiedByName 属性以字符串的方式指定,如下图所示

  1. @Data
  2. @AlpacaMap(targetType = Student.class,uses = {Person.class})
  3. @Service
  4. public class Person {
  5.     private String make;
  6.     private SexType type;
  7.     @AlpacaMapField(target = "age")
  8.     private Integer sax;
  9.     @AlpacaMapField(target="dateStr" ,dateFormat = "yyyy-MM-dd")
  10.     private Date date;
  11.     @AlpacaMapField(target = "brandTypeName",qualifiedByName ="convertBrandTypeName")
  12.     private Integer brandType;
  13.     @Named("convertBrandTypeName")
  14.     public  String convertBrandTypeName(Integer brandType){
  15.         return BrandTypeEnum.getDescByValue(brandType);
  16.     }
  17.     @Named("convertBrandTypeName")
  18.     public  Integer convertBrandType(String brandTypeName){
  19.         return BrandTypeEnum.getValueByDesc(brandTypeName);
  20.     }
  21. }
复制代码
5.3 生成结果

使用 maven 打包或者编译后观察,此时在 target/generated-source/annotatins 目录中生成两个文件 PersonToStudentAssembler 和 PersonToStudentAssemblerImpl

类文件 PersonToStudentAssembler 是由自定义注解器自动生成,内容如下

  1. @Mapper(
  2.     config = AutoMapSpringConfig.class,
  3.     uses = {Person.class}
  4. )
  5. public interface PersonToStudentAssembler extends AlpacaBaseAutoAssembler&lt;Person, Student&gt; {
  6.   @Override
  7.   @Mapping(
  8.       target = "age",
  9.       source = "sax",
  10.       ignore = false
  11.   )
  12.   @Mapping(
  13.       target = "dateStr",
  14.       dateFormat = "yyyy-MM-dd",
  15.       source = "date",
  16.       ignore = false
  17.   )
  18.   @Mapping(
  19.       target = "brandTypeName",
  20.       source = "brandType",
  21.       ignore = false,
  22.       qualifiedByName = "convertBrandTypeName"
  23.   )
  24.   Student copy(final Person source);
  25. }
复制代码

PersonToStudentAssemblerImpl 是 mapstruct 根据 PersonToStudentAssembler 接口注解器自动生成,内容如下

  1. @Component
  2. public class PersonToStudentAssemblerImpl implements PersonToStudentAssembler {
  3.     @Autowired
  4.     private Person person;
  5.     @Override
  6.     public Person reverseCopy(Student arg0) {
  7.         if ( arg0 == null ) {
  8.             return null;
  9.         }
  10.         Person person = new Person();
  11.         person.setSax( arg0.getAge() );
  12.         try {
  13.             if ( arg0.getDateStr() != null ) {
  14.                 person.setDate( new SimpleDateFormat( "yyyy-MM-dd" ).parse( arg0.getDateStr() ) );
  15.             }
  16.         } catch ( ParseException e ) {
  17.             throw new RuntimeException( e );
  18.         }
  19.         person.setBrandType( person.convertBrandType( arg0.getBrandTypeName() ) );
  20.         person.setMake( arg0.getMake() );
  21.         person.setType( arg0.getType() );
  22.         return person;
  23.     }
  24.     @Override
  25.     public Student copy(Person source) {
  26.         if ( source == null ) {
  27.             return null;
  28.         }
  29.         Student student = new Student();
  30.         student.setAge( source.getSax() );
  31.         if ( source.getDate() != null ) {
  32.             student.setDateStr( new SimpleDateFormat( "yyyy-MM-dd" ).format( source.getDate() ) );
  33.         }
  34.         student.setBrandTypeName( person.convertBrandTypeName( source.getBrandType() ) );
  35.         student.setMake( source.getMake() );
  36.         student.setType( source.getType() );
  37.         return student;
  38.     }
  39. }
复制代码
5.4 Spring 容器引用

此时在我们的 spring 容器中可直接 @Autowired 引入接口 PersonToStudentAssembler 实例进行四种维护数据相互转换

  1. AnnotationConfigApplicationContext applicationContext = new  AnnotationConfigApplicationContext();
  2.         applicationContext.scan("com.jdl.alpaca.mapstruct");
  3.         applicationContext.refresh();
  4.         PersonToStudentAssembler personToStudentAssembler = applicationContext.getBean(PersonToStudentAssembler.class);
  5.         Person person = new Person();
  6.         person.setMake("make");
  7.         person.setType(SexType.BOY);
  8.         person.setSax(100);
  9.         person.setDate(new Date());
  10.         person.setBrandType(1);
  11.         Student student = personToStudentAssembler.copy(person);
  12.         System.out.println(student);
  13.         System.out.println(personToStudentAssembler.reverseCopy(student));
  14.         List<Person> personList = Lists.newArrayList();
  15.         personList.add(person);
  16.         System.out.println(personToStudentAssembler.copyL(personList));
  17.         System.out.println(personToStudentAssembler.reverseCopyL(personToStudentAssembler.copyL(personList)));
复制代码

控制台打印:

personToStudentStudent(make=make, type=BOY, age=100, dateStr=2022-11-09, brandTypeName=集团KA)
studentToPersonPerson(make=make, type=BOY, sax=100, date=Wed Nov 09 00:00:00 CST 2022, brandType=1)
personListToStudentList[Student(make=make, type=BOY, age=100, dateStr=2022-11-09, brandTypeName=集团KA)]
studentListToPersonList[Person(make=make, type=BOY, sax=100, date=Wed Nov 09 00:00:00 CST 2022, brandType=1)]

注意:

  • qualifiedByName 注解属性使用不太友好,如果使用到此属性时,需要定义反转类型转换函数。因为在前面我们定义的抽象接口 AlpacaBaseAutoAssembler 有如下图一个注解,从目的对象到源对象的反转映射,因为 java 的重载性,同名不同参非同一个方法,所以在 S 转 T 的时候回找不到此方法。故需要自行定义好转换函数

  1. @InheritInverseConfiguration(name = "copy")
复制代码
比如从 S 转换 T 会使用第一个方法,从 T 转 S 的时候必须定义一个同名 Named 注解的方法,方法参数和前面方法是入参变出参、出参变入参。




  1. @Named("convertBrandTypeName")
  2.     public  String convertBrandTypeName(Integer brandType){
  3.         return BrandTypeEnum.getDescByValue(brandType);
  4.     }
  5.     @Named("convertBrandTypeName")
  6.     public  Integer convertBrandType(String brandTypeName){
  7.         return BrandTypeEnum.getValueByDesc(brandTypeName);
  8.     }
复制代码
  • 在使用 qualifiedByName 注解时,指定的 Named 注解方法必须定义为 spring 容器可管理的对象,并需要通过模型类注解属性 used 引入此对象 Class

知识点:

InheritInverseConfiguration 功能很强大,可以逆向映射,从上面 PersonToStudentAssemblerImpl 看到上面属性 sax 可以正映射到 sex,逆映射可自动从 sex 映射到 sax。但是正映射的 @Mapping#expression、#defaultExpression、#defaultValue 和 #constant 会被逆映射忽略。此外某个字段的逆映射可以被 ignore,expression 或 constant 覆盖.



晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。

  离线 

TA的专栏

  • 打卡等级:即来则安
  • 打卡总天数:23
  • 打卡月天数:0
  • 打卡总奖励:314
  • 最近打卡:2025-04-05 09:18:29
等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
357
积分
54
注册时间
2023-1-13
最后登录
2025-4-5

发表于 2023-1-29 16:54:20 | 显示全部楼层
感谢分享~~~~学习学习~~~~~
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
16
积分
12
注册时间
2022-12-27
最后登录
2022-12-27

发表于 2023-2-10 15:27:37 | 显示全部楼层
感谢大大分享~~~~~~~~
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
21
积分
22
注册时间
2022-12-25
最后登录
2022-12-25

发表于 2025-1-19 12:04:03 | 显示全部楼层
顶顶更健康!!!
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
20
积分
20
注册时间
2022-12-29
最后登录
2022-12-29

发表于 2025-7-1 00:01:01 | 显示全部楼层
感谢楼主分享。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1楼
2楼
3楼
4楼
5楼

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表