1. 程式人生 > >基礎篇:帶你從頭到尾玩轉註解

基礎篇:帶你從頭到尾玩轉註解

前面寫了Android 開發:由模組化到元件化(一),很多小夥伴來問怎麼沒有Demo啊?之所以沒有立刻放demo的原因在還有許多技術點沒說完.

今天我們就來細細評味Java當中Annotation,也就是我們常說的註解.

本文按照以下順序進行:元資料->元註解->執行時註解->編譯時註解處理器.

什麼是元資料(metadata)

元資料由metadata譯來,所謂的元資料就是“關於資料的資料”,更通俗的說就是描述資料的資料,對資料及資訊資源的描述性資訊.比如說一個文字檔案,有建立時間,建立人,檔案大小等資料,這都可以理解為是元資料.

在java中,元資料以標籤的形式存在java程式碼中,它的存在並不影響程式程式碼的編譯和執行,通常它被用來生成其它的檔案或執行時知道被執行程式碼的描述資訊。java當中的javadoc和註解都屬於元資料.

什麼是註解(Annotation)?

註解是從java 5.0開始加入,可以用於標註包,類,方法,變數等.比如我們常見的@Override,再或者Android原始碼中的@hide,@systemApi,@privateApi等

對於@Override,多數人往往都是知其然而不知其所以然,今天我就來聊聊Annotation背後的祕密,開始正文.

元註解

元註解就是定義註解的註解,是java提供給我們用於定義註解的基本註解.在java.lang.annotation包中我們可以看到目前元註解共有以下幾個:

  1. @Retention
  2. @Target
  3. @Inherited
  4. @Documented
  5. @interface

下面我們將集合@Override註解來解釋著5個基本註解的用法.

@interface

@interface是java中用於宣告註解類的關鍵字.使用該註解表示將自動繼承java.lang.annotation.Annotation類,該過程交給編譯器完成.

因此我們想要定義一個註解只需要如下做即可,以@Override註解為例

public @interface Override {
}

需要注意:在定義註解時,不能繼承其他註解或介面.

@Retention

@Retention:該註解用於定義註解保留策略,即定義的註解類在什麼時候存在(原始碼階段 or 編譯後 or 執行階段).該註解接受以下幾個引數:RetentionPolicy.SOURCE,RetentionPolicy.CLASS,RetentionPolicy.RUNTIME

,其具體使用及含義如下:

註解保留策略 含義
@Retention(RetentionPolicy.SOURCE) 註解僅在原始碼中保留,class檔案中不存在
@Retention(RetentionPolicy.CLASS) 註解在原始碼和class檔案中都存在,但執行時不存在,即執行時無法獲得,該策略也是預設的保留策略
@Retention(RetentionPolicy.RUNTIME) 註解在原始碼,class檔案中存在且執行時可以通過反射機制獲取到

來看一下@Override註解的保留策略:

@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

這表明@Override註解只在原始碼階段存在,javac在編譯過程中去去掉該註解.

@Target

該註解用於定義註解的作用目標,即註解可以用在什麼地方,比如是用於方法上還是用於欄位上,該註解接受以下引數:

作用目標 含義
@Target(ElementType.TYPE) 用於介面(註解本質上也是介面),類,列舉
@Target(ElementType.FIELD) 用於欄位,列舉常量
@Target(ElementType.METHOD) 用於方法
@Target(ElementType.PARAMETER) 用於方法引數
@Target(ElementType.CONSTRUCTOR) 用於構造引數
@Target(ElementType.LOCAL_VARIABLE) 用於區域性變數
@Target(ElementType.ANNOTATION_TYPE) 用於註解
@Target(ElementType.PACKAGE) 用於包

以@Override為例,不難看出其作用目標為方法:

@Target(ElementType.METHOD)
public @interface Override {
}

到現在,通過@interface,@Retention,@Target已經可以完整的定義一個註解,來看@Override完整定義:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

@Inherited

預設情況下,我們自定義的註解用在父類上不會被子類所繼承.如果想讓子類也繼承父類的註解,即註解在子類也生效,需要在自定義註解時設定@Inherited.一般情況下該註解用的比較少.

@Documented

該註解用於描述其它型別的annotation應該被javadoc文件化,出現在api doc中.
比如使用該註解的@Target會出出現在api說明中.

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {

    ElementType[] value();
}

