簡單介紹 Java 中的編譯時註解

1. 前言

上一篇 主要介紹了什麼是 註解 (Annotation) 以及如何讀取 運行時註解 中的數據, 同時用註解實現了簡單的 ORM 功能. 此次介紹另外一部分: 如何讀取 編譯時註解 ( RetentionPolicy.SOURCE )java

2. 做用

編譯時註解能夠用來動態生成代碼. 使用 SOURCE 類型註解的代碼會在編譯時被解析, 生成新的 java 文件, 而後和原來的 java 文件一塊兒編譯成字節碼. 因爲不使用反射功能, 編譯時註解不會拖累性能, 於是被許多框架使用, 好比 Butter Knife, Dragger2 等.segmentfault

3. 例子

1. 代碼

仍是從簡單的例子開始看. 這裏要作的是生成一個 java 類, 其擁有一個打印註解信息的方法.
先定義一個註解app

package apt;
......
@Retention(RetentionPolicy.SOURCE) // 註解只在源碼中保留
@Target(ElementType.TYPE) // 用於修飾類
public @interface Hello {
    String name() default "";
}

使用註解的類框架

package apt;
@Hello(name = "world")
public class Player {
}

不使用註解的類, 用於對比jvm

package apt;
public class Ignored {
}

上一篇說過, 註解沒有行爲, 只有數據, 須要對應的處理器才能發揮做用. javac 提供瞭解析編譯時註解的註解處理器 ( Annotation Processor ). 對於自定義的註解, 須要手動實現它的註解處理器.下面來看一個簡單的註解處理器實現.ide

package apt;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.Writer;
import java.util.Set;

/**
 * Created by away on 2017/6/12.
 */
@SupportedSourceVersion(SourceVersion.RELEASE_8) // 源碼級別, 這裏的環境是 jdk 1.8
@SupportedAnnotationTypes("apt.Hello") // 處理的註解類型, 這裏須要處理的是 apt 包下的 Hello 註解(這裏也能夠不用註解, 改爲重寫父類中對應的兩個方法)
public class HelloProcessor extends AbstractProcessor {

    // 計數器, 用於計算 process() 方法運行了幾回
    private int count = 1;

