上一篇博客討論了關(guān)于注解的基礎(chǔ)知識(shí),以及運(yùn)行時(shí)(Runtime)通過反射機(jī)制來處理注解,但既然是Runtime,那么總會(huì)有效率上的損耗,如果我們能夠在編譯期(Compile time)就能處理注解,那自然更好,而很多框架其實(shí)都是在編譯期處理注解,比如大名鼎鼎的bufferknife,這個(gè)過程并不復(fù)雜,只需要我們自定義注解處理器(Annotation Processor)就可以了。(Annotation Processor下文有些地方直接簡稱處理器,不要理解成cpu那個(gè)處理器)。
在Compile time注解就能起作用,這才是真正體現(xiàn)注解價(jià)值的地方,不過自定義Compile time的注解處理器也沒什么神秘的。注解處理器是編譯器(javac)的一個(gè)工具,它用來在編譯時(shí)掃描和處理注解。我們可以自定義一個(gè)注解,并編寫和注冊對應(yīng)的處理器。在寫法上它其實(shí)就是我們自定義一個(gè)類,該類 extends javax.annotation.processing.AbstractProcessor
, AbstractProcessor
是一個(gè)abstract的基類。它以我們寫好的java源碼或者編譯好的代碼做為輸入,然后就可以通過處理器代碼來實(shí)現(xiàn)我們所希望的輸出了,比如輸出一份新的java代碼,此時(shí)注解管理器就以遞歸的形式進(jìn)行多趟處理,直到把代碼(包括你手寫的代碼,以及注解處理器生成的代碼)中所有的注解都被處理完畢。
我們已經(jīng)寫好的代碼固然是不能修改了,但是這并不影響通過注解處理器來生成新的代碼。還以bufferknife為例,寫findViewById實(shí)在太無聊了,所以我們就使用了bufferknife的注解方式省略這個(gè)過程。
public class TestMainActivity extends BaseActivity { @BindView(R.id.mainSwitchGoneBtn) Button goneBtn; ....... }
但是實(shí)際上呢,是bufferknife通過其注解處理器器來生成了相應(yīng)的代碼,它生成的文件是這樣的:
public class TestMainActivity_ViewBinding<T extends TestMainActivity> implements Unbinder { protected T target; @UiThread public TestMainActivity_ViewBinding(T target, View source) { this.target = target; target.goneBtn = Utils.findRequiredViewAsType(source, R.id.mainSwitchGoneBtn, "field 'goneBtn'", Button.class); } }
所以bufferknife就是通過這種方式來麻煩了自己,方便了我們。
注解處理器是運(yùn)行在它自己的虛擬機(jī)jvm當(dāng)中的,也就是說,javac啟動(dòng)了一個(gè)完整的java虛擬機(jī)來運(yùn)行注解處理器,這點(diǎn)非常重要,因?yàn)檫@說明你編寫的注解處理器代碼,和你寫的其他java代碼是沒什么區(qū)別的。不管是你使用的API,還是設(shè)計(jì)時(shí)的思想,編碼習(xí)慣,甚至你想使用的其他第三方類庫,框架等,都是一樣的。
認(rèn)識(shí)處理器
前面就說過,我們自定義的過程,就是extends AbstractProcessor
,先來看看這個(gè)抽象處理器類。
package com.yaoxiaowen.testprocessor;import java.util.Set;import javax.annotation.processing.AbstractProcessor;import javax.annotation.processing.ProcessingEnvironment;import javax.annotation.processing.RoundEnvironment;import javax.lang.model.SourceVersion;import javax.lang.model.element.TypeElement;public class TestProcessor extends AbstractProcessor{ /** * 每個(gè)注解處理器都必須有一個(gè)空的構(gòu)造方法(父類已經(jīng)實(shí)現(xiàn)了),這個(gè)init方法會(huì)被構(gòu)造器調(diào)用, * 并傳入一個(gè) ProcessingEnvironment 參數(shù),該參數(shù)提供了很多工具類, * 比如 Elements, Filer, Messager, Types * @author www.yaoxiaowen.com */ @Override public synchronized void init(ProcessingEnvironment env) { // TODO Auto-generated method stub super.init(env); } /** * 這個(gè)方法在父類中是abstract的,所以子類必須實(shí)現(xiàn)。 * 這個(gè)方法就是相當(dāng)于 注解處理器的 入口 main()方法,我們說在編譯時(shí),對注解進(jìn)行的處理, * 比如對注解的掃描,評估和處理,以及后續(xù)的我們要做的其他操作。(比如生成其他java代碼文件), * 都是在這里發(fā)生的。 * * 參數(shù)RoundEnvironment可以讓我們查出包含特定注解的被注解元素。 * @author www.yaoxiaowen.com */ @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // TODO Auto-generated method stub return false; } /** * 這個(gè)方法雖然在父類當(dāng)中不是 abstract的,但是我們也必須實(shí)現(xiàn)。 * 因?yàn)樵摲椒ǖ淖饔檬侵付ㄎ覀円幚砟男┳⒔獾模?nbsp; * 比如你想處理注解MyAnnotation,可是該處理器怎么知道你想處理MyAnnotation,而不是OtherAnnotation呢。 * 所以你要在這里指明,你需要處理的注解的全稱。 * * 返回值是一個(gè)字符串的集合,包含著本處理器想要處理的注解類型的合法全稱。 * @author www.yaoxiaowen.com */ @Override public Set<String> getSupportedAnnotationTypes() { // TODO Auto-generated method stub return super.getSupportedAnnotationTypes(); } /** * 本方法用來指明你支持的java版本, * 不過一般使用 SourceVersion.latestSupported() 就可以了。 */ @Override public SourceVersion getSupportedSourceVersion() { // TODO Auto-generated method stub return super.getSupportedSourceVersion(); } }
這幾個(gè)主要方法,在代碼片段的注釋已經(jīng)寫的很清楚了。
我們使用TestProcessor.java這個(gè)處理器的目的就是分析處理java代碼,而代碼是遵循一定的結(jié)構(gòu)規(guī)范的,代碼文件被讀取后,各個(gè)字符串會(huì)被分解成token進(jìn)行處理,而javac的編譯器首先將java代碼分解為抽象語法樹(AST)。而這個(gè)結(jié)構(gòu),在處理器內(nèi)部,其實(shí)是被表示成這樣的:
package com.example; // PackageElementpublic class Foo { // TypeElement private int a; // VariableElement private Foo other; // VariableElement public Foo () {} // ExecuteableElement public void setA ( // ExecuteableElement int newA // TypeElement ){} }
處理器在處理代碼時(shí),其實(shí)就是對抽象語法樹進(jìn)行遍歷操作,分解出每一個(gè)的類,方法,屬性等,然后再將這些元素的內(nèi)容進(jìn)行處理。
而實(shí)際上,這些PackageElement,VariableElement等元素模型都是在一個(gè)專門的類包中javax.lang.model
。javax.lang.model
用來為 Java 編程語言建立模型的包的類和層次結(jié)構(gòu)。 此包及其子包的成員適用于語言建模、語言處理任務(wù)和 API(包括但并不僅限于注釋處理框架)。
繼承 AbstractProcessor實(shí)現(xiàn)自定義處理器
我們現(xiàn)在通過繼承AbstractProcessor來實(shí)現(xiàn)一個(gè)小demo。
流程和功能如下:我們定義了一個(gè)注解SQLString
,然后實(shí)現(xiàn)注解處理器 DbProcessor
。該注解處理器功能很簡單,就是生成一個(gè)文件,將實(shí)現(xiàn)了SQLString
的屬性元素的相關(guān)內(nèi)容寫入到這個(gè)文件(比如所在類的名字,屬性名,所設(shè)置的注解的值)。
我們先自定義一個(gè)注解
package com.yaoxiaowen.comp.proce.db;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;import javax.lang.model.element.Element;/** * 該注解的 使用范圍是 屬性(域) 上 */@Target(ElementType.FIELD)@Retention(RetentionPolicy.CLASS)public @interface SQLString { int value() default 0; String name() default ""; }
然后再來定義注解處理器
package com.yaoxiaowen.comp.proce.db;import java.io.File;import java.io.FileWriter;import java.util.HashMap;import java.util.Map;import java.util.Set;import java.util.TreeSet;import javax.annotation.processing.AbstractProcessor;import javax.annotation.processing.Messager;import javax.annotation.processing.ProcessingEnvironment;import javax.annotation.processing.RoundEnvironment;import javax.lang.model.SourceVersion;import javax.lang.model.element.Element;import javax.lang.model.element.TypeElement;import javax.lang.model.element.VariableElement;import javax.tools.Diagnostic;/** * @author www.yaoxiaowen.com */public class DbProcessor extends AbstractProcessor{ private Messager messager; private int count = 0; private int forCount = 0; private StringBuilder generateStr = new StringBuilder(); @Override public synchronized void init(ProcessingEnvironment env) { // TODO Auto-generated method stub super.init(env); messager = env.getMessager(); String logStr = "enter init(), 進(jìn)入 init()"; printMsg(logStr); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) { // TODO Auto-generated method stub String logStr = "enter process(), 進(jìn)入process"; //用來 存儲(chǔ) (className, 輸出語句) 這種結(jié)構(gòu) Map<String, String> maps = new HashMap<>(); //得到 使用了 SQLString注解的元素 Set<? extends Element> eleStrSet = env.getElementsAnnotatedWith(SQLString.class); count++; for (Element eleStr : eleStrSet){ //因?yàn)槲覀冎繱QLString元素的使用范圍是在域上,所以這里我們進(jìn)行了強(qiáng)制類型轉(zhuǎn)換 VariableElement eleStrVari = (VariableElement)eleStr; forCount++; // 得到該元素的封裝類型,也就是 包裹它的父類型 TypeElement enclosingEle = (TypeElement)eleStrVari.getEnclosingElement(); String className = enclosingEle.getQualifiedName().toString(); generateStr.append("className = " + className); generateStr.append("\t fieldName = " + eleStrVari.getSimpleName().toString()); //得到在元素上,使用了注解的相關(guān)情況 SQLString sqlString = eleStrVari.getAnnotation(SQLString.class); generateStr.append("\t annotationName = " + sqlString.name()); generateStr.append("\t annotationValue = " + sqlString.value()); generateStr.append("\t forCount=" + forCount); generateStr.append("\n"); } generateStr.append("test File yaowen"); generateStr.append("\t count=" + count); generateFile(generateStr.toString()); return true; } @Override public Set<String> getSupportedAnnotationTypes() { // TODO Auto-generated method stub Set<String> strings = new TreeSet<>(); strings.add("com.yaoxiaowen.comp.proce.db.SQLString"); return strings; } @Override public SourceVersion getSupportedSourceVersion() { // TODO Auto-generated method stub return SourceVersion.latestSupported(); } //將內(nèi)容輸出到文件 private void generateFile(String str){ try { //這是mac環(huán)境下的路徑 File file = new File( "/Users/yw/code/dbCustomProcFile"); FileWriter fw = new FileWriter(file); fw.append(str); fw.flush(); fw.close(); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); printMsg(e.toString()); } } private void printMsg(String msg){ messager.printMessage(Diagnostic.Kind.ERROR, msg); } }
結(jié)合著注釋,我們知道,這個(gè)處理器的功能就是將一些信息輸出到 /Users/yw/code/dbCustomProcFile
這個(gè)文件中。
我在代碼中使用了 javax.annotation.processing.Messager
來輸出一些log信息,因?yàn)檫@個(gè)過程是在編譯時(shí)輸出的,所以System.out.println()
就沒用了,這個(gè)輸出信息是給使用了該處理器的第三方程序員看的,不是給該處理器的作者看的。
比如demo當(dāng)中的log代碼,在最后成功的打包成jar,在另一個(gè)項(xiàng)目中使用時(shí)(Android Studio環(huán)境下,Eclipse我愣是沒找到哪個(gè)窗口輸出編譯信息),編譯時(shí)期輸出信息如下:
.......:app:compileSc_360DebugJavaWithJavac注: enter init, 進(jìn)入init注: enter process, 進(jìn)入process注: Creating DefaultRealmModule注: enter process, 進(jìn)入process注: enter process, 進(jìn)入process注: 某些輸入文件使用了未經(jīng)檢查或不安全的操作。 注: 有關(guān)詳細(xì)信息, 請使用 -Xlint:unchecked 重新編譯。:app:generateJsonModelSc_360Debug UP-TO-DATE:app:externalNativeBuildSc_360Debug......
添加注冊信息
處理器的代碼雖然寫完了,但是這還沒完呢,剩下還有非常重要的步驟,那就是添加注冊信息。因?yàn)樽⒔馓幚砥魇菍儆趈avac的一個(gè)平臺(tái)級的功能,所以我們的使用方式是將代碼打包成jar的形式,這樣就可以在其他第三方項(xiàng)目當(dāng)中使用了。而在打包jar之前,則要在項(xiàng)目中添加注冊信息。
先看一下這個(gè)目錄的結(jié)構(gòu):
(eclipse)注冊的步驟如下:
1,選中工程,鼠標(biāo)右鍵,New -> Source Folder,創(chuàng)建 resources文件夾,然后依次通過New -> Folder 創(chuàng)建兩個(gè)文件夾 : META-INF,services
2,在services文件夾下,New -> File, 創(chuàng)建一個(gè)文件,javax.annotation.processing.Processor。在文件中,輸入自定義的處理器的全名:com.yaoxiaowen.comp.proce.db.DbProcessor
輸入之后記得鍵入回車一下。
其實(shí)這個(gè)手動(dòng)注冊的過程,也是可以不用我們麻煩的。google開發(fā)了一個(gè)注解工具AutoService,我們可以直接在處理器代碼上使用。類似下面這樣:
/** * @author www.yaoxiaowen.com */@AutoService(Processor.class)public class DbProcessor extends AbstractProcessor{ .......
這個(gè)注解工具自動(dòng)生成META-INF/services/javax.annotation.processing.Processor文件,文件里還包含了處理器的全名:com.yaoxiaowen.comp.proce.db.DbProcessor
看到這里,你也許會(huì)比較震驚,我們在注解處理器的代碼中也可以使用注解。
那么此時(shí)請?jiān)倏纯幢疚拈_頭的那句話
注解處理器是運(yùn)行在它自己的虛擬機(jī)jvm當(dāng)中的,也就是說,javac啟動(dòng)了一個(gè)完整的java虛擬機(jī)來運(yùn)行注解處理器.....
做完這些,我們的項(xiàng)目就已經(jīng)完成了,下面要做的就是打包成jar了。
打包和使用jar(eclipse為例)
1: 打包jar
前面說過,編譯期的注解處理器是平臺(tái)級的功能,是要注冊給javac的, 所以需要打包成jar, 我們的項(xiàng)目打包的名字是
AnnoCustomProce.jar
關(guān)于具體的打包過程,參見gif圖(這是從鴻洋大神的博客上學(xué)習(xí)到的)。
2: 建立新項(xiàng)目
eclipse下的java項(xiàng)目,新建立一個(gè)lib文件夾,然后將AnnoCustomProce.jar手動(dòng)拷貝到這個(gè)目錄下。
3: 引用包,并啟用annotation processor。
具體操作見gif圖。
http://www.cnblogs.com/yaoxiaowen/p/6753964.html