好玩的編譯時註解處理工具——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萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章