好玩的编译时注解处理工具——APT

大家对Java中的注解(Annotation)应该都不陌生吧,JDK1.5就引进来了,它本质上只是一种元数据,和配置文件一样。利用反射在运行时解析处理能够实现各种灵活强大的功能,比如Spring就将其作用发挥得淋漓尽致。至于用法,这里就不说了,我的其它文章里面很多地方有用到过,可以参考一下。

一、运行时注解与编译时注解 

我们看到的大部分注解,它们都是在代码运行时才使用的,所以一般定义成这样

@Retention(RetentionPolicy.RUNTIME)

表示注解信息保留到代码运行时阶段。如果你足够细心,一定也见过这种

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

它表示注解信息只保留在源文件,即Xxx.java,编译之后就丢失了。所以,这类注解在编译时就需要解析和处理了。

二、编译时注解的使用方法 

怎么在编译阶段使用我们自定义的注解呢?很幸运,JDK1.6之后,Java提供了APT,即Annotation Processing Tool,基于SPI机制(什么是SPI?与API又有什么不同呢?之前文章已经详细介绍,点这里查看),让我们可以扩展自己的编译处理器。

APT的使用步骤

1、定义一个继承javax.annotation.processing.AbstractProcessor的类,实现process方法,重写其它方法或者使用指定注解配置一些其它信息。

1)实现process方法,这个是核心,处理逻辑都写在这里。public abstract boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv),其参数和返回值含义如下

  • 参数annotations:请求处理的注释类型,包含了该类处理的所有注解信息。
  • 参数roundEnv:有关当前和以前round的环境信息,可以从这里拿到可以处理的所有元素信息。
  • 返回值:true——则这些注释已声明并且不要求后续Processor处理它们;false——则这些注释未声明并且可能要求后续 Processor处理它们。

2)配置该类可处理的注解,两种方式可选

  • 类上面使用注解,比如
@SupportedAnnotationTypes("cn.zhh.Getter")
public class GetterProcessor extends AbstractProcessor {
// ...
}
  • 重写getSupportedAnnotationTypes方法,比如
@Override
public Set<String> getSupportedAnnotationTypes() {
    return Collections.singleton(SuppressWarnings.class.getCanonicalName());
}

3)配置支持的源码版本,两种方式可选

  • 类上面使用注解,比如
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GetterProcessor extends AbstractProcessor {
// ...
}
  • 重写getSupportedSourceVersion方法,比如
@Override
public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.latestSupported();
}

2、将处理器类注册到Processor接口的实现里面。

在源码的根目录下新建META-INF/services/文件夹,然后新建一个javax.annotation.processing.Processor文件,里面写上注解处理器的全类名,每个单独一行。

这是SPI的常规操作:这样子注册后,Java就可以在运行时找到Processor接口的这个实现类了。

除了这种方式,Google也提供了更简单的方式,即在处理器类上面加一个注解AutoService就可以了,我没试过,大家可以了解一下。

3、在需要使用的项目引入,编译时指定annotationProcessor。

一般上,注解处理器项目是独立打成jar包或者作为maven工程引入到具体需要使用的项目里面的,因为它不需要也不能再进行编译(编译它的时候你又指定了自己作为注解处理器,会报找不到类的异常)。如果你非要放到同一个项目里,然后分开编译也是可以的。

  • 1)javac命令可以指定注解处理器。
  • 2)maven工程的compile插件可以指定注解处理器。
  • 3)IDEA可以在配置里面指定注解处理器。

三、APT的用途

市面上比较广泛的用法目前有两种

  • querydsl(http://www.querydsl.com/):在Model上面加个注解,编译时生成它们的查询对象。比如MyBatis我们也可以在编译时根据DO类生成Example、Mapper等文件。
  • lombok(https://projectlombok.org/):在编译时生成POJO的getter、setter、toString等方法。

第一个比较简单,毕竟只是生成新的源文件。第二个就牛逼了,直接修改源文件编译生成的语法树,隐式增加新的代码。

基于这两个厉害的项目,我们来写两个小demo。

1、小试牛刀:编译时日志打印出带有指定注解的Java类信息

1)新建一个maven工程apt-core1作为注解处理器项目,pom文件添加compile插件并设置compilerArgument参数(避免编译时使用自身的annotationProcessor)。

<build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.0</version>
                <configuration>
                    <encoding>${project.build.sourceEncoding}</encoding>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                    <proc>none</proc><!--等同于:<compilerArgument>-proc:none</compilerArgument>-->
                </configuration>
            </plugin>
        </plugins>
    </build>

2)自定义一个用于处理SuppressWarnings注解的处理器。

至于为什么是@SuppressWarnings,其实没什么意思,随便玩一下。

/**
 * SuppressWarnings处理器
 *
 * @author Zhou Huanghua
 * @date 2020/1/5 14:19
 */
public class SuppressWarningsProcessor extends AbstractProcessor {

    private static final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass().getName());

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        roundEnv.getElementsAnnotatedWith(SuppressWarnings.class).forEach(element -> {
            logger.info(String.format("element %s has been processed.", element.getSimpleName()));
        });
        return false;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(SuppressWarnings.class.getCanonicalName());
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

