編譯時註解之APT

首發於個人公衆號html

編譯時註解之APTjava

0x00 概述

註解系列android

前一篇介紹了註解的基本知識以及常見用法,因爲運行期(RunTime)利用反射去獲取信息仍是比較損耗性能的,本篇將介紹一種使用註解更加優雅的方式,編譯期(Compile time)註解,以及處理編譯期註解的手段APT和Javapoet,限於篇幅,本篇着重介紹APT 首先你的註解須要聲明爲CLASS @Retention(RetentionPolicy.CLASS)git

編譯期解析註解基本原理: 在某些代碼元素上(如類型、函數、字段等)添加註解,在編譯時編譯器會檢查AbstractProcessor的子類,而且調用該類型的process函數,而後將添加了註解的全部元素都傳遞到process函數中,使得開發人員能夠在編譯器進行相應的處理,例如,根據註解生成新的Java類,這也就是ButterKnife等開源庫的基本原理。github

0x01 APT

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

什麼是APTbash

APT是一種處理註解的工具,確切的說它是javac的一個工具,它用來在編譯時掃描和處理註解,一個註解的註解處理器,以java代碼(或者編譯過的字節碼)做爲輸入,生成.java文件做爲輸出,核心是交給本身定義的處理器去處理,數據結構

如何使用oracle

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

  • 繼承虛處理器 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_六、七、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
相關文章
相關標籤/搜索