APT(Annotation Processing Tool) 编译时处理工具

1,965 阅读6分钟

1. APT简介

1.1 什么是APT?

APT(Annotation Processing Tool) 基于注解的处理工具。
使用该技术可以在java代码编译的期间做一些处理,比如代码插桩、代码生成。
在实际使用中最多的是用 AbstractProcessor

1.2 编译过程

JVM执行代码时执行的是.class字节码文件。因此需要使用编译器将.java文件编译为二进制字节码文件。
官方编译器为javac命令。

当你执行javac命令时

  • 第一步: javac命令会启动一个完整的JVM执行编译行为。(是的你没看错,javac使用JVM完成编译)
  • 第二步: 加载你的.java文件,并将其解释为AST(Abstract Syntax Tree 抽象语法树 自行了解),所有的语法错误均在这一阶段。
  • 第三步: 然后JVM虚拟机会使用访问者模式遍历AST,为语法树添加代码意义,Processor注解处理器就是在这一阶段被执行。最终这些AST被编译保存为.class二进制字节码文件。

Processor解释:

注释处理按照rounds的顺序进行。 在每一轮中,可以向处理器询问process在前一轮产生的源文件和类文件上找到的注释的子集。 第一轮处理的输入是工具运行的初始输入; 这些初始输入可以被视为虚拟第0轮处理的输出。 如果要求处理器在给定轮次上进行处理,则会要求处理后续轮次,包括最后一轮,即使没有要处理的注释。 工具基础结构还可以要求处理器处理由工具的操作隐式生成的文件。

Annotation processing happens in a sequence of rounds. On each round, a processor may be asked to process a subset of the annotations found on the source and class files produced by a prior round. The inputs to the first round of processing are the initial inputs to a run of the tool; these initial inputs can be regarded as the output of a virtual zeroth round of processing. If a processor was asked to process on a given round, it will be asked to process on subsequent rounds, including the last round, even if there are no annotations for it to process. The tool infrastructure may also ask a processor to process files generated implicitly by the tool's operation.

1.3 Processor

该类在javax.annotation.processing包中,它被设计为编译期间针对某些注解进行处理,比如生成新的类或者修改现有的类。 如:Lombok的Getter、Setter方法生成就是基于该技术。

方法描述:

方法名描述
getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member, String userText)返回一个空的迭代完成。
getSupportedAnnotationTypes()如果处理器类使用SupportedAnnotationTypes进行批注,则返回与注释具有相同字符串集的不可修改集。
getSupportedOptions()如果处理器类使用SupportedOptions进行批注,则返回具有与批注相同的字符串集的不可修改集
getSupportedSourceVersion()如果处理器类使用SupportedSourceVersion进行批注,请在批注中返回源版本。
init(ProcessingEnvironment processingEnv)通过将 processingEnv字段设置为 processingEnv参数的值,使用处理环境初始化处理器。
process(Set annotations, RoundEnvironment roundEnv)处理源自前一轮的类型元素的一组注释类型,并返回此处理器是否声明了这些注释类型。 如果返回true ,则声明注释类型,并且不会要求后续处理器处理它们; 如果返回false ,则注释类型无人认领,可能会要求后续处理器处理它们。 处理器可以总是返回相同的布尔值,或者可以根据其自己选择的标准改变结果。

ProcessingEnviroment

这个类中包含了编译过程中的所有工具类。

工具类方法名功能
FilergetFiler就是文件流输出路径,当我们用AbstractProcess生成一个java类的时候,我们需要保存在Filer指定的目录下。
MessagergetMessager输出日志工具,需要输出一些日志相关的时候我们就要使用这个了。
ElementsgetElementUtils获取元素信息的工具,比如说一些类信息继承关系等。
TypesgetTypeUtils类型相关的工具类

1.3 AST(Abstract Syntax Tree)抽象语法树

每个.java文件都会被解析为一个JCCompilationUnit对象,类的所有信息均会被解析为树节点。 各个节点的意义参考: AST解析

1.4 通过修改AST树在当前类文件中生成Getter方法(而不是生成新的类文件)

1.4.1 创建一个多模块工程

apt-core模块: 该模块中的代码为注解处理类
apt-demo模块: 该模块中的代码为使用类

为什么要使用两个模块?

JDK使用javac命令来进行编译。该命令的-cp选项可以引入其他的.jar文件。因此需要把注解处理器所在的类打成.jar包才行,所以使用多模块。

1.4.2 配置POM.xml引用私有API

在jdk1.9之后oracle移除了tools.jar这个包,里面包含了许多私有API,包括JCTree处理,如果要修改AST树的话则必须引用该类。
有同学发现引入tools.jar仍然无法使用JCTree等等这些类。那是因为在java9之后,jdk引入模块化概念,同时jdk自身也被模块化了。而java.compile在java9中被列为私有API,并没有暴露。因此可以添加编译参数来暴露这些package详情如下