3)注册到META-INF/services/下的javax.annotation.processing.Processor文件里。

cn.zhh.SuppressWarningsProcessor

4)到此,注解处理器项目开发完成了,最后install一下到本地maven仓库,工程完整结构如下

5)新建一个maven工程apt-demo1作为实际使用的项目,pom文件添加compile插件并设置annotationProcessorPaths参数。

<build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.0</version>
                <configuration>
                    <encoding>${project.build.sourceEncoding}</encoding>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>cn.zhh</groupId>
                            <artifactId>apt-core1</artifactId>
                            <version>1.0-SNAPSHOT</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>

6)定义一个类,上面添加SuppressWarnings注解。

/**
 * 学生类
 *
 * @author Zhou Huanghua
 */
@SuppressWarnings("unchecked")
public class Student implements Serializable {
}

7)最后,编译工程apt-demo1,可以看到我们在注解处理器里面编写的打印日志内容出来了。

2、大显身手:编译时为类的属性生成getter方法

1)新建一个maven工程apt-core2作为注解处理器项目,pom文件添加compile插件并设置compilerArgument参数(避免编译时使用自身的annotationProcessor)。此外,因为编译时需要修改语法树,所以添加了sun的tools依赖(JDK有但不会默认引入)。

<dependencies>
        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.0</version>
                <configuration>
                    <encoding>${project.build.sourceEncoding}</encoding>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                    <proc>none</proc><!--等同于:<compilerArgument>-proc:none</compilerArgument>-->
                </configuration>
            </plugin>
        </plugins>
    </build>

2)自定义一个用于类上面的编译时注解Getter。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Getter {
}

3)自定义一个用于处理Getter注解的处理器。

代码涉及比较底层的语法树修改,这方面是难点...

/**
 * Getter处理器
 *
 * @author Zhou Huanghua
 * @date 2020/1/5 14:19
 */
@SupportedAnnotationTypes("cn.zhh.Getter")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GetterProcessor extends AbstractProcessor {

    private static final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass().getName());

    private Messager messager;
    private JavacTrees trees;
    private TreeMaker treeMaker;
    private Names names;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.messager = processingEnv.getMessager();
        this.trees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
    }

    @Override
    public synchronized boolean process(Set<? extends TypeElement> annotationSet, RoundEnvironment roundEnv) {
        // 处理带Getter注解的元素
        roundEnv.getElementsAnnotatedWith(Getter.class).forEach(element -> {
            JCTree jcTree = trees.getTree(element);
            jcTree.accept(new TreeTranslator() {
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                    List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();
                    for (JCTree tree : jcClassDecl.defs) {
                        if (tree.getKind().equals(Tree.Kind.VARIABLE)) {
                            JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) tree;
                            jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
                        }
                    }
                    jcVariableDeclList.forEach(jcVariableDecl -> {
                        logger.info(String.format("%s has been processed.", jcVariableDecl.getName()));
                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(jcVariableDecl));
                    });
                    super.visitClassDef(jcClassDecl);
                }

            });
        });
        return true;
    }

    private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
        statements.append(treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName())));
        JCTree.JCBlock body = treeMaker.Block(0, statements.toList());
        return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), getNewMethodName(jcVariableDecl.getName()), jcVariableDecl.vartype, List.nil(), List.nil(), List.nil(), body, null);
    }

    /**
     * 获取新方法名,get + 将第一个字母大写 + 后续部分, 例如 value 变为 getValue
     *
     * @param name
     * @return
     */
    private Name getNewMethodName(Name name) {
        String s = name.toString();
        return names.fromString("get" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
    }
}

4)注册到META-INF/services/下的javax.annotation.processing.Processor文件里。

cn.zhh.GetterProcessor

5)到此,注解处理器项目开发完成了,最后install一下到本地maven仓库,工程完整结构如下

6) 新建一个maven工程apt-demo2作为实际使用的项目。因为代码里面需要使用注解处理器项目的注解,所以将其依赖引进来。此外,增加compile插件并且设置annotationProcessors。(注意这里与上个demo的差异)

<dependencies>
        <dependency>
            <groupId>cn.zhh</groupId>
            <artifactId>apt-core2</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.0</version>
                <configuration>
                    <encoding>${project.build.sourceEncoding}</encoding>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                    <annotationProcessors>
                        <annotationProcessor>
                            cn.zhh.GetterProcessor
                        </annotationProcessor>
                    </annotationProcessors>
                </configuration>
            </plugin>
        </plugins>
    </build>

7)新建一个定义了id属性的Student类,上面添加注解Getter。

/**
 * 学生类
 *
 * @author Zhou Huanghua
 */
@Getter
public class Student implements Serializable {
    /** id */
    private Long id = 1L;
}

8)编译apt-demo2项目,然后反编译生成的Student.class文件,发现getId方法存在了。至此说明在编译时增加属性的getter方法做到了。

public class Student implements Serializable {
    private Long id = 1L;

    public Long getId() {
        return this.id;
    }

    public Student() {
    }
}

总结:利用APT,我们可以在编译阶段自动生成一些重复的模板代码,以提高开发效率。

相关代码:https://github.com/zhouhuanghua/apt

发布了109 篇原创文章 · 获赞 104 · 访问量 9万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章