入門篇:神奇的Annotation

前面寫了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
  1. @Target
  2. @Inherited
  3. @Documented
  4. @interface

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

@interface

@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)的注解.

方法 含義
<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() 返回直接存在與該元素上的所有注釋

在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<? extends TypeElement> 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構建.

以下示例在gradle 2.2.X上構建,如果你在之前的版本進行構建,則需要手動配置apt.

注:
android-apt插件作者近期已經發表聲明表示后續不會再繼續維護該插件,而android Gradle從2.2版本開始支持annotationProcessor功能來代替android-apt。另外,和android-apt只支持javac編譯器相比,annotationProcessor同時支持javac和jack編譯器。

首先創建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("http://" + 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的相關知識.

Github:https://github.com/closedevice/MagicAnnotation

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容