<build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <compilerArgs>
                        <arg>--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
                        <arg>--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
                        <arg>--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
                        <arg>--add-exports=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
                        <arg>--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
                        <arg>--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
                        <arg>--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
                        <arg>--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
                        <arg>--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
                        <arg>--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
                    </compilerArgs>
                    <!-- 这里如果用Release的话会报错 使用target source代替 -->
                    <target>11</target>
                    <source>11</source>
                    <!-- 这是AutoService的处理类 -->
                    <annotationProcessorPaths>
                        <path>
                            <groupId>com.google.auto.service</groupId>
                            <artifactId>auto-service</artifactId>
                            <version>${auto-service.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>

1.4.3 创建注解类

package com.czl.apt;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 表明该Getter注解只能被标注在类上
 */
@Target(value = ElementType.TYPE)
/**
 * 表明该Getter注解将被编译器编译但不会存在JVM运行时
 */
@Retention(RetentionPolicy.CLASS)
public @interface Getter
{

}

1.4.4 创建注解处理类

package com.czl.apt;

import com.google.auto.service.AutoService;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeScanner;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.ListBuffer;
import com.sun.tools.javac.util.Names;

import javax.annotation.processing.*;
import javax.lang.model.element.*;
import javax.lang.model.util.Elements;
import java.util.*;

/**
 * 该注解表明当前注解处理器仅能处理
 * com.czl.apt.Getter注解
 */
@SupportedAnnotationTypes("com.czl.apt.Getter")
/**
 * javac调用注解处理器时是使用spi机制调用
 * 因此需要在META-INF下创建spi文件
 * 使用该@AutoService注解可以自动创建
 * Google的工具
 */
@AutoService(Processor.class)
public class GetterProcessor extends AbstractProcessor {

    private Elements elementUtils;
    private JavacTrees javacTrees;
    private TreeMaker treeMaker;
    private Names names;

    /**
     * 初始化,此处可以获取各种官方提供的工具类
     *
     * @param processingEnv
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.elementUtils = processingEnv.getElementUtils();

        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        //JCTree工具类
        this.javacTrees = JavacTrees.instance(processingEnv);
        //JCTree工具类
        this.treeMaker = TreeMaker.instance(context);
        //命名工具类
        this.names = Names.instance(context);
    }

    /**
     * 重点:Jvm会调用该方法执行注解处理
     *
     * @param annotations 被该处理器支持的注解
     * @param roundEnv    环境
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //1. 获取要处理的注解的TypeElement(可以遍历annotations入参获取)  这里采用全限定类名获取
        TypeElement anno = elementUtils.getTypeElement("com.czl.apt.Getter");
        //2. 获取打上注解的Element(这些Element可能为方法,接口等等)
        Set<? extends Element> types = roundEnv.getElementsAnnotatedWith(anno);
        //3. 遍历这些Element
        types.forEach(t -> {
            //3. 判断类型是否为Class  在编译时需要使用getKind判断当前Element类型
            if (t.getKind().isClass()) {
                //4. 处理
                currentClassProcess((TypeElement) t);
            }
        });
        /**
         * 重要:当该方法返回True时  后续的注解处理器将不处理该注解
         *      当该方法返回False时  后续的注解处理器会处理该注解
         * 处理顺序将以SPI文件顺序处理
         */
        return false;
    }

    /**
     * 在当前类生成Getter方法
     *
     * @param typeElement
     */
    private void currentClassProcess(TypeElement typeElement) {
        try {
            //获取当前类的ast语法树
            JCTree.JCClassDecl classDecl = javacTrees.getTree(typeElement);
            //对该树进行访问   因为需要修改树结构,因此使用TreeTranslator
            classDecl.accept(new TreeScanner() {
                @Override
                public void visitClassDef(JCTree.JCClassDecl tree) {
                    //该列表用于存放生成的Get方法
                    List<JCTree.JCMethodDecl> methods = new ArrayList<>();
                    //tree.defs为当前类中的方法和属性定义  遍历
                    for (JCTree varTree : tree.defs) {
                        //由于本次只对属性生成方法,因此只关注属性
                        if (varTree.getKind().equals(Tree.Kind.VARIABLE)) {
                            JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) varTree;
                            methods.add(generateGetMethods(jcVariableDecl));
                        }
                    }
                    /**
                     * prependList方法会将参数添加到列表头部并返回新列表
                     * 此处将生成的方法加入至方法定义中
                     */
                    tree.defs = tree.defs.prependList(com.sun.tools.javac.util.List.from(methods));
                    super.visitClassDef(tree);
                }
            });
        } catch (Exception e) {
            System.out.println(1);
        }

    }

    //使用TreeMaker生成方法
    private JCTree.JCMethodDecl generateGetMethods(JCTree.JCVariableDecl variableDecl) {
        //treeMaker.MethodDef  创建方法
        JCTree.JCMethodDecl methodDecl = treeMaker.MethodDef(
                //创建方法访问符 public
                treeMaker.Modifiers(Flags.PUBLIC),
                //方法名
                names.fromString("get" + variableDecl.getName().toString()),
                //返回值
                treeMaker.Ident(variableDecl.vartype.type.tsym),
                //泛型参数列表 此处写无
                com.sun.tools.javac.util.List.nil(),
                //入参列表  此处写无
                com.sun.tools.javac.util.List.nil(),
                //异常  此处写无
                com.sun.tools.javac.util.List.nil(),
                //方法体
                treeMaker.Block(
                        //我也不知道这是啥
                        0,
                        //语句链表  为了构建 return this.id;
                        new ListBuffer<JCTree.JCStatement>()
                                .append(
                                        //返回值  return
                                        treeMaker.Return(
                                                //此处是构建   this.id; 语句块
                                                treeMaker.Select(treeMaker.Ident(names.fromString("this")), variableDecl.name)
                                        )
                                ).toList()
                ),
                //不需要默认值
                null
        );
        return methodDecl;
    }


}

1.4.4 成果

GitHub项目地址