這裡寫圖片描述

藉助@Interface,@Target,@Retention,@Inherited,@Documented這五個元註解,我們就可以自定義註解了,其中前三個註解是任何一個註解都必備具備的.

你以為下面會直接來將如何自定義註解嘛?不,你錯了,我們還是來聊聊java自帶的幾個註解.

系統註解

java設計者已經為我們自定義了幾個常用的註解,我們稱之為系統註解,主要是這三個:

系統註解 含義
@Override 用於修飾方法,表示此方法重寫了父類方法
@Deprecated 用於修飾方法,表示此方法已經過時
@SuppressWarnnings 該註解用於告訴編譯器忽視某類編譯警告

如果你已經完全知道這三者的用途,跳過這一小節,直接往下看.

@Override

它用作標註方法,說明被標註的方法重寫了父類的方法,其功能類似斷言.如果在一個沒有重寫父類方法的方法上使用該註解,java編譯器將會以一個編譯錯誤提示:
這裡寫圖片描述

@Deprecated

當某個型別或者成員使用該註解時意味著
編譯器不推薦開發者使用被標記的元素.另外,該註解具有”傳遞性”,子類中重寫該註解標記的方法,儘管子類中的該方法未使用該註解,但編譯器仍然報警.

public class SimpleCalculator {

    @Deprecated
    public int add(int x, int y) {
        return x+y;
    }
}

public class MultiplCalculator extends SimpleCalculator {
    // 重寫SimpleCalculator中方法,但不使用@Deprecated
    public int add(int x, int y) {
        return  Math.abs(x)+Math.abs(y);
    }
}

//test code
public class Main {

    public static void main(String[] args) {
        new SimpleCalculator().add(3, 4);
        new MultiplCalculator().add(3,5);
    }
}

對於像new SimpleCalculator().add(3,4)這種直接呼叫的,Idea會直接提示,而像第二種則不是直接提示:
這裡寫圖片描述

但是在編譯過程中,編譯器都會警告:

這裡寫圖片描述

需要注意@Deprecated和@deprecated這兩者的區別,前者被javac識別和處理,而後者則是被javadoc工具識別和處理.因此當我們需要在原始碼標記某個方法已經過時應該使用@Deprecated,如果需要在文件中說明則使用@deprecated,因此可以這麼:

public class SimpleCalculator {
    /**
     * @param x
     * @param y
     * @return
     * 
     * @deprecated deprecated As of version 1.1,
     * replace by <code>SimpleCalculator.add(double x,double y)</code>
     */
    @Deprecated
    public int add(int x, int y) {
        return x+y;
    }

    public double add(double x,double y) {
        return x+y;
    }

}

@SuppressWarnning

該註解被用於有選擇的關閉編譯器對類,方法,成員變數即變數初始化的警告.該註解可接受以下引數:

引數 含義
deprecated 使用已過時類,方法,變數
unchecked 執行了未檢查的轉告時的警告,如使用集合是為使用泛型來制定集合儲存時的型別
fallthrough 使用switch,但是沒有break時
path 類路徑,原始檔路徑等有不存在的路徑
serial 可序列化的類上缺少serialVersionUID定義時的警告
finally 任何finally字句不能正常完成時的警告
all 以上所有情況的警告

滋溜一下,我們飛過了2016年,不,是看完了上一節.繼續往下飛.

自定義註解

瞭解完系統註解之後,現在我們就可以自己來定義註解了,通過上面@Override的例項,不難看出定義註解的格式如下:

public @interface 註解名 {定義體}

定義體就是方法的集合,每個方法實則是聲明瞭一個配置引數.方法的名稱作為配置引數的名稱,方法的返回值型別就是配置引數的型別.和普通的方法不一樣,可以通過default關鍵字來宣告配置引數的預設值.

需要注意:

  1. 此處只能使用public或者預設的defalt兩個許可權修飾符
  2. 配置引數的型別只能使用基本型別(byte,boolean,char,short,int,long,float,double)和String,Enum,Class,annotation
  3. 對於只含有一個配置引數的註解,引數名建議設定中value,即方法名為value
  4. 配置引數一旦設定,其引數值必須有確定的值,要不在使用註解的時候指定,要不在定義註解的時候使用default為其設定預設值,對於非基本型別的引數值來說,其不能為null.

