编译时注解之APT
首發(fā)于我的公眾號
編譯時注解之APT
0x00 概述
注解系列
- 注解基礎
 - JavaPoet
 - 編譯期注解處理之APT
 
前一篇介紹了注解的基本知識以及常見用法,由于運行期(RunTime)利用反射去獲取信息還是比較損耗性能的,本篇將介紹一種使用注解更加優(yōu)雅的方式,編譯期(Compile time)注解,以及處理編譯期注解的手段APT和Javapoet,限于篇幅,本篇著重介紹APT 首先你的注解需要聲明為CLASS @Retention(RetentionPolicy.CLASS)
編譯期解析注解基本原理: 在某些代碼元素上(如類型、函數、字段等)添加注解,在編譯時編譯器會檢查AbstractProcessor的子類,并且調用該類型的process函數,然后將添加了注解的所有元素都傳遞到process函數中,使得開發(fā)人員可以在編譯器進行相應的處理,例如,根據注解生成新的Java類,這也就是ButterKnife等開源庫的基本原理。
0x01 APT
在處理編譯器注解的第一個手段就是APT(Annotation Processor Tool),即注解處理器。在java5的時候已經存在,但是java6開始的時候才有可用的API,最近才隨著butterknife這些庫流行起來。本章將闡述什么是注解處理器,以及如何使用這個強大的工具。
什么是APT
APT是一種處理注解的工具,確切的說它是javac的一個工具,它用來在編譯時掃描和處理注解,一個注解的注解處理器,以java代碼(或者編譯過的字節(jié)碼)作為輸入,生成.java文件作為輸出,核心是交給自己定義的處理器去處理,
如何使用
每個自定義的處理器都要繼承虛處理器,實現其關鍵的幾個方法
- 繼承虛處理器 AbstractProcessor
 
下面重點介紹下這幾個函數:
- 注冊 處理器
 
由于處理器是javac的工具,因此我們必須將我們自己的處理器注冊到javac中,在以前我們需要提供一個.jar文件,打包你的注解處理器到此文件中,并在在你的jar中,需要打包一個特定的文件 javax.annotation.processing.Processor到META-INF/services路徑下 把MyProcessor.jar放到你的builpath中,javac會自動檢查和讀取javax.annotation.processing.Processor中的內容,并且注冊MyProcessor作為注解處理器。
超級麻煩有木有,不過不要慌,谷歌baba給我們開發(fā)了AutoService注解,你只需要引入這個依賴,然后在你的解釋器第一行加上
@AutoService(Processor.class) 復制代碼然后就可以自動生成META-INF/services/javax.annotation.processing.Processor文件的。省去了打jar包這些繁瑣的步驟。
APT中的Elements和TypeMirrors
在前面的init()中我們可以獲取如下引用
- Elements:一個用來處理Element的工具類
 - Types:一個用來處理TypeMirror的工具類
 - Filer:正如這個名字所示,使用Filer你可以創(chuàng)建文件(通常與javapoet結合)
 
