Mapstruct核心原理介绍

分享完希望能知道

  • Java Annotation Processing以及使用方式
  • 如何debug类似Mapstruct编译期生成代码的框架
  • Mapstruct的生成代码的过程
  • Javapoet框架

Mapstruct介绍

官网介绍:

MapStruct is a code generator that greatly simplifies the implementation of mappings between Java bean types based on a convention over configuration approach. The generated mapping code uses plain method invocations and thus is fast, type-safe and easy to understand.

翻译一下,就是Mapstruct是一个通过配置的方式来极大的简化Java对象之间转换的代码生成器。生成的转换代码使用的普通的方法调用,所以性能高、类型安全并且转换代码可读性高。

Mapstruct和BeanUtils的区别

Mapstruct:编译期生成转换代码,后续转换对象时,相当于调用转换代码的普通方法

BeanUtils:在运行时通过反射,对字段进行赋值

优缺点

Mapstruct

优点:

  • 性能高
  • 编译后可以查看转换代码,方便排查问题
  • 转换灵活
  • 转换错误,在编译的时候就可以发现

缺点:

  • 有一定的上手成本

  • 需要定义convert类

BeanUtils

优点:

  • 使用简单,不需要创建convert类,做一些配置等等

缺点:

  • 转换逻辑不灵活,针对一些不同名的字段,转换不方便
  • 转换代码盲盒,不知道转换的逻辑是什么样的
  • 因为转换用的是反射,性能相比于Mapstruct是比较低的(因为jvm优化在这种场景下有可能无效,所以在对性能要求很高或者经常被调用的程序中,尽量不要使用。)
数据量 Apache BeanUtils Spring BeanUtils Mapstruct
10 2ms 1ms 1ms
100 5ms 3ms 1ms
1k 15ms 6ms 1ms
1w 30ms 19ms 2ms

Mapstruct源码

Java Annotation Processing

是什么

用于在编译期扫描关心的注解,再通过对应的processor处理逻辑,可以在编译期改动/新增java源码文件或者生成一些别的元素,比如其他类型的文件、注释等等,像Lombok、Mapstruct、AutoService都是使用了这个技术。平时我们大部分用到的都是运行时注解,比如@Service、@Autowired等等。后面方便称呼,统一叫APT(Annotation Processing Tool)。

ps:AutoService是google的一个自动生成Spi实现类的工具,不需要在META-INF/services中做配置。

APT关键的组件介绍

AbstractProcessor

java 复制代码
package com.example;
// @SupportedAnnotationTypes("org.mapstruct.Mapper")
// @SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment env){ }

    // 实现自己的逻辑
    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }

    // 标明这个处理器用来处理哪些注解,也可以通过注解来设置@SupportedAnnotationTypes("org.mapstruct.Mapper")
    @Override
    public Set<String> getSupportedAnnotationTypes() { }

    // 支持的JDK版本,也可以通过注解来设置,@SupportedSourceVersion(SourceVersion.RELEASE_8)
    @Override
    public SourceVersion getSupportedSourceVersion() { }

}

如果自定义一个编译期注解,并且需要实现这个注解处理逻辑,那么需要新建一个Processor继承AbstractProcessor,这个AbstractProcessor用到了模板方法的设计模式。

Element

Element: 一种元素的抽象,包、类、接口、方法、静态方法块、方法参数等都可以叫做Element TypeElement: 类、接口或者是方法的参数 PackageElement: 包 ExecutableElement: 方法、方法块、静态方法块 VariableElement: 类的字段

TypeParameterElement: 对应类、接口、方法或构造函数的类型参数,也就是定义泛型时的E、T之类的泛型通配符

TypeMirror

用来表明一个元素的类型,是一个接口,Element.asType()返回的对象,TypeMirror下有很多子类,比如ExecutableType(方法)、DeclaredType(类/接口)等等

AnnotationMirror

用来表明一个注解,可以获取注解上的值等信息。

RoundEnvironment

APT会多轮进行处理插入式注解的处理,每一轮会生成一个新的RoundEnvironment对象,根据这个可以知道该注解对应的处理类是否都处理完成、获取注解对应的Element

ProcessingEnvironment

获取一些中间的工具类

Filer

通过这个接口,可以在编译期创建/获取相关资源,比如.java、.class、.properties等文件。

Messager

相当于日志打印,有ERROR、WARNING、NOTE等

用法示例

java 复制代码
processingEnv.getMessager().printMessage(
                        Kind.NOTE, "MapStruct: referred types not available (yet), deferring mapper: "
                            + mapperElement );

Mapstruct一些类介绍

MappingProcessor

继承了AbstractProcessor,是Mapstruct处理的入口

ModelElementProcessor

Mapstruct生成代码的核心处理器,MappingProcessor中使用ModelElementProcessor来组装Freemarker模板需要的元数据、生成文件等动作。

ModelElementProcessor使用策略模式+责任链模式+模板模式来处理逻辑 ModelElementProcessor使用SPI方式加载 具体运行时的顺序,按照优先级运行,值越小优先级越高

