編譯時註解之APT

龍湫發表於2016-12-11

首發於我的公眾號

編譯時註解之APT

0x00 概述

註解系列

前一篇介紹了註解的基本知識以及常見用法,由於執行期(RunTime)利用反射去獲取資訊還是比較損耗效能的,本篇將介紹一種使用註解更加優雅的方式,編譯期(Compile time)註解,以及處理編譯期註解的手段APT和Javapoet,限於篇幅,本篇著重介紹APT 首先你的註解需要宣告為CLASS @Retention(RetentionPolicy.CLASS)

編譯期解析註解基本原理: 在某些程式碼元素上(如型別、函式、欄位等)新增註解,在編譯時編譯器會檢查AbstractProcessor的子類,並且呼叫該型別的process函式,然後將新增了註解的所有元素都傳遞到process函式中,使得開發人員可以在編譯器進行相應的處理,例如,根據註解生成新的Java類,這也就是ButterKnife等開源庫的基本原理。

0x01 APT

在處理編譯器註解的第一個手段就是APT(Annotation Processor Tool),即註解處理器。在java5的時候已經存在,但是java6開始的時候才有可用的API,最近才隨著butterknife這些庫流行起來。本章將闡述什麼是註解處理器,以及如何使用這個強大的工具。

什麼是APT

APT是一種處理註解的工具,確切的說它是javac的一個工具,它用來在編譯時掃描和處理註解,一個註解的註解處理器,以java程式碼(或者編譯過的位元組碼)作為輸入,生成.java檔案作為輸出,核心是交給自己定義的處理器去處理,

如何使用

每個自定義的處理器都要繼承虛處理器,實現其關鍵的幾個方法

  • 繼承虛處理器 AbstractProcessor
public class MyProcessor extends AbstractProcessor {
    @Override
    public synchronized void init(ProcessingEnvironment env){ }

    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }

    @Override
    public Set<String> getSupportedAnnotationTypes() { }

    @Override
    public SourceVersion getSupportedSourceVersion() { }
}

複製程式碼

下面重點介紹下這幾個函式:

  1. init(ProcessingEnvironment env): 每一個註解處理器類都必須有一個空的建構函式。然而,這裡有一個特殊的init()方法,它會被註解處理工具呼叫,並輸入ProcessingEnviroment引數。ProcessingEnviroment提供很多有用的工具類Elements, Types和Filer
  2. process(Set<? extends TypeElement> annotations, RoundEnvironment env): 這相當於每個處理器的主函式main()。你在這裡寫你的掃描、評估和處理註解的程式碼,以及生成Java檔案。輸入引數RoundEnviroment,可以讓你查詢出包含特定註解的被註解元素。這是一個布林值,表明註解是否已經被處理器處理完成,官方原文whether or not the set of annotations are claimed by this processor,通常在處理出現異常直接返回false、處理完成返回true。
  3. getSupportedAnnotationTypes(): 必須要實現;用來表示這個註解處理器是註冊給哪個註解的。返回值是一個字串的集合,包含本處理器想要處理的註解型別的合法全稱。
  4. getSupportedSourceVersion(): 用來指定你使用的Java版本。通常這裡返回SourceVersion.latestSupported(),你也可以使用SourceVersion_RELEASE_6、7、8
  • 註冊 處理器

由於處理器是javac的工具,因此我們必須將我們自己的處理器註冊到javac中,在以前我們需要提供一個.jar檔案,打包你的註解處理器到此檔案中,並在在你的jar中,需要打包一個特定的檔案 javax.annotation.processing.Processor到META-INF/services路徑下 把MyProcessor.jar放到你的builpath中,javac會自動檢查和讀取javax.annotation.processing.Processor中的內容,並且註冊MyProcessor作為註解處理器。

超級麻煩有木有,不過不要慌,谷歌baba給我們開發了AutoService註解,你只需要引入這個依賴,然後在你的直譯器第一行加上

@AutoService(Processor.class)
複製程式碼

然後就可以自動生成META-INF/services/javax.annotation.processing.Processor檔案的。省去了打jar包這些繁瑣的步驟。

APT中的Elements和TypeMirrors

在前面的init()中我們可以獲取如下引用

  • Elements:一個用來處理Element的工具類
  • Types:一個用來處理TypeMirror的工具類
  • Filer:正如這個名字所示,使用Filer你可以建立檔案(通常與javapoet結合)

在註解處理過程中,我們掃面所有的Java原始檔。原始檔的每一個部分都是一個特定型別的Element

先來看一下Element

對於編譯器來說 程式碼中的元素結構是基本不變的,如,組成程式碼的基本元素包括包、類、函式、欄位、變數的等,JDK為這些元素定義了一個基類也就是Element

