Mapstruct源码解析- 框架实现原理

8,046 阅读5分钟

       只有用过Mapstruct才知道它是有多么的好用与顺手。本篇主要讲述Mapstuct的实现原理,它是怎么去生成转换代码的过程,让大家对这个框架的实现原理有个比较透彻的了解。

1. Java动态编译与JSR 269

       首先,我们先重温下java的编译过程:Java源代码-->编译器-->jvm可执行的Java字节码(即虚拟指令)-->jvm-->jvm中解释器-->机器可执行的二进制机器码-->程序。其实java编译器提供了一套完整的api,我们使用接口可以方便地进行动态编译。下面是一个简单的从源代码文件到生成执行文件的完整生成过程。

//创建源文件
String currentDir = System.getProperty("user.dir");
String src = "package com.seewo.phoenix ;"
        + "public class TestCompiler {"
        + "    public void disply() {"
        + "    System.out.println(\"Hello\");"
        + "}}";

String filename = currentDir + "/src/main/java/com/seewo/phoenix/TestCompiler.java";
File file = new File(filename);

File fileParent = file.getParentFile();

if (!fileParent.exists()) {
    fileParent.mkdir();
}

if (!file.exists()) {
    file.createNewFile();
}

FileWriter fw = new FileWriter(file);
fw.write(src);
fw.flush();
fw.close();

// 使用JavaCompiler 编译java文件
JavaCompiler jc = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = jc.getStandardFileManager(null, null, null);
Iterable fileObjects = fileManager.getJavaFileObjects(filename);
CompilationTask cTask = jc.getTask(null, fileManager, null, null, null, fileObjects);
cTask.call();
fileManager.close();

// 使用URLClassLoader加载class到内存
URL[] urls = new URL[] { new URL("file:/" + currentDir + "/src/main/java/com/seewo/phoenix/TestCompiler.java") };
URLClassLoader cLoader = new URLClassLoader(urls);
Class c = cLoader.loadClass("com.seewo.phoenix.TestCompiler");
cLoader.close();

// 利用class创建实例,反射执行方法
Object obj = c.newInstance();
Method method = c.getMethod("disply");
method.invoke(obj);

       大家都会问,这个跟mapstruct说的有半毛钱关系?别急,请看下面的代码过程,在执行JavaCompile#compile中就有去执行processAnnotation(注解扫描与处理)这个步骤。这就是mapstruct注解扫描的入口调用方法。这里对java compile的调用过程就不详细阐述,另外会开一篇讲其中的原理。

跟踪整个调用链路,最后是不是惊喜地发现了MappingProcessor的入口。

      其实,这就是“JSR 269 Pluggable Annotation Processing API”规范,只要程序实现了该API,就能在javac运行的时候得到调用。 举例来说,现在有一个实现了"JSR 269 API"的程序A,那么使用javac编译源码的时候具体流程如下:

  1. javac对源代码进行分析,生成一棵抽象语法树(AST) ;

  2. 运行过程中调用实现了"JSR 269 API"的A程序 ;

  3. 此时A程序就可以完成它自己的逻辑,包括修改第一步骤得到的抽象语法树(AST) ;

  4. javac使用修改后的抽象语法树(AST)生成字节码文件.

mapstruct本质上就是这样的一个实现了"JSR 269 API"的程序。在使用javac的过程中,它产生作用的具体流程如下:

2. 实现原理

2.1 框架主体

       在上节中,mapsstruct利用的JSR269规范去扫描和生成的,但是从一个接口定义就能生成一个.class文件,是不是还有点遥远?

       首先我们看下整个框架代码的组成部分,主要分为两个包: org.mapstruct:mapstruct:包含了必要的注解,例如@Mapping; org.mapstruct:mapstruct-processor:包含生成映射器实现的注解处理器。这个就是整个mapstruct框架的入口,继承了注解处理器,在java compile时将会调用process做操作。

//支持@Mapper注解
@SupportedAnnotationTypes("org.mapstruct.Mapper")
public class MappingProcessor extends AbstractProcessor {
    //处理入口
    @Override
    public boolean process(final Set annotations, final RoundEnvironment roundEnvironment) {
        if ( !roundEnvironment.processingOver() ) {
            RoundContext roundContext = new RoundContext( annotationProcessorContext );
            Set deferredMappers = getAndResetDeferredMappers();
            processMapperElements( deferredMappers, roundContext );
            
            Set mappers = getMappers( annotations, roundEnvironment );
            processMapperElements( mappers, roundContext );
        }
        return ANNOTATIONS_CLAIMED_EXCLUSIVELY;
    }
}
  • Element是一个接口,表示一个程序元素,它可以是包、类、方法或者一个变量。Element已知的子接口有:
  • PackageElement 表示一个包程序元素。提供对有关包及其成员的信息的访问。
  • ExecutableElement 表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。
  • TypeElement 表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口。
  • VariableElement 表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。

2.2 processor处理链

      在入口方法中可以看到,主要使用了processor来对解析生成过程进行处理,我们可以看到processor的相关定义。

其中每个process节点都继承ModelElementProcessor基类。

注解处理器以及框架自带的处理类 都以java SPI的方式使用ServiceClas加载进来了,主要实现方法在MappingProcessor#getProcessors中。这里是用serviceClassLoader去加载所有定义好的Process类,形成类似于处理链,类似于责任链的一种方式(用数组记录执行节点而不是用链表)

主要调用链路图如下,每个process节点都有优先级,顺序执行后将生成的内容写到了文件中。

3.怎样debug注解处理器?

因为这个注解处理器是在解析->编译的过程完成,跟普通的jar包调试不太一样,maven框架为我们提供了调试入口,需要借助maven才能实现debug。所以只需要在编译过程打开debug才可调试。

  • 在项目的pom文件所在目录执行mvnDebug compile

  • 接着用idea打开项目,添加一个remote,端口为8000

  • 打上断点,debug 运行remote即可调试。

4.小结

       本篇先简短介绍了java动态编译的过程,并用相关api接口实现了从源文件到执行文件的整个过程,并针对JSR269规范进行了注解扫描与处理过程,结合mapstruct 框架解析了生成转换代码的过程原理。希望大家对mapstruct利用JSR269规范生成代码有所帮助,同样的剖析思路可用于lombok/kotlin等语法糖的原理探究。

参考资料:

www.cnblogs.com/avenwu/p/41…

blog.csdn.net/lmy86263/ar…

www.cnblogs.com/JeffChen/ar…

www.cnblogs.com/hbuwdx/p/94…

blog.csdn.net/wusj3/artic…

www.jianshu.com/p/26c88fbb5…

stackoverflow.com/questions/3…