首發於我的公眾號
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() { }
}
複製程式碼
下面重點介紹下這幾個函式:
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_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 其他
-
通常我們需要分離處理器和註解 這樣做的原因是,在釋出程式時註解及生成的程式碼會被打包到使用者程式中,而註解處理器則不會(註解處理器是在編譯期在JVM上執行跟執行時無關)。要是不分離的話,假如註解處理器中使用到了其他第三方庫,那就會佔用系統資源,特別是方法數,
-
該技術可以讓我們在設計自己框架時候多了一種技術選擇,更加的優雅
-
反射優化
執行時註解的使用可以減少很多程式碼的編寫,但是誰都知道這是有效能損耗的,不過權衡利弊,我們選擇了妥協,這個技術手段可以處理這個問題