像@Override這樣,沒有成員定義的註解稱之為標記註解.

現在我們來自定義個註解@UserMeta,這個註解目前並沒啥用,就是為了演示一番:

@Documented
@Target(ElementType.CONSTRUCTOR)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserMeta {
    public int id() default 0;

    public String name() default "";

    public int age() default ;
}

有了米飯,沒有筷子沒法吃啊(手抓飯的走開),下面來看看如何處理註解.

註解處理器

上面我們已經學會了如何定義註解,要想註解發揮實際作用,需要我們為註解編寫相應的註解處理器.根據註解的特性,註解處理器可以分為執行時註解處理和編譯時註解處理器.執行時處理器需要藉助反射機制實現,而編譯時處理器則需要藉助APT來實現.

無論是執行時註解處理器還是編譯時註解處理器,主要工作都是讀取註解及處理特定註解,從這個角度來看註解處理器還是非常容易理解的.

先來看看如何編寫執行時註解處理器.

執行時註解處理器

熟悉java反射機制的同學一定對java.lang.reflect包非常熟悉,該包中的所有api都支援讀取執行時Annotation的能力,即屬性為@Retention(RetentionPolicy.RUNTIME)的註解.

在java.lang.reflect中的AnnotatedElement介面是所有程式元素的(Class,Method)父介面,我們可以通過反射獲取到某個類的AnnotatedElement物件,進而可以通過該物件提供的方法訪問Annotation資訊,常用的方法如下:

方法 含義
<T extends Annotation> T getAnnotation(Class<T> annotationClass) 返回該元素上存在的制定型別的註解
Annotation[] getAnnotations() 返回該元素上存在的所有註解
default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) 返回該元素指定型別的註解
default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) 返回直接存在與該元素上的所有註釋
default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) 返回直接存在該元素岸上某型別的註釋
Annotation[] getDeclaredAnnotations() 返回直接存在與該元素上的所有註釋

編寫執行時註解大體就需要了解以上知識點,下面來做個小實驗.

簡單示例

首先我們用一個簡單的例項來介紹如何編寫執行時註解處理器:我們的系統中存在一個User實體類:

public class User {
    private int id;
    private int age;
    private String name;

    @UserMeta(id=1,name="dong",age = 10)
    public User() {
    }


    public User(int id, int age, String name) {
        this.id = id;
        this.age = age;
        this.name = name;
    }

  //...省略setter和getter方法

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

我們希望可以通過@UserMeta(id=1,name="dong",age = 10)(這個註解我們在上面提到了)來為設定User例項的預設值。

自定義註解類如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.CONSTRUCTOR)
public @interface UserMeta {
    public int id() default 0;

    public String name() default "";

    public int age() default 0;
}

該註解類作用於構造方法,並在執行時存在,這樣我們就可以在執行時通過反射獲取註解進而為User例項設值,看看如何處理該註解吧.

執行時註解處理器:

public class AnnotationProcessor {

    public static void init(Object object) {

        if (!(object instanceof User)) {
            throw new IllegalArgumentException("[" + object.getClass().getSimpleName() + "] isn't type of User");
        }

        Constructor[] constructors = object.getClass().getDeclaredConstructors();
        for (Constructor constructor : constructors) {
            if (constructor.isAnnotationPresent(UserMeta.class)) {
                UserMeta userFill = (UserMeta) constructor.getAnnotation(UserMeta.class);
                int age = userFill.age();
                int id = userFill.id();
                String name = userFill.name();
                ((User) object).setAge(age);
                ((User) object).setId(id);
                ((User) object).setName(name);
            }
        }
    }
}


測試程式碼:

public class Main {

    public static void main(String[] args) {
        User user = new User();
        AnnotationProcessor.init(user);
        System.out.println(user.toString());
    }
}

執行測試程式碼,便得到我們想要的結果:

User{id=1, age=10, name=’dong’}

這裡通過反射獲取User類宣告的構造方法,並檢測是否使用了@UserMeta註解。然後從註解中獲取引數值並將其賦值給User物件。

正如上面提到,執行時註解處理器的編寫本質上就是通過反射獲取註解資訊,隨後進行其他操作。編譯一個執行時註解處理器就是這麼簡單。執行時註解通常多用於引數配置類模組。