    // 用於寫文件
    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        filer = processingEnv.getFiler();
    }

    // 處理編譯時註解的方法
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.out.println("start process, count = " + count++);
        // 得到全部類
        Set<? extends Element> rootElements = roundEnv.getRootElements();
        System.out.println("all class:");

        for (Element rootElement : rootElements) {
            System.out.println("  " + rootElement.getSimpleName());
        }

        // 得到有註解的元素, 這裏 Hello 只能修飾類, 因此只有類
        Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(Hello.class);
        System.out.println("annotated class:");
        for (Element element : elementsAnnotatedWith) {
            String className = element.getSimpleName().toString();
            System.out.println("  " + className);

            String output = element.getAnnotation(Hello.class).name();
            // 產生的動態類的名字
            String newClassName = className + "_New";
            // 寫 java 文件
            createFile(newClassName, output);
        }
        return true;
    }

    private void createFile(String className, String output) {
        StringBuilder cls = new StringBuilder();
        cls.append("package apt;\n\npublic class ")
                .append(className)
                .append(" {\n  public static void main(String[] args) {\n")
                .append("    System.out.println(\"")
                .append(output)
                .append("\");\n  }\n}");
        try {
            JavaFileObject sourceFile = filer.createSourceFile("apt." + className);
            Writer writer = sourceFile.openWriter();
            writer.write(cls.toString());
            writer.flush();
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

代碼的邏輯很簡單:函數

  • 得到全部標有註解的類工具

  • 取出註解中的信息性能

  • 生成新的 java 文件ui

這裏只須要知道, 自定義註解處理器要繼承 AbstractProcessor 類, 並重寫 process 方法.

2. 運行

此時項目目錄以下, 這裏 out 目錄爲手動建立

  • out

    • production

      • apt

  • src

    • apt

在命令行中進入項目根目錄, 即 src 文件夾的上一層.

首先編譯註解處理器: javac -encoding UTF-8 -d out\production\ src\apt\HelloProcessor.java src\apt\Hello.java

接着執行註解處理器: javac -encoding UTF-8 -cp out\production\ -processor apt.HelloProcessor -d out\production -s src\ src\apt\*.java

獲得以下輸出

start process, count = 1
all class:
  Hello
  HelloProcessor
  Ignored
  Player
annotated class:
  Player
start process, count = 2
all class:
  Player_New
annotated class:
start process, count = 3
all class:
annotated class:

這時 src/apt 目錄下會出現新的 Player_New.java 文件, 內容以下

package apt;

public class Player_New {
  public static void main(String[] args) {
    System.out.println("world");
  }
}

執行 java -cp out\production\elevator apt.Player_New
獲得輸出 world.

到這裏, 編譯時註解便處理成功了. 咱們定義了一個極其簡單的註解處理器, 讀取了註解信息, 並生成了新的 java 類來打印該信息.

這裏可能會報一個錯誤

編譯器 (1.8.0_131) 中出現異常錯誤。若是在 Bug Database (http://bugs.java.com) 中沒有找到該錯誤, 請經過 Java Bug 報告頁 (http://bugreport.java.com) 創建該 Java 編譯器 Bug。請在報告中附上您的程序和如下診斷信息。謝謝。
java.lang.IllegalStateException: endPosTable already set
...
...

這時把產生的 Player_New.java 文件刪去從新執行註解處理器就行了

3. javac

這裏稍微解釋一下 javac 命令, IDE 用多了, 寫的時候都忘得差很少了 (:зゝ∠)
javac 用於啓動 java 編譯器, 格式爲 javac <options> <source files>, 其中 <options> 的格式爲 -xx xxxx, 都是配對出現的, 用於指定一些信息.

這裏 <options> 的位置並無講究, 只要在 javac 後面就好了, 在兩個 xxx.java 之間出現也是能夠的, 好比: javac -d out\production\ src\apt\HelloProcessor.java -encoding UTF-8 src\apt\Hello.java 正常執行.

一些 <option>

  • -cp <路徑>

    • -classpath <路徑> 同樣, 用於指定查找用戶類文件和註釋處理程序的位置

  • -d <目錄>

    • 指定放置生成的類文件的位置

  • -s <目錄>

    • 指定放置生成的源文件的位置

  • -processorpath <路徑>

    • 指定查找註釋處理程序的位置

    • 不寫的話會使用 -cp 的位置

  • -processor <class1>[,<class2>,<class3>...]

    • 要運行的註釋處理程序的名稱; 繞過默認的搜索進程

4. 問題

到這裏應該會有一些問題, 好比

  1. AbstractProcessor, Elememt 分別是什麼

  2. process 爲何執行了 3 次

  3. 運行註解處理器的時候會啓動 jvm

這裏先說一下第三個問題. javac 運行註解處理器的時候, 會開一個完整的 java 虛擬機執行代碼, 因此自定義的註解處理器是可使用各類類庫的.
接下來說一下一些基本概念, 用來回答上面兩個問題.

4.概念

1. AbstractProcessor

  • 這是處理器的API,全部的處理器都是基於 AbstractProcessor, 它實現了接口 Processor

  • 接口

    • void init(ProcessingEnvironment processingEnv);

      • 會被註解處理工具調用, ProcessingEnvironment 提供了一些實用的工具類 Elements, TypesFiler.

    • boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv);

      • 至關於 main 函數, 是註解處理器的入口. 輸入參數 RoundEnviroment 能夠查詢出包含特定註解的被註解元素

    • SourceVersion getSupportedSourceVersion();

      • 用來指定使用的 java 版本

    • Set<String> getSupportedAnnotationTypes();

      • 指定這個註解處理器是註冊給哪一個註解的, 這裏須要用註解的全稱, 好比上面的 apt.Hello

    • 最後兩個也能夠用註解的形式實現, 例子中的代碼就是這麼作的

2. Element

  • 程序的元素, 例如包, 類或者方法. 每一個 Element 表明一個靜態的, 語言級別的構件. 能夠參考下面的代碼理解

package com.example;    // PackageElement

public class Foo {        // TypeElement
    private int a;      // VariableElement
    private Foo other;  // VariableElement
    public Foo () {}    // ExecuteableElement
    public void setA (  // ExecuteableElement
                     int newA   // TypeElement
                     ) {}
}

因而可知 roundEnv.getElementsAnnotatedWith(xxx.class) 獲得的並不必定是類, 也多是方法, 成員變量等, 只是例子中用的註解只能用於修飾類.

3. 註解處理器的執行

javadoc 中對此的描述以下

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.

概況來講, 就是 process() 方法會被調用屢次, 直到沒有新的類產生爲止.
由於新生成的文件中也可能包含 @Hello 註解,它們會繼續被 HelloProcessor 處理.

Round input output
1 Hello.java
HelloProcessor.java
Ignored.java
Player.java
Player_New.java
2 Player_New.java -
3 - -

下一篇會開始分析 Butter Knife 的源碼.

相關文章
相關標籤/搜索