上一篇博客討論了關(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):
平面設(shè)計(jì)培訓(xùn),網(wǎng)頁設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

(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í)到的)。
平面設(shè)計(jì)培訓(xùn),網(wǎng)頁設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

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