自己動手編寫ButterKnife

對從事Android開發的小夥伴而言,ButterKnife可謂是神兵利器,能極大的減少我們書寫findViewById(XXX).現在,我們就利用剛才所學的執行時註解處理器來編寫一個簡化版的ButterKnife。

自定義註解:

//該註解用於配置layout資源
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentView {
    int value();//只有一個返回時,可用value做名稱,這樣在使用的時候就不需要使用的名稱進行標誌
}

//該註解用於配置控制元件ID
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {
    int id();
    boolean clickable() default  false;
}

自定義執行時註解:

public class ButterKnife {

    //view控制元件
    public static void initViews(Object object, View sourceView){
        //獲取該類宣告的成員變數
        Field[] fields = object.getClass().getDeclaredFields();
        for (Field field : fields){
            //獲取該成員變數上使用的ViewInject註解
            ViewInject viewInject = field.getAnnotation(ViewInject.class);
            if(viewInject != null){
                int viewId = viewInject.id();//獲取id引數值
                boolean clickable = viewInject.clickable();//獲取clickable引數值
                if(viewId != -1){
                    try {
                        field.setAccessible(true);
                        field.set(object, sourceView.findViewById(viewId));
                        if(clickable == true){
                            sourceView.findViewById(viewId).setOnClickListener((View.OnClickListener) (object));
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    //佈局資源
    public static void initLayout(Activity activity){
        Class<? extends Activity> activityClass =  activity.getClass();
        ContentView contentView = activityClass.getAnnotation(ContentView.class);
        if(contentView != null){
            int layoutId = contentView.value();
            try {
                //反射執行setContentView()方法
                Method method = activityClass.getMethod("setContentView", int.class);
                method.invoke(activity, layoutId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }


    public static void init(Activity activity) {
        initLayout(activity);
        initViews(activity,activity.getWindow().getDecorView());
    }
}

測試程式碼:

@ContentView(id=R.layout.activity_main)
public class MainActivity extends Activity implements View.OnClickListener {

    @ViewInject(id=R.id.tvDis,clickable = true)
    private TextView tvDis;

    @ViewInject(id=R.id.btnNew,clickable =true)
    private Button btnNew;

    @ViewInject(id =R.id.btnScreenShot,clickable = true)
    private Button btnScreenShot;

    @ViewInject(id =R.id.imgContainer)
    private ImageView imgContainer;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        AnnotationUtil.inJect(this);

    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.tvDis:
                break;
            case R.id.btnNew:
                break;
            case R.id.btnScreenShot:
                break;
        }
    }
}

一個簡單的ButterKnife就實現了,是不是非常簡單。下面我們就進入本文的最重要的一點:編譯時註解處理器。

編譯時註解處理器

不同於執行時註解處理器,編寫編譯時註解處理器(Annotation Processor Tool).

APT用於在編譯時期掃描和處理註解資訊.一個特定的註解處理器可以以java原始碼檔案或編譯後的class檔案作為輸入,然後輸出另一些檔案,可以是.java檔案,也可以是.class檔案,但通常我們輸出的是.java檔案.(注意:並不是對原始檔修改).如果輸出的是.java檔案,這些.java檔案回合其他原始碼檔案一起被javac編譯.

你可能很納悶,註解處理器是到底是在什麼階段介入的呢?好吧,其實是在javac開始編譯之前,這也就是通常我們為什麼願意輸出.java檔案的原因.

註解最早是在java 5引入,主要包含apt和com.sum.mirror包中相關mirror api,此時apt和javac是各自獨立的。從java 6開始,註解處理器正式標準化,apt工具也被直接整合在javac當中。

我們還是回到如何編寫編譯時註解處理器這個話題上,編譯一個編譯時註解處理主要分兩步:

  1. 繼承AbstractProcessor,實現自己的註解處理器
  2. 註冊處理器,並打成jar包

看起來很簡單不是麼?來慢慢的看看相關的知識點吧.

自定義註解處理器

首先來看一下一個標準的註解處理器的格式:

public class MyAnnotationProcessor extends AbstractProcessor {

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
}

來簡單的瞭解下其中5個方法的作用

方法 作用
init(ProcessingEnvironment processingEnv) 該方法有註解處理器自動呼叫,其中ProcessingEnvironment類提供了很多有用的工具類:Filter,Types,Elements,Messager等
getSupportedAnnotationTypes() 該方法返回字串的集合表示該處理器用於處理那些註解
getSupportedSourceVersion() 該方法用來指定支援的java版本,一般來說我們都是支援到最新版本,因此直接返回SourceVersion.latestSupported()即可
process(Set annotations, RoundEnvironment roundEnv) 該方法是註解處理器處理註解的主要地方,我們需要在這裡寫掃描和處理註解的程式碼,以及最終生成的java檔案。其中需要深入的是RoundEnvironment類,該用於查找出程式元素上使用的註解

編寫一個註解處理器首先要對ProcessingEnvironment和RoundEnvironment非常熟悉。接下來我們一覽這兩個類的風采.首先來看一下ProcessingEnvironment類:

public interface ProcessingEnvironment {

    Map<String,String> getOptions();

    //Messager用來報告錯誤,警告和其他提示資訊
    Messager getMessager();

    //Filter用來建立新的原始檔,class檔案以及輔助檔案
    Filer getFiler();

    //Elements中包含用於操作Element的工具方法
    Elements getElementUtils();

     //Types中包含用於操作TypeMirror的工具方法
    Types getTypeUtils();

    SourceVersion getSourceVersion();

    Locale getLocale();
}

重點來認識一下Element,Types和Filer。Element(元素)是什麼呢?

Element

element表示一個靜態的,語言級別的構件。而任何一個結構化文件都可以看作是由不同的element組成的結構體,比如XML,JSON等。這裡我們用XML來示例:

<root>
  <child>
    <subchild>.....</subchild>
  </child>
</root>

這段xml中包含了三個元素:<root>,<child>,<subchild>,到現在你已經明白元素是什麼。對於java原始檔來說,他同樣是一種結構化文件:

package com.closedevice;             //PackageElement

public class Main{                  //TypeElement
    private int x;                  //VariableElement

    private Main(){                 //ExecuteableElement

    }

    private void print(String msg){ //其中的引數部分String msg為TypeElement

    }

}

對於java原始檔來說,Element代表程式元素:包,類,方法都是一種程式元素。另外如果你對網頁解析工具jsoup熟悉,你會覺得操作此處的element是非常容易,關於jsoup不在本文講解之內。

接下來看看看各種Element之間的關係圖圖,以便有個大概的瞭解:
這裡寫圖片描述

元素 含義
VariableElement 代表一個 欄位, 列舉常量, 方法或者構造方法的引數, 區域性變數及 異常引數等元素
PackageElement 代表包元素
TypeElement 代表類或介面元素
ExecutableElement 程式碼方法,建構函式,類或介面的初始化程式碼塊等元素,也包括註解型別元素
TypeMirror

這三個類也需要我們重點掌握:
DeclaredType代表宣告型別:類型別還是介面型別,當然也包括引數化型別,比如Set<String>,也包括原始型別

TypeElement代表類或介面元素,而DeclaredType代表類型別或介面型別。

TypeMirror代表java語言中的型別.Types包括基本型別,宣告型別(類型別和介面型別),陣列,型別變數和空型別。也代表通配型別引數,可執行檔案的簽名和返回型別等。TypeMirror類中最重要的是getKind()方法,該方法返回TypeKind型別,為了方便大家理解,這裡附上其原始碼:

public enum TypeKind {
    BOOLEAN,BYTE,SHORT,INT,LONG,CHAR,FLOAT,DOUBLE,VOID,NONE,NULL,ARRAY,DECLARED,ERROR,  TYPEVAR,WILDCARD,PACKAGE,EXECUTABLE,OTHER,UNION,INTERSECTION;

    public boolean isPrimitive() {
        switch(this) {
        case BOOLEAN:
        case BYTE:
        case SHORT:
        case INT:
        case LONG:
        case CHAR:
        case FLOAT:
        case DOUBLE:
            return true;

        default:
            return false;
        }
    }
}

簡單來說,Element代表原始碼,TypeElement代表的是原始碼中的型別元素,比如類。雖然我們可以從TypeElement中獲取類名,TypeElement中不包含類本身的資訊,比如它的父類,要想獲取這資訊需要藉助TypeMirror,可以通過Element中的asType()獲取元素對應的TypeMirror。

然後來看一下RoundEnvironment,這個類比較簡單,一筆帶過:

public interface RoundEnvironment {

    boolean processingOver();

     //上一輪註解處理器是否產生錯誤
    boolean errorRaised();

     //返回上一輪註解處理器生成的根元素
    Set<? extends Element> getRootElements();

   //返回包含指定註解型別的元素的集合
    Set<? extends Element> getElementsAnnotatedWith(TypeElement a);

    //返回包含指定註解型別的元素的集合
    Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a);
}

Filer

Filer用於註解處理器中建立新檔案。具體用法在下面示例會做演示.另外由於Filer用起來實在比較麻煩,後面我們會使用javapoet簡化我們的操作.

好了,關於AbstractProcessor中一些重要的知識點我們已經看完了.假設你現在已經編寫完一個註解處理器了,下面,要做什麼呢?

打包並註冊.

自定義的處理器如何才能生效呢?為了讓java編譯器或能夠找到自定義的註解處理器我們需要對其進行註冊和打包:自定義的處理器需要被打成一個jar,並且需要在jar包的META-INF/services路徑下中建立一個固定的檔案javax.annotation.processing.Processor,在javax.annotation.processing.Processor檔案中需要填寫自定義處理器的完整路徑名,有幾個處理器就需要填寫幾個。

從java 6之後,我們只需要將打出的jar防止到專案的buildpath下即可,javac在執行的過程會自動檢查javax.annotation.processing.Processor註冊的註解處理器,並將其註冊上。而java 5需要單獨使用apt工具,java 5想必用的比較少了,就略過吧.

到現在為止,已經大體的介紹了與註解處理器相關的一些概念,最終我們需要獲得是一個包含註解處理器程式碼的jar包.

接下來,來實踐一把.

簡單例項

用個簡單的示例,來演示如何在Gradle來建立一個編譯時註解處理器,為了方便起見,這裡就直接藉助Android studio.當然你也可以採用maven構建.

首先建立AnnotationTest工程,在該工程內建立apt moudle.需要注意,AbstractProcessor是在javax包中,而android 核心庫中不存在該包,因此在選擇建立moudle時需要選擇java Library:
這裡寫圖片描述

此時專案結構如下:
這裡寫圖片描述

接下在我們在apt下建立annotation和processor子包,其中annotation用於存放我們自定義的註解,而processor則用於存放我們自定義的註解處理器.

先來個簡單的,自定義@Print註解:該註解最終的作用是輸出被註解的元素:

@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})  
@Retention(RetentionPolicy.CLASS)                                  
public @interface Print {                                     
}

接下來為其編寫註解處理器:

public class PrintProcessor extends AbstractProcessor {

    private Messager mMessager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mMessager = processingEnvironment.getMessager();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (TypeElement te : annotations) {
            for (Element e : roundEnv.getElementsAnnotatedWith(te)) {//find special annotationed element
                print(e.toString());//print element
            }
        }
        return true;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {

        return SourceVersion.latestSupported();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        LinkedHashSet<String> annotations = new LinkedHashSet<>();
        annotations.add(Print.class.getCanonicalName());
        return super.getSupportedAnnotationTypes();
    }

    private void print(String msg) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, msg);
    }
}

現在我們完成了一個簡單的註解.在編譯階段,編譯器將會輸出被註解元素的資訊.由於我們是在Gradle環境下,因此該資訊將在Gradle Console下輸出.

接下來我們編寫一個稍微難點的註解@Code:該註解會生成一個指定格式的類,先看看該註解的定義:

@Retention(CLASS)
@Target(METHOD)
public @interface Code {
    public String author();
    public String date() default "";
}

接下來,我們需要為其編寫註解處理器,程式碼比較簡單,直接來看:

public class CodeProcessor extends AbstractProcessor {

    private final String SUFFIX = "$WrmRequestInfo";

    private Messager mMessager;
    private Filer mFiler;
    private Types mTypeUtils;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mMessager = processingEnvironment.getMessager();
        mFiler = processingEnvironment.getFiler();
        mTypeUtils = processingEnvironment.getTypeUtils();

    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        LinkedHashSet<String> annotations = new LinkedHashSet<>();
        annotations.add(Code.class.getCanonicalName());
        return annotations;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        for (Element e : roundEnvironment.getElementsAnnotatedWith(Code.class)) {//find special annotationed element
            Code ca = e.getAnnotation(Code.class);
            TypeElement clazz = (TypeElement) e.getEnclosingElement();
            try {
                generateCode(e, ca, clazz);
            } catch (IOException x) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                        x.toString());
                return false
            }
        }
        return true;
    }

    //generate 
    private void generateCode(Element e, Code ca, TypeElement clazz) throws IOException {
        JavaFileObject f = mFiler.createSourceFile(clazz.getQualifiedName() + SUFFIX);
        mMessager.printMessage(Diagnostic.Kind.NOTE, "Creating " + f.toUri());
        Writer w = f.openWriter();
        try {
            String pack = clazz.getQualifiedName().toString();
            PrintWriter pw = new PrintWriter(w);
            pw.println("package " + pack.substring(0, pack.lastIndexOf('.')) + ";"); //create package element
            pw.println("\n class " + clazz.getSimpleName() + "Autogenerate {");//create class element
            pw.println("\n    protected " + clazz.getSimpleName() + "Autogenerate() {}");//create class construction
            pw.println("    protected final void message() {");//create method
            pw.println("\n//" + e);
            pw.println("//" + ca);
            pw.println("\n        System.out.println(\"author:" + ca.author() + "\");");
            pw.println("\n        System.out.println(\"date:" + ca.date() + "\");");
            pw.println("    }");
            pw.println("}");
            pw.flush();
        } finally {
            w.close();
        }
    }

}

