首發於個人公衆號html
編譯時註解之APTjava
註解系列android
前一篇介紹了註解的基本知識以及常見用法,因爲運行期(RunTime)利用反射去獲取信息仍是比較損耗性能的,本篇將介紹一種使用註解更加優雅的方式,編譯期(Compile time)註解,以及處理編譯期註解的手段APT和Javapoet,限於篇幅,本篇着重介紹APT 首先你的註解須要聲明爲CLASS @Retention(RetentionPolicy.CLASS)git
編譯期解析註解基本原理: 在某些代碼元素上(如類型、函數、字段等)添加註解,在編譯時編譯器會檢查AbstractProcessor的子類,而且調用該類型的process函數,而後將添加了註解的全部元素都傳遞到process函數中,使得開發人員能夠在編譯器進行相應的處理,例如,根據註解生成新的Java類,這也就是ButterKnife等開源庫的基本原理。github
在處理編譯器註解的第一個手段就是APT(Annotation Processor Tool),即註解處理器。在java5的時候已經存在,可是java6開始的時候纔有可用的API,最近才隨着butterknife這些庫流行起來。本章將闡述什麼是註解處理器,以及如何使用這個強大的工具。api
什麼是APTbash
APT是一種處理註解的工具,確切的說它是javac的一個工具,它用來在編譯時掃描和處理註解,一個註解的註解處理器,以java代碼(或者編譯過的字節碼)做爲輸入,生成.java文件做爲輸出,核心是交給本身定義的處理器去處理,數據結構
如何使用oracle
每一個自定義的處理器都要繼承虛處理器,實現其關鍵的幾個方法框架
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() { }
}
複製代碼
下面重點介紹下這幾個函數:
init(ProcessingEnvironment env)
: 每個註解處理器類都必須有一個空的構造函數。然而,這裏有一個特殊的init()方法,它會被註解處理工具調用,並輸入ProcessingEnviroment參數。ProcessingEnviroment提供不少有用的工具類Elements, Types和Filerprocess(Set<? extends TypeElement> annotations, RoundEnvironment env)
: 這至關於每一個處理器的主函數main()。你在這裏寫你的掃描、評估和處理註解的代碼,以及生成Java文件。輸入參數RoundEnviroment,可讓你查詢出包含特定註解的被註解元素。這是一個布爾值,代表註解是否已經被處理器處理完成,官方原文whether or not the set of annotations are claimed by this processor
,一般在處理出現異常直接返回false、處理完成返回true。getSupportedAnnotationTypes()
: 必需要實現;用來表示這個註解處理器是註冊給哪一個註解的。返回值是一個字符串的集合,包含本處理器想要處理的註解類型的合法全稱。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()中咱們能夠獲取以下引用
在註解處理過程當中,咱們掃面全部的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。
在自定義註解器的初始化時候,能夠獲取如下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();
}
}
複製代碼
通常配合JavaPoet來生成須要的java文件(下一篇將詳細介紹javaPoet)
Messager提供給註解處理器一個報告錯誤、警告以及提示信息的途徑。它不是註解處理器開發者的日誌工具,而是用來寫一些信息給使用此註解器的第三方開發者的。在官方文檔中描述了消息的不一樣級別中很是重要的是Kind.ERROR,由於這種類型的信息用來表示咱們的註解處理器處理失敗了。頗有多是第三方開發者錯誤的使用了註解。這個概念和傳統的Java應用有點不同,在傳統Java應用中咱們可能就拋出一個異常Exception。若是你在process()中拋出一個異常,那麼運行註解處理器的JVM將會崩潰(就像其餘Java應用同樣),使用咱們註解處理器第三方開發者將會從javac中獲得很是難懂的出錯信息,由於它包含註解處理器的堆棧跟蹤(Stacktace)信息。所以,註解處理器就有一個Messager類,它可以打印很是優美的錯誤信息。除此以外,你還能夠鏈接到出錯的元素。在像如今的IDE(集成開發環境)中,第三方開發者能夠直接點擊錯誤信息,IDE將會直接跳轉到第三方開發者項目的出錯的源文件的相應的行。
Types是一個用來處理TypeMirror的工具
Elements是一個用來處理Element的工具
優勢(結合javapoet)
缺點
一般咱們須要分離處理器和註解 這樣作的緣由是,在發佈程序時註解及生成的代碼會被打包到用戶程序中,而註解處理器則不會(註解處理器是在編譯期在JVM上運行跟運行時無關)。要是不分離的話,假如註解處理器中使用到了其餘第三方庫,那就會佔用系統資源,特別是方法數,
該技術可讓咱們在設計本身框架時候多了一種技術選擇,更加的優雅
反射優化
運行時註解的使用能夠減小不少代碼的編寫,可是誰都知道這是有性能損耗的,不過權衡利弊,咱們選擇了妥協,這個技術手段能夠處理這個問題