作者:京东物流 王北永 姚再毅

1 布景

日常开发进程中,尤其在 DDD 进程中,经常遇到 VO/MODEL/PO 等领域模型的相互转化。此时咱们会一个字段一个字段进行 set|get 设置。要么运用东西类进行暴力的特点复制,在这个暴力特点复制进程中好的东西更能提高程序的运行效率,反之引起性能低下、躲藏细节设置 OOM 等极端情况呈现。

2 现有技能

  1. 直接 set|get 办法:字段少时还好,当字段非常大时工作量巨大,重复操作,费时费力。
  1. 经过反射 + 内省的办法完成值映射完成:比如许多开源的 apache-common、spring、hutool 东西类都供给了此种完成东西。这种办法的缺点便是性能低、黑盒特点复制。不同东西类的处理又有差异:spring 的特点复制会疏忽类型转化但不报错、hutool 会主动进行类型转、有些东西设置抛出异常等等。呈现出产问题,定位比较困难。
  1. mapstruct:运用前需求手动界说转化器接口,依据接口类注解和办法注解主动生成完成类,特点转化逻辑明晰,可是不同的领域目标转化还需求独自写一层转化接口或许增加一个转化办法。

3 扩展规划

3.1 mapstruct 介绍

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

mapstruct 是基于 JSR 269 完成的,JSR 269 是 JDK 引进的一种标准。有了它,能够完成在编译期处理注解,并且读取、修正和增加笼统语法树中的内容。JSR 269 运用 Annotation Processor 在编译期间处理注解,Annotation Processor 相当于编译器的一种插件,因此又称为刺进式注解处理。

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

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

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

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

以上大约能够概括如下几个过程:

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 两个模型互转,一般需界说四个办法:

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

鉴于此,本方案经过将原 mapstruct 界说在转化接口类注解和转化办法的注解,经过映射,构成新包装注解。将此注解直接界说在模型的类或许字段上,然后依据模型上的自界说注解直接编译期生成转化接口,然后 mapstruct 依据主动生成的接口再次生成具体的转化完成类。

留意:主动生成的接口中类和办法的注解为原 mapstruct 的注解,所以 mapstruct 原有功用上没有丢掉。详细调整如下图:

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

4 完成

4.1 技能依靠

  1. 编译期注解处理器 AbstractProcessor:Annotation Processor 相当于编译器的一种插件,因此又称为刺进式注解处理。想要完成 JSR 269,首要有以下几个过程。

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

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

  1. 谷歌 AutoService:AutoService 是 Google 开源的用来便利生成符合 ServiceLoader 标准的开源库,运用非常的简单。只需求增加注解,便可主动生成标准束缚文件。

知识点: 运用 AutoService 的优点是协助咱们不需求手动维护 Annotation Processor 所需求的 META-INF 文件目录和文件内容。它会主动帮咱们出产,运用办法也很简单,只需求在自界说的 Annotation Processor 类上加上以下注解即可 @AutoService (Processor.class)

  1. mapstruct:协助完成自界说插件主动生成的转化接口,并注入到 spring 容器中 (现有方案中已做说明)。
  2. javapoet:JavaPoet 是一个动态生成代码的开源库。协助咱们简单快速的生成 java 类文件,期首要特点如下:

1) JavaPoet 是一款能够主动生成 Java 文件的第三方依靠。

2) 简练易懂的 API,上手快。

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

4.2 完成过程

  • 第一步:主动生成转化接口类所需的枚举,分别为类注解 AlpacaMap 和字段注解 AlpacaMapField。

1) AlpacaMap:界说在类上,特点 target 指定所转化目标模型;特点 uses 指定雷专转化进程中所依靠的外部目标。

2)AlpacaMapField:原始 mapstruct 所支持的一切注解做一次别号包装,运用 spring 供给的 AliasFor 注解。

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

注解内的别号

元数据的别号

两者首要的差异在于是否在同一个注解内。

  • 第二步:AlpacaMapMapperDescriptor 完成。此类首要功用是加载运用第一步界说枚举的一切模型类,然后将类的信息和类 Field 信息保存起来便利后面直接运用,片段逻辑如下:
AutoMapFieldDescriptor descriptor = new AutoMapFieldDescriptor();
            descriptor.target = fillString(alpacaMapField.target());
            descriptor.dateFormat = fillString(alpacaMapField.dateFormat());
            descriptor.numberFormat = fillString(alpacaMapField.numberFormat());
            descriptor.constant = fillString(alpacaMapField.constant());
            descriptor.expression = fillString(alpacaMapField.expression());
            descriptor.defaultExpression = fillString(alpacaMapField.defaultExpression());
            descriptor.ignore = alpacaMapField.ignore();
             ..........
  • 第三步:AlpacaMapMapperGenerator 类首要是经过 JavaPoet 生成对应的类信息、类注解、类办法以及办法上的注解信息
生成类信息:TypeSpec createTypeSpec(AlpacaMapMapperDescriptor descriptor)
生成类注解信息 AnnotationSpec buildGeneratedMapperConfigAnnotationSpec(AlpacaMapMapperDescriptor descriptor) {
生成类办法信息: MethodSpec buildMappingMethods(AlpacaMapMapperDescriptor descriptor)
生成办法注解信息:List<AnnotationSpec> buildMethodMappingAnnotations(AlpacaMapMapperDescriptor descriptor){

在完成生成类信息进程中,需求指定生成类的接口类 AlpacaBaseAutoAssembler,此类首要界说四个办法如下:

public interface AlpacaBaseAutoAssembler<S,T>{
    T copy(S source);
    default List<T> copyL(List<S> sources){
        return sources.stream().map(c->copy(c)).collect(Collectors.toList());
    }
    @InheritInverseConfiguration(name = "copy")
    S reverseCopy(T source);
    default List<S> reverseCopyL(List<T> sources){
        return sources.stream().map(c->reverseCopy(c)).collect(Collectors.toList());
    }
}
  • 第四步:由于生成的类转化器是注入 spring 容器的。所以需求顶一个专门生成 mapstruct 注入 spring 容器的注解,此注解经过类 AlpacaMapSpringConfigGenerator 主动生成,核心代码如下
private AnnotationSpec buildGeneratedMapperConfigAnnotationSpec() {
        return AnnotationSpec.builder(ClassName.get("org.mapstruct", "MapperConfig"))
                .addMember("componentModel", "$S", "spring")
                .build();
    }
  • 第五步:经过以上过程,咱们界说好了相关类、相关类的办法、相关类的注解、相关类办法的注解。此时将他们串起来经过 Annotation Processor 生成类文件输出,核心办法如下
private void writeAutoMapperClassFile(AlpacaMapMapperDescriptor descriptor){
        System.out.println("开始生成接口:"+descriptor.sourcePackageName() + "."+ descriptor.mapperName());
        try (final Writer outputWriter =
                     processingEnv
                             .getFiler()
                             .createSourceFile(  descriptor.sourcePackageName() + "."+ descriptor.mapperName())
                             .openWriter()) {
            alpacaMapMapperGenerator.write(descriptor, outputWriter);
        } catch (IOException e) {
            processingEnv
                    .getMessager()
                    .printMessage( ERROR,   "Error while opening "+ descriptor.mapperName()  + " output file: " + e.getMessage());
        }
    }

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

JavaFile 用于结构输出包含一个尖端类的 Java 文件,是对.java 文件的笼统界说

TypeSpec TypeSpec 是类 / 接口 / 枚举的笼统类型

MethodSpec MethodSpec 是办法 / 结构函数的笼统界说

FieldSpec FieldSpec 是成员变量 / 字段的笼统界说

ParameterSpec ParameterSpec 用于创建办法参数

AnnotationSpec AnnotationSpec 用于创建标记注解

5 实践

下面举例说明如何运用,在这里咱们界说一个模型 Person 和模型 Student,其中触及字段转化的普通字符串、枚举、时刻格式化和杂乱的类型换砖,具体运用如下过程。

5.1 引进依靠

<dependency>
            <groupId>com.jdl</groupId>
            <artifactId>alpaca-mapstruct-processor</artifactId>
            <version>1.1-SNAPSHOT</version>
        </dependency>

5.2 目标界说

uses 办法有必要为正常的 spring 容器中的 bean,此 bean 供给 @Named 注解的办法可供类字段注解 AlpacaMapField 中的 qualifiedByName 特点以字符串的办法指定,如下图所示

@Data
@AlpacaMap(targetType = Student.class,uses = {Person.class})
@Service
public class Person {
    private String make;
    private SexType type;
    @AlpacaMapField(target = "age")
    private Integer sax;
    @AlpacaMapField(target="dateStr" ,dateFormat = "yyyy-MM-dd")
    private Date date;
    @AlpacaMapField(target = "brandTypeName",qualifiedByName ="convertBrandTypeName")
    private Integer brandType;
    @Named("convertBrandTypeName")
    public  String convertBrandTypeName(Integer brandType){
        return BrandTypeEnum.getDescByValue(brandType);
    }
    @Named("convertBrandTypeName")
    public  Integer convertBrandType(String brandTypeName){
        return BrandTypeEnum.getValueByDesc(brandTypeName);
    }
}

5.3 生成成果

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

类文件 PersonToStudentAssembler 是由自界说注解器主动生成,内容如下

@Mapper(
    config = AutoMapSpringConfig.class,
    uses = {Person.class}
)
public interface PersonToStudentAssembler extends AlpacaBaseAutoAssembler<Person, Student> {
  @Override
  @Mapping(
      target = "age",
      source = "sax",
      ignore = false
  )
  @Mapping(
      target = "dateStr",
      dateFormat = "yyyy-MM-dd",
      source = "date",
      ignore = false
  )
  @Mapping(
      target = "brandTypeName",
      source = "brandType",
      ignore = false,
      qualifiedByName = "convertBrandTypeName"
  )
  Student copy(final Person source);
}

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

@Component
public class PersonToStudentAssemblerImpl implements PersonToStudentAssembler {
    @Autowired
    private Person person;
    @Override
    public Person reverseCopy(Student arg0) {
        if ( arg0 == null ) {
            return null;
        }
        Person person = new Person();
        person.setSax( arg0.getAge() );
        try {
            if ( arg0.getDateStr() != null ) {
                person.setDate( new SimpleDateFormat( "yyyy-MM-dd" ).parse( arg0.getDateStr() ) );
            }
        } catch ( ParseException e ) {
            throw new RuntimeException( e );
        }
        person.setBrandType( person.convertBrandType( arg0.getBrandTypeName() ) );
        person.setMake( arg0.getMake() );
        person.setType( arg0.getType() );
        return person;
    }
    @Override
    public Student copy(Person source) {
        if ( source == null ) {
            return null;
        }
        Student student = new Student();
        student.setAge( source.getSax() );
        if ( source.getDate() != null ) {
            student.setDateStr( new SimpleDateFormat( "yyyy-MM-dd" ).format( source.getDate() ) );
        }
        student.setBrandTypeName( person.convertBrandTypeName( source.getBrandType() ) );
        student.setMake( source.getMake() );
        student.setType( source.getType() );
        return student;
    }
}

5.4 Spring 容器引用

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

AnnotationConfigApplicationContext applicationContext = new  AnnotationConfigApplicationContext();
        applicationContext.scan("com.jdl.alpaca.mapstruct");
        applicationContext.refresh();
        PersonToStudentAssembler personToStudentAssembler = applicationContext.getBean(PersonToStudentAssembler.class);
        Person person = new Person();
        person.setMake("make");
        person.setType(SexType.BOY);
        person.setSax(100);
        person.setDate(new Date());
        person.setBrandType(1);
        Student student = personToStudentAssembler.copy(person);
        System.out.println(student);
        System.out.println(personToStudentAssembler.reverseCopy(student));
        List<Person> personList = Lists.newArrayList();
        personList.add(person);
        System.out.println(personToStudentAssembler.copyL(personList));
        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 的时分回找不到此办法。故需求自行界说好转化函数
@InheritInverseConfiguration(name = "copy")

比如从 S 转化 T 会运用第一个办法,从 T 转 S 的时分有必要界说一个同名 Named 注解的办法,办法参数和前面办法是入参变出参、出参变入参。

@Named("convertBrandTypeName")
    public  String convertBrandTypeName(Integer brandType){
        return BrandTypeEnum.getDescByValue(brandType);
    }
    @Named("convertBrandTypeName")
    public  Integer convertBrandType(String brandTypeName){
        return BrandTypeEnum.getValueByDesc(brandTypeName);
    }
  • 在运用 qualifiedByName 注解时,指定的 Named 注解办法有必要界说为 spring 容器可办理的目标,并需求经过模型类注解特点 used 引进此目标 Class

知识点:

InheritInverseConfiguration 功用很强大,能够逆向映射,从上面 PersonToStudentAssemblerImpl 看到上面特点 sax 能够正映射到 sex,逆映射可主动从 sex 映射到 sax。可是正映射的 @Mapping#expression、#defaultExpression、#defaultValue 和 #constant 会被逆映射疏忽。此外某个字段的逆映射能够被 ignore,expression 或 constant 覆盖

6 结束语

参阅文档:

github.com/google/auto…

mapstruct.org/

github.com/square/java…