核心內容在generateCode()方法中,該方法利用上面我們提到的Filer來寫出原始檔.你會發現,這裡主要就是字元創拼接類的過程嘛,真是太麻煩了.

到現在為止,我們已經編寫好了兩個註解及其對應的處理器.現在我們僅需要對其進行配置.

在resources資原始檔夾下建立META-INF.services,然後在該路徑下建立名為javax.annotation.processing.Processor的檔案,在該檔案中配置需要啟用的註解處理器,即寫上處理器的完整路徑,有幾個處理器就寫幾個,分行寫么,比如我們這裡是:

com.closedevice.processor.PrintProcessor
com.closedevice.processor.CodeProcessor

到現在我們已經做好打包之前的準備了,此時專案結構如下:
這裡寫圖片描述

下面就需要將apt moudle打成jar包.無論你是在什麼平臺上,最終打出jar包就算成功一半了.為了方便演示,直接視覺化操作:
這裡寫圖片描述

來看一下apt.jar的結構:
這裡寫圖片描述

接下來將apt.jar檔案複製到主moudle app下的libs資料夾中,開始使用它.我們簡單的在MainActivity.java中使用一下:

public class MainActivity extends AppCompatActivity {

    @Override
    @Print
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        process();
    }

    @Code(author = "closedevice",date="20161225")
    private void process() {

    }


}