Element有五個直接子類,分別代表一種特定型別

==

PackageElement 表示一個包程式元素,可以獲取到包名等
TypeParameterElement 表示一般類、介面、方法或構造方法元素的泛型引數
TypeElement 表示一個類或介面程式元素
VariableElement 表示一個欄位、enum 常量、方法或構造方法引數、區域性變數或異常引數
ExecutableElement 表示某個類或介面的方法、構造方法或初始化程式(靜態或例項),包括註解型別元素

==

開發中Element可根據實際情況強轉為以上5種中的一種,它們都帶有各自獨有的方法,如下所示

package com.example;    // PackageElement

public class Test {        // TypeElement

    private int a;      // VariableElement
    private Test other;  // VariableElement

    public Test () {}    // ExecuteableElement
    public void setA (  // ExecuteableElement
                     int newA   // TypeElement
                     ) {}
}
複製程式碼

再舉個栗子?:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Test {

    String value();
}
複製程式碼

這個註解因為只能作用於函式型別,因此,它對應的元素型別就是ExecutableElement當我們想通過APT處理這個註解的時候就可以獲取目標物件上的Test註解,並且將所有這些元素轉換為ExecutableElement元素,以便獲取到他們對應的資訊。

檢視其程式碼定義

定義如下:

** 
 * 表示一個程式元素,比如包、類或者方法,有如下幾種子介面: 
 * ExecutableElement:表示某個類或介面的方法、構造方法或初始化程式(靜態或例項),包括註解型別元素 ; 
 * PackageElement:表示一個包程式元素; 
 * TypeElement:表示一個類或介面程式元素; 
 * TypeParameterElement:表示一般類、介面、方法或構造方法元素的形式型別引數; 
 * VariableElement:表示一個欄位、enum 常量、方法或構造方法引數、區域性變數或異常引數 
 */  
public interface Element extends AnnotatedConstruct {  
    /** 
     * 返回此元素定義的型別 
     * 例如,對於一般類元素 C<N extends Number>,返回引數化型別 C<N> 
     */  
    TypeMirror asType();  
  
    /** 
     * 返回此元素的種類:包、類、介面、方法、欄位...,如下列舉值 
     * PACKAGE, ENUM, CLASS, ANNOTATION_TYPE, INTERFACE, ENUM_CONSTANT, FIELD, PARAMETER, LOCAL_VARIABLE, EXCEPTION_PARAMETER, 
     * METHOD, CONSTRUCTOR, STATIC_INIT, INSTANCE_INIT, TYPE_PARAMETER, OTHER, RESOURCE_VARIABLE; 
     */  
    ElementKind getKind();  
  
    /** 
     * 返回此元素的修飾符,如下列舉值 
     * PUBLIC, PROTECTED, PRIVATE, ABSTRACT, DEFAULT, STATIC, FINAL, 
     * TRANSIENT, VOLATILE, SYNCHRONIZED, NATIVE, STRICTFP; 
     */  
    Set<Modifier> getModifiers();  
  
    /** 
     * 返回此元素的簡單名稱,例如 
     * 型別元素 java.util.Set<E> 的簡單名稱是 "Set"; 
     * 如果此元素表示一個未指定的包,則返回一個空名稱; 
     * 如果它表示一個構造方法,則返回名稱 "<init>"; 
     * 如果它表示一個靜態初始化程式,則返回名稱 "<clinit>"; 
     * 如果它表示一個匿名類或者例項初始化程式,則返回一個空名稱 
     */  
    Name getSimpleName();  
  
    /** 
     * 返回封裝此元素的最裡層元素。 
     * 如果此元素的宣告在詞法上直接封裝在另一個元素的宣告中,則返回那個封裝元素; 
     * 如果此元素是頂層型別,則返回它的包; 
     * 如果此元素是一個包,則返回 null; 
     * 如果此元素是一個泛型引數,則返回 null. 
     */  
    Element getEnclosingElement();  
  
    /** 
     * 返回此元素直接封裝的子元素 
     */  
    List<? extends Element> getEnclosedElements();  
    
    boolean equals(Object var1);

    int hashCode();
  
    /** 
     * 返回直接存在於此元素上的註解 
     * 要獲得繼承的註解,可使用 getAllAnnotationMirrors 
     */  
    List<? extends AnnotationMirror> getAnnotationMirrors();  
  
    /** 
     * 返回此元素針對指定型別的註解(如果存在這樣的註解),否則返回 null。註解可以是繼承的,也可以是直接存在於此元素上的 
     */   
    <A extends Annotation> A getAnnotation(Class<A> annotationType); 
     
    //接受訪問者的訪問 (??)
     <R, P> R accept(ElementVisitor<R, P> var1, P var2);
}  
複製程式碼