Processor名称 优先级
MethodRetrievalProcessor 1
MapperCreationProcessor 1000
AnnotationBasedComponentModelProcessor 1100
MapperRenderingProcessor 9999
MapperServiceProcessor 10000
为什么这个值是跳跃式的,猜测是为了在中间可以让用户自定义策略。

Mapper

用来表示生成Mapper的元数据,存储了生成Mapper需要的各种元数据,渲染模板需要从Mapper对象里去取。

怎么debug Mapstruct源码

  1. 配置debug信息 端口配置8000
  2. 进入项目根目录,执行mvnDebug compile
  3. 在MappingProcessor中的process方法处打断点
  4. debug模式运行即可进入断点处

Mapstruct处理流程

从0到1实现一个简易版的mapstruct

背景

通过@MyMapper、@MyMapping注解实现类似于Mapstruct的转换。

准备工作

了解JavaPoet

github: github.com/square/java...

JavaPoet is a Java API for generating .java source files. Source file generation can be useful when doing things such as annotation processing or interacting with metadata files (e.g., database schemas, protocol formats). By generating code, you eliminate the need to write boilerplate while also keeping a single source of truth for the metadata.

简单概括一下:用于生成Java源文件的工具包。

如果正常生成Java源码文件,会比较麻烦,因为需要你做各种字符串的拼接,但是用这个可以通过调用它的API来生成响应代码,可读性和易用性大大提升。

我们需要用他来给我们生成源码文件。

新建一个项目

annotation-processing:处理我们的注解逻辑 yard:引用annotation-processing模块

maven插件配置

java 复制代码
<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
                <executions>
                    <execution>
                        <id>default-compile</id>
                        <configuration>
                            <compilerArgument>-proc:none</compilerArgument>
                        </configuration>
                    </execution>
                    <execution>
                        <id>compile-project</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

ps: 注意-proc:none,只有加上这个配置才能够运行,不然会报错。

创建注解

java 复制代码
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.TYPE })
public @interface MyMapper {
}
java 复制代码
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.METHOD })
public @interface MyMapping {
    String target();

    String source();
}

实现AbstractProcessor