分別在onCreate()和process()方法中使用我們的註解,現在編譯app模組,在編譯過程中你可以在Gradle Console看到輸出的資訊,不出意外的話,你講看到一下資訊:
這裡寫圖片描述

另外在app moudle的build/intermediates/classes/debug/com/closedevice/annotationtest就可以看到自動生成的MainActivityAutogenerate.class了.當然你也可以直接檢視編譯階段生成的原始碼檔案com/closedevice/annotationtest/MainActivity$WrmRequestInfo.java

這裡寫圖片描述

再來看看自動生成的原始碼:

package com.closedevice.annotationtest;

 class MainActivityAutogenerate {

    protected MainActivityAutogenerate() {}
    protected final void message() {

//process()
//@com.closedevice.annotation.Code(date=20161225, author=closedevice)

        System.out.println("author:closedevice");

        System.out.println("date:20161225");
    }
}

將該工程部署到我們的模擬器上,不出意外,會看到以下日誌資訊:
這裡寫圖片描述

就這樣,一個簡單的編譯時註解處理器就實現了.上面我們利用執行時註解處理器來做了個簡單的ButterKnife,但真正ButterKnife是利用編譯是利用APT來實現的,限於篇幅,這一小節就不做演示了

總結

本文初步介紹了執行時註解處理器和編譯時註解處理器,但是有關APT的內容絕非一文可以說明白的,我將在後面逐步介紹有關APT的相關知識.