最後一個,並沒有使用到,感覺不太好理解,查了資料這個函式接受一個ElementVisitor和型別為P的引數。

public interface ElementVisitor<R, P> {
    //訪問元素
    R visit(Element e, P p);

    R visit(Element e);

    //訪問包元素
    R visitPackage(PackageElement e, P p);

    //訪問型別元素
    R visitType(TypeElement e, P p);

   //訪問變數元素
    R visitVariable(VariableElement e, P p);

    //訪問克而執行元素
    R visitExecutable(ExecutableElement e, P p);

    //訪問引數元素
    R visitTypeParameter(TypeParameterElement e, P p);

    //處理位置的元素型別,這是為了應對後續Java語言的擴折而預留的介面,例如後續元素型別新增了,那麼通過這個介面就可以處理上述沒有宣告的型別
    R visitUnknown(Element e, P p);
}
複製程式碼

在ElementgVisitor中定義了多個visit介面,每個介面處理一種元素型別,這就是典型的訪問者模式。我們制定,一個類元素和函式元素是完全不一樣的,他們的結構不一樣,因此,在編譯器對他們的操作肯定是不一樣,通過訪問者模式正好可以解決資料結構與資料操作分離的問題,避免某些操作汙染資料物件類。

因此,程式碼在APT眼中只是一個結構化的文字而已。Element代表的是原始碼。TypeElement代表的是原始碼中的型別元素,例如類。然而,TypeElement並不包含類本身的資訊。你可以從TypeElement中獲取類的名字,但是你獲取不到類的資訊,例如它的父類。這種資訊需要通過TypeMirror獲取。你可以通過呼叫elements.asType()獲取元素的TypeMirror。

0x02 輔助介面

在自定義註解器的初始化時候,可以獲取以下4個輔助介面

  public class MyProcessor extends AbstractProcessor {  
      
        private Types typeUtils;  
        private Elements elementUtils;  
        private Filer filer;  
        private Messager messager;  
      
        @Override  
        public synchronized void init(ProcessingEnvironment processingEnv) {  
            super.init(processingEnv);  
            typeUtils = processingEnv.getTypeUtils();  
            elementUtils = processingEnv.getElementUtils();  
            filer = processingEnv.getFiler();  
            messager = processingEnv.getMessager();  
        }  
    }  
複製程式碼
  • Filer

一般配合JavaPoet來生成需要的java檔案(下一篇將詳細介紹javaPoet)

  • Messager

Messager提供給註解處理器一個報告錯誤、警告以及提示資訊的途徑。它不是註解處理器開發者的日誌工具,而是用來寫一些資訊給使用此註解器的第三方開發者的。在官方文件中描述了訊息的不同級別中非常重要的是Kind.ERROR,因為這種型別的資訊用來表示我們的註解處理器處理失敗了。很有可能是第三方開發者錯誤的使用了註解。這個概念和傳統的Java應用有點不一樣,在傳統Java應用中我們可能就丟擲一個異常Exception。如果你在process()中丟擲一個異常,那麼執行註解處理器的JVM將會崩潰(就像其他Java應用一樣),使用我們註解處理器第三方開發者將會從javac中得到非常難懂的出錯資訊,因為它包含註解處理器的堆疊跟蹤(Stacktace)資訊。因此,註解處理器就有一個Messager類,它能夠列印非常優美的錯誤資訊。除此之外,你還可以連線到出錯的元素。在像現在的IDE(整合開發環境)中,第三方開發者可以直接點選錯誤資訊,IDE將會直接跳轉到第三方開發者專案的出錯的原始檔的相應的行。

  • Types

Types是一個用來處理TypeMirror的工具

  • Elements

Elements是一個用來處理Element的工具

0x03 優缺點

優點(結合javapoet)

  • 對程式碼進行標記、在編譯時收集資訊並做處理
  • 生成一套獨立程式碼,輔助程式碼執行

缺點

  • 可以自動生成程式碼,但在執行時需要主動呼叫
  • 如果要生成程式碼需要編寫模板函式

0x04 其他

  1. 通常我們需要分離處理器和註解 這樣做的原因是,在釋出程式時註解及生成的程式碼會被打包到使用者程式中,而註解處理器則不會(註解處理器是在編譯期在JVM上執行跟執行時無關)。要是不分離的話,假如註解處理器中使用到了其他第三方庫,那就會佔用系統資源,特別是方法數,

  2. 該技術可以讓我們在設計自己框架時候多了一種技術選擇,更加的優雅

  3. 反射優化

執行時註解的使用可以減少很多程式碼的編寫,但是誰都知道這是有效能損耗的,不過權衡利弊,我們選擇了妥協,這個技術手段可以處理這個問題

0x05 參考文獻

公眾號小.jpg

相關文章