在注解處理過程中,我們掃面所有的Java源文件。源文件的每一個部分都是一個特定類型的Element
先來看一下Element
對于編譯器來說 代碼中的元素結構是基本不變的,如,組成代碼的基本元素包括包、類、函數、字段、變量的等,JDK為這些元素定義了一個基類也就是Element類
Element有五個直接子類,分別代表一種特定類型
==
| TypeParameterElement | 表示一般類、接口、方法或構造方法元素的泛型參數 | 
| TypeElement | 表示一個類或接口程序元素 | 
| VariableElement | 表示一個字段、enum 常量、方法或構造方法參數、局部變量或異常參數 | 
| ExecutableElement | 表示某個類或接口的方法、構造方法或初始化程序(靜態(tài)或實例),包括注解類型元素 | 
==
開發(fā)中Element可根據實際情況強轉為以上5種中的一種,它們都帶有各自獨有的方法,如下所示
package com.example; // PackageElementpublic class Test { // TypeElementprivate int a; // VariableElementprivate Test other; // VariableElementpublic Test () {} // ExecuteableElementpublic void setA ( // ExecuteableElementint newA // TypeElement) {} } 復制代碼再舉個栗子?:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) public @interface Test {String value(); } 復制代碼這個注解因為只能作用于函數類型,因此,它對應的元素類型就是ExecutableElement當我們想通過APT處理這個注解的時候就可以獲取目標對象上的Test注解,并且將所有這些元素轉換為ExecutableElement元素,以便獲取到他們對應的信息。
查看其代碼定義
定義如下:
** * 表示一個程序元素,比如包、類或者方法,有如下幾種子接口: * ExecutableElement:表示某個類或接口的方法、構造方法或初始化程序(靜態(tài)或實例),包括注解類型元素 ; * 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>"; * 如果它表示一個靜態(tài)初始化程序,則返回名稱 "<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);//訪問克而執(zhí)行元素R visitExecutable(ExecutableElement e, P p);//訪問參數元素R visitTypeParameter(TypeParameterElement e, P p);//處理位置的元素類型,這是為了應對后續(xù)Java語言的擴折而預留的接口,例如后續(xù)元素類型添加了,那么通過這個接口就可以處理上述沒有聲明的類型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提供給注解處理器一個報告錯誤、警告以及提示信息的途徑。它不是注解處理器開發(fā)者的日志工具,而是用來寫一些信息給使用此注解器的第三方開發(fā)者的。在官方文檔中描述了消息的不同級別中非常重要的是Kind.ERROR,因為這種類型的信息用來表示我們的注解處理器處理失敗了。很有可能是第三方開發(fā)者錯誤的使用了注解。這個概念和傳統(tǒng)的Java應用有點不一樣,在傳統(tǒng)Java應用中我們可能就拋出一個異常Exception。如果你在process()中拋出一個異常,那么運行注解處理器的JVM將會崩潰(就像其他Java應用一樣),使用我們注解處理器第三方開發(fā)者將會從javac中得到非常難懂的出錯信息,因為它包含注解處理器的堆棧跟蹤(Stacktace)信息。因此,注解處理器就有一個Messager類,它能夠打印非常優(yōu)美的錯誤信息。除此之外,你還可以連接到出錯的元素。在像現在的IDE(集成開發(fā)環(huán)境)中,第三方開發(fā)者可以直接點擊錯誤信息,IDE將會直接跳轉到第三方開發(fā)者項目的出錯的源文件的相應的行。
- Types
 
Types是一個用來處理TypeMirror的工具
- Elements
 
Elements是一個用來處理Element的工具
0x03 優(yōu)缺點
優(yōu)點(結合javapoet)
- 對代碼進行標記、在編譯時收集信息并做處理
 - 生成一套獨立代碼,輔助代碼運行
 
缺點
- 可以自動生成代碼,但在運行時需要主動調用
 - 如果要生成代碼需要編寫模板函數
 
0x04 其他
通常我們需要分離處理器和注解 這樣做的原因是,在發(fā)布程序時注解及生成的代碼會被打包到用戶程序中,而注解處理器則不會(注解處理器是在編譯期在JVM上運行跟運行時無關)。要是不分離的話,假如注解處理器中使用到了其他第三方庫,那就會占用系統(tǒng)資源,特別是方法數,
該技術可以讓我們在設計自己框架時候多了一種技術選擇,更加的優(yōu)雅
反射優(yōu)化
運行時注解的使用可以減少很多代碼的編寫,但是誰都知道這是有性能損耗的,不過權衡利弊,我們選擇了妥協(xié),這個技術手段可以處理這個問題
0x05 參考文獻
-  
hannesdorfmann.com/annotation-…
 -  
blog.csdn.net/github_3518…
 -  
www.zhangningning.com.cn/blog/Androi…
 -  
docs.oracle.com/javase/7/do…
歡迎關注我的公眾號,一起學習,共同提高~ 復制代碼 
總結
                            
                        - 上一篇: Basic Calculator II
 - 下一篇: 传统反病毒软件厂商学会新把戏