java 复制代码
@SupportedAnnotationTypes("org.example.mymapstruct.MyMapper")
public class MyMapstructProcessor extends AbstractProcessor {
    Messager messager;
    ProcessingEnvironment processingEnvironment;
    Elements elementUtils;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        Messager messager = processingEnv.getMessager();
        this.messager = messager;
        this.processingEnvironment = processingEnv;
        this.elementUtils = processingEnv.getElementUtils();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (TypeElement annotatedElement : annotations) {
            messager.printMessage(Diagnostic.Kind.NOTE, annotatedElement.getSimpleName().toString());

            Set<? extends Element> annotatedMappers = roundEnv.getElementsAnnotatedWith(annotatedElement);
            for (Element annotatedMapper : annotatedMappers) {
                TypeElement mapperTypeElement = TypeElement.class.cast(annotatedMapper);
                List<MappingMethod> mappingMethodList = listAllNeedConvertMethodElement(mapperTypeElement);

                // 生成Mapper实现类
                generateMapperImpl(mapperTypeElement, mappingMethodList);
            }
        }
        return true;
    }

    private void generateMapperImpl(TypeElement mapperTypeElement, List<MappingMethod> mappingMethodList) {

        PackageElement interfacePackageElement = elementUtils.getPackageOf(mapperTypeElement);
        String interfacePackageName = interfacePackageElement.toString();
        String interfaceName = mapperTypeElement.getSimpleName().toString();

        String implClassName = mapperTypeElement.getSimpleName().toString() + "Impl";
        if (mappingMethodList == null || mappingMethodList.size() == 0) {
            return;
        }

        List<MethodSpec> methodSpecList = mappingMethodList.stream().map(mappingMethod -> {
            int lastOutputParamDotIndex = mappingMethod.getOutputParameter().getParameterTypeName().lastIndexOf(".");

            String outputParameterPackageName = mappingMethod.getOutputParameter().getParameterTypeName().substring(0, lastOutputParamDotIndex);

            String outputParameterClassName = mappingMethod.getOutputParameter().getParameterTypeName().substring(lastOutputParamDotIndex+1);

            ClassName returnClassName = ClassName.get(outputParameterPackageName, outputParameterClassName);

            String inputParamPackageName = mappingMethod.getInputParameter().getParameterTypeName().substring(0, mappingMethod.getInputParameter().getParameterTypeName().lastIndexOf("."));
            String inputParamClassName = mappingMethod.getInputParameter().getParameterTypeName().substring(mappingMethod.getInputParameter().getParameterTypeName().lastIndexOf(".") + 1);

            ClassName inputClass = ClassName.get(inputParamPackageName, inputParamClassName);

            StringBuilder setResult = new StringBuilder();
            for (MappingMethod.MappingItem mappingItem : mappingMethod.getMappingItemList()) {
                setResult.append(String.format("result.set%s(%s.get%s());\n", StringUtils.capitalize(mappingItem.getTarget()), mappingMethod.getInputParameter().getName(), StringUtils.capitalize(mappingItem.getSource())));
            }
            MethodSpec methodSpec = MethodSpec.methodBuilder(mappingMethod.getMethodName())
                    .addParameter(inputClass, mappingMethod.getInputParameter().getName())
                    .addModifiers(Modifier.PUBLIC)
                    .returns(returnClassName)
                    .beginControlFlow("if ($N == null)", mappingMethod.getInputParameter().getName())
                    .addStatement("return null")
                    .endControlFlow()
                    .addStatement("$T result = new $T();", returnClassName, returnClassName)
                    .addCode(setResult.toString())
                    .addStatement("return result")
                    .build();
            return methodSpec;
        }).collect(Collectors.toList());

        ClassName interfaceClass = ClassName.get(interfacePackageName, interfaceName);

        TypeSpec generatedClass = TypeSpec.classBuilder(implClassName)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addSuperinterface(interfaceClass)
                .addMethods(methodSpecList)
                .build();

        JavaFile javaFile = JavaFile.builder(interfacePackageName, generatedClass)
                .build();
        try {
            javaFile.writeTo(processingEnv.getFiler());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private List<MappingMethod> listAllNeedConvertMethodElement(TypeElement typeElement) {
        List<MappingMethod> result = new ArrayList<>();
        List<? extends Element> enclosedElements = typeElement.getEnclosedElements();
        for (Element element : enclosedElements) {
            if (!element.getKind().equals(ElementKind.METHOD)) {
                continue;
            }
            ExecutableElement methodElement = ExecutableElement.class.cast(element);
            List<? extends VariableElement> parameterElementList = methodElement.getParameters();

            MyMapping[] annotationsByType = methodElement.getAnnotationsByType(MyMapping.class);
            MappingMethod mappingMethod = new MappingMethod();
            mappingMethod.setMethodName(methodElement.getSimpleName().toString());
            Parameter inputParameter = new Parameter();
            inputParameter.setName(parameterElementList.get(0).getSimpleName().toString());
            inputParameter.setParameterTypeName(parameterElementList.get(0).asType().toString());
            mappingMethod.setInputParameter(inputParameter);


            Parameter outputParameter = new Parameter();
            outputParameter.setName(parameterElementList.get(0).getSimpleName().toString());
            outputParameter.setParameterTypeName(methodElement.getReturnType().toString());
            mappingMethod.setOutputParameter(outputParameter);
            if (annotationsByType.length == 0) {
                mappingMethod.setNeedConvert(false);
            } else {
                List<MappingMethod.MappingItem> mappingItemList = convert2MappingItemList(annotationsByType);
                mappingMethod.setMappingItemList(mappingItemList);
                mappingMethod.setNeedConvert(true);
            }

            result.add(mappingMethod);
        }
        return result;
    }

    private List<MappingMethod.MappingItem> convert2MappingItemList(MyMapping[] annotationsByType) {
        return Arrays.stream(annotationsByType).map(myMappingAnno -> {
            MappingMethod.MappingItem mappingItem = new MappingMethod.MappingItem();
            mappingItem.setTarget(myMappingAnno.target());
            mappingItem.setSource(myMappingAnno.source());
            return mappingItem;
        }).collect(Collectors.toList());
    }
}

SPI配置

有两种方式,第一种:META-INF/services方式,第二种:google的AutoService。

第一种:

在annotation-processing模块下

第二种:

先引入google AutoService jar包

java 复制代码
    <dependency>
      <groupId>com.google.auto.service</groupId>
      <artifactId>auto-service</artifactId>
      <version>1.1.1</version>
    </dependency>

然后在对应的processor上,加上注解

打包

mvn clean package或者mvn clean installl

引入

yard模块引入annotation-processing jar包

总结

从Mapstruct的源码学习中我们能学到的是:

  • 责任链+策略模式+模板方法的使用
  • 生成模板类代码的处理方式
  • SPI的使用
  • 框架的生命周期的各个阶段都应该支持钩子函数
  • APT中对Java相关元素的抽象(一个类的所有东西都可以抽象成Element)
相关推荐
骆晨学长26 分钟前
基于springboot的智慧社区微信小程序
java·数据库·spring boot·后端·微信小程序·小程序
AskHarries31 分钟前
利用反射实现动态代理
java·后端·reflect
Flying_Fish_roe1 小时前
Spring Boot-Session管理问题
java·spring boot·后端
hai405872 小时前
Spring Boot中的响应与分层解耦架构
spring boot·后端·架构
Adolf_19933 小时前
Flask-JWT-Extended登录验证, 不用自定义
后端·python·flask
叫我:松哥3 小时前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap
海里真的有鱼3 小时前
Spring Boot 项目中整合 RabbitMQ,使用死信队列(Dead Letter Exchange, DLX)实现延迟队列功能
开发语言·后端·rabbitmq
工业甲酰苯胺3 小时前
Spring Boot 整合 MyBatis 的详细步骤(两种方式)
spring boot·后端·mybatis
新知图书4 小时前
Rust编程的作用域与所有权
开发语言·后端·rust
wn5315 小时前
【Go - 类型断言】
服务器·开发语言·后端·golang