1. 程式人生 > >Android 自定義註解(Annotation)

Android 自定義註解(Annotation)

       現在市面上很多框架都有使用到註解,比如butterknife庫、EventBus庫、Retrofit庫等等。也是一直好奇他們都是怎麼做到的,註解的工作原理是啥。咱們能不能自己去實現一個簡單的註解呢。

       註解(Annotation)是JDK1.5新增加功能,註解其實就是新增在類、變數、方法、引數等前面的一個修飾符一個標記而已(不要把他想的太複雜)。比如下面的程式碼裡面@Override、@IdRes就是註解。

    @Override
    public <T extends View> T findViewById
(@IdRes int id) { return getDelegate().findViewById(id); }

       上面我們強調了註解就是一個修飾符一個標記而且。但是通過註解能做的事情確是無窮。在程式碼編譯或者執行的過程中我們可以找到這些 註解,在找到這些註解之後咱們就可以做很多事情了,比如自動做一些程式碼處理(賦值、檢測、呼叫等等)或者乾脆生成一些額外的java檔案等。下面會用更加具體的例項來說明。

       註解的作用:簡化程式碼,提高開發效率。

注意哦,肯定是能提高程式碼開發效率,並不一定能提供程式執行效率。


       接下來我們通過學習自定義註解(定義我們自己的註解)來讓大家對註解有一個深刻的認識。

一、元註解

       在我們自定義註解之前我們需要來先了解下元註解。元註解是用來定義其他註解的註解(在自定義註解的時候,需要使用到元註解來定義我們的註解)。java.lang.annotation提供了四種元註解:@Retention、 @Target、@Inherited、@Documented。

元註解是用來修飾註解的註解。在自定義註解的時候我們肯定都是要用到元註解的。因為我們需要定義我們註解的是方法還是變數,註解的存活時間等等。

元註解 說明
@Target 表明我們註解可以出現的地方。是一個ElementType列舉
@Retention 這個註解的的存活時間
@Document 表明註解可以被javadoc此類的工具文件化
@Inherited 是否允許子類繼承該註解,預設為false

1.1、@Target

       @Target元註解用來表明我們註解可以出現的地方,引數是一個ElementType型別的陣列,所以@Target可以設定註解同時出現在多個地方。比如既可以出現來類的前面也可以出現在變數的前面。

       @Target元註解ElementType列舉(用來指定註解可以出現的地方):

@Target-ElementType型別 說明
ElementType.TYPE 介面、類、列舉、註解
ElementType.FIELD 欄位、列舉的常量
ElementType.METHOD 方法
ElementType.PARAMETER 方法引數
ElementType.CONSTRUCTOR 建構函式
ElementType.LOCAL_VARIABLE 區域性變數
ElementType.ANNOTATION_TYPE 註解
ElementType.PACKAGE

1.2、@Retention

       @Retention表示需要在什麼級別儲存該註釋資訊,用於描述註解的生命週期(即:被描述的註解在什麼範圍內有效)。引數是RetentionPolicy列舉物件。

       RetentionPolicy的列舉型別有(預設值為CLASS.):

@Retention-RetentionPolicy型別 說明
RetentionPolicy.SOURCE 註解只保留在原始檔,當Java檔案編譯成class檔案的時候,註解被遺棄
RetentionPolicy.CLASS 註解被保留到class檔案,但jvm載入class檔案時候被遺棄,這是預設的生命週期
RetentionPolicy.RUNTIME 註解不僅被儲存到class檔案中,jvm載入class檔案之後,仍然存在

SOURCE < CLASS < RUNTIME,前者能作用的地方後者一定也能作用.

1.3、@Document

       @Document表明我們標記的註解可以被javadoc此類的工具文件化。

1.4、@Inherited

       @Inherited表明我們標記的註解是被繼承的。比如,如果一個父類使用了@Inherited修飾的註解,則允許子類繼承該父類的註解。

二、自定義註解

2.1、自定義執行時註解

       執行時註解:在程式碼執行的過程中通過反射機制找到我們自定義的註解,然後做相應的事情。

反射:對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意一個方法和屬性。

       自定義執行是註解大的方面分為兩步:一個是申明註解、第二個是解析註解。

2.1.1、申明註解

       申明註解步驟:

  1. 通過@Retention(RetentionPolicy.RUNTIME)元註解確定我們註解是在執行的時候使用。
  2. 通過@Target確定我們註解是作用在什麼上面的(變數、函式、類等)。
  3. 確定我們註解需要的引數。

       比如下面一段程式碼我們聲明瞭一個作用在變數上的BindString執行時註解。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindString {

    int value();

}

2.1.2、註解解析

       執行時註解的解析我們簡單的分為三個步驟:

  1. 找到類對應的所有屬性或者方法(至於是找類的屬性還是方法就要看我自定義的註解是定義方法上還是屬性上了)。
  2. 找到添加了我們註解的屬性或者方法。
  3. 做我們註解需要自定義的一些操作。

2.1.2.1、獲取類的屬性和方法

既然註解是我們自定義的,我肯定事先會確定我們註解是加在屬性上的還是加在方法上的。

       通過Class物件我們就可以很容易的獲取到當前類裡面所有的方法和屬性了:

Class類裡面常用方法介紹(這裡我們不僅僅介紹了獲取屬性和方法的,還介紹了一些其他Class裡面常用的方法)

    /**
     * 包名加類名
     */
    public String getName();

    /**
     * 類名
     */
    public String getSimpleName();

    /**
     * 返回當前類和父類層次的public構造方法
     */
    public Constructor<?>[] getConstructors();

    /**
     * 返回當前類所有的構造方法(public、private和protected)
     * 不包括父類
     */
    public Constructor<?>[] getDeclaredConstructors();

    /**
     * 返回當前類所有public的欄位,包括父類
     */
    public Field[] getFields();

    /**
     * 返回當前類所有申明的欄位,即包括public、private和protected,
     * 不包括父類
     */
    public native Field[] getDeclaredFields();

    /**
     * 返回當前類所有public的方法,包括父類
     */
    public Method[] getMethods();

    /**
     * 返回當前類所有的方法,即包括public、private和protected,
     * 不包括父類
     */
    public Method[] getDeclaredMethods();

    /**
     * 獲取區域性或匿名內部類在定義時所在的方法
     */
    public Method getEnclosingMethod();

    /**
     * 獲取當前類的包
     */
    public Package getPackage();

    /**
     * 獲取當前類的包名
     */
    public String getPackageName$();

    /**
     * 獲取當前類的直接超類的 Type
     */
    public Type getGenericSuperclass();

    /**
     * 返回當前類直接實現的介面.不包含泛型引數資訊
     */
    public Class<?>[] getInterfaces();

    /**
     * 返回當前類的修飾符,public,private,protected
     */
    public int getModifiers();

       類裡面每個屬性對應一個物件Field,每個方法對應一個物件Method。

2.1.2.2、找到添加註解的屬性或者方法

       上面說道每個屬性對應Field,每個方法對應Method。而且Field和Method都實現了AnnotatedElement介面。都有AnnotatedElement接了我們就可以很容易的找到添加了我們指定註解的方法或者屬性了。

AnnotatedElement介面常用方法如下:

    /**
     * 指定型別的註釋是否存在於此元素上
     */
    default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
        return getAnnotation(annotationClass) != null;
    }

    /**
     * 返回該元素上存在的指定型別的註解
     */
    <T extends Annotation> T getAnnotation(Class<T> annotationClass);

    /**
     * 返回該元素上存在的所有註解
     */
    Annotation[] getAnnotations();

    /**
     * 返回該元素指定型別的註解
     */
    default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) {
        return AnnotatedElements.getDirectOrIndirectAnnotationsByType(this, annotationClass);
    }

    /**
     * 返回直接存在與該元素上的所有註釋(父類裡面的不算)
     */
    default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) {
        Objects.requireNonNull(annotationClass);
        // Loop over all directly-present annotations looking for a matching one
        for (Annotation annotation : getDeclaredAnnotations()) {
            if (annotationClass.equals(annotation.annotationType())) {
                // More robust to do a dynamic cast at runtime instead
                // of compile-time only.
                return annotationClass.cast(annotation);
            }
        }
        return null;
    }

    /**
     * 返回直接存在該元素岸上某型別的註釋
     */
    default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) {
        return AnnotatedElements.getDirectOrIndirectAnnotationsByType(this, annotationClass);
    }

    /**
     * 返回直接存在與該元素上的所有註釋
     */
    Annotation[] getDeclaredAnnotations();

2.1.2.3、做自定義註解需要做的事情

       添加了我們註解的屬性或者方法已經拿到了,之後要做的就是自定義註解自定義的一些事情了。比如在某些特定條件下自動去執行我們添加註解的方法。下面我們也會用兩個具體的例項來說明。

2.1.3、執行時註解例項

       我們通過兩個簡單的例項來看下自定義執行時註解是怎麼操作的。

2.1.3.1、通過註解自動建立物件

       程式碼過程中,我們可能經常會犯這樣的錯誤,定義了一個物件,但是經常忘了建立物件。跑出空指標異常。接下來我們通過自定義一個AutoWired註解來自動去幫我們建立物件。

AutoWired註解的聲,指定註解是在變數上使用,並且在執行時有效。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoWired {

}

AutoWired註解的解析,找到AutoWired註解的變數,建立物件,在吧物件賦值給AutoWired指定的那個變數。

public class AutoWiredProcess {

    public static void bind(final Object object) {
        Class parentClass = object.getClass();
        Field[] fields = parentClass.getFields();
        for (final Field field : fields) {
            AutoWired autoWiredAnnotation = field.getAnnotation(AutoWired.class);
            if (autoWiredAnnotation != null) {
                field.setAccessible(true);
                try {
                    Class<?> autoCreateClass = field.getType();
                    Constructor autoCreateConstructor = autoCreateClass.getConstructor();
                    field.set(object, autoCreateConstructor.newInstance());
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (InstantiationException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }

            }
        }
    }

}

AutoWired註解的使用,在onCrate()方法裡面呼叫了AutoWiredProcess.bind(this);來解析註解。這樣在執行的時候就會自動去建立UserInfo物件。

public class MainActivity extends AppCompatActivity {

    //自動建立物件,不用我們去new UserInfo()了
    @AutoWired
    UserInfo mUserInfo;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        AutoWiredProcess.bind(this);
    }

}

2.1.3.2、通過註解自動findViewById()

       我們也來簡單的來實現一個類似Butterknife 庫裡面自動繫結View的一個功能。不用在每個View都要去寫findViewById來找到這個View了。

宣告BindView註解,而且規定需要一個int引數。int引數代表View對應的id

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {

    int value();
}

解析BindView註解,通過findViewById找到VIew,在把View賦值給BindView註解指向的變數。

public class ButterKnifeProcess {

    /**
     * 繫結Activity
     */
    public static void bind(final Activity activity) {
        Class annotationParent = activity.getClass();
        Field[] fields = annotationParent.getDeclaredFields();
        Method[] methods = annotationParent.getDeclaredMethods();
        // OnClick
        // 找到類裡面所有的方法
        for (final Method method : methods) {
            //找到添加了OnClick註解的方法
            OnClick clickMethod = method.getAnnotation(OnClick.class);
            if (clickMethod != null && clickMethod.value().length != 0) {
                for (int id : clickMethod.value()) {
                    final View view = activity.findViewById(id);
                    view.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            try {
                                method.invoke(activity, view);
                            } catch (IllegalAccessException e) {
                                e.printStackTrace();
                            } catch (InvocationTargetException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                }
            }
        }

    }

}

使用BindView註解,onCreate裡面呼叫了ButterKnifeProcess.bind(this);來解析註解。

public class MainActivity extends AppCompatActivity {

    //自動繫結view
    @BindView(R.id.text_abstract_processor)
    TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnifeProcess.bind(this);
    }
}

2.2、自定義編譯時註解

       編譯時註解就是在編譯的過程中用一個javac註解處理器來掃描到我們自定義的註解,生成我們需要的一些檔案(通常是java檔案)。

       自定義編譯時註解的步驟:
1. 宣告註解。
2. 編寫註解處理器。
3. 生成檔案(通常是JAVA檔案)。

第二步和第三步其實是柔和在一起的。我這裡為了清晰一點就把他們獨立開來了。

2.2.1、宣告註解

       編譯時註解的宣告和執行時註解的宣告一樣也是三步:

  1. 通過@Retention(RetentionPolicy.TYPE)元註解確定我們註解是在編譯的時候使用。
  2. 通過@Target確定我們註解是作用在什麼上面的(變數、函式、類等)。
  3. 確定我們註解需要的引數。

       比如下面的程式碼我們自定義了一個作用在類上的編譯時註解Factory,並且這個註解是需要兩個引數的,一個是Class型別,一個是String型別。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Factory {
    Class type();

    String id();
}

2.2.2、編寫註解處理器

       和執行時註解的解析不一樣,編譯時註解的解析需要我們自己去實現一個註解處理器。

註解處理器(Annotation Processor)是javac的一個工具,它用來在編譯時掃描和處理註解(Annotation)。一個註解的註解處理器,以Java程式碼(或者編譯過的位元組碼)作為輸入,生成檔案(通常是.java檔案)作為輸出。而且這些生成的Java檔案同咱們手動編寫的Java原始碼一樣可以呼叫。(注意:不能修改已經存在的java檔案程式碼)。

       註解處理器所做的工作,就是在程式碼編譯的過程中,找到我們指定的註解。然後讓我們更加自己特定的邏輯做出相應的處理(通常是生成JAVA檔案)。

       註解處理器的寫法有固定套路的,兩步:

  1. 註冊註解處理器(這個註解器就是我們第二步自定義的類)。
  2. 自定義註解處理器類繼承AbstractProcessor。

2.2.2.1、註冊註解處理器

       打包註解處理器的時候需要一個特殊的檔案 javax.annotation.processing.Processor 在 META-INF/services 路徑下。在javax.annotation.processing.Processor檔案裡面寫上我們自定義註解處理器的全稱(包加類的名字)如果有多個註解處理器換行寫入就可以。

       偉大的google為了方便我們註冊註解處理器。給提供了一個註冊處理器的庫
@AutoService(Processor.class)的註解來簡化我們的操作。我們只需要在我們自定義的註解處理器類前面加上google的這個註解,在打包的時候就會自動生成javax.annotation.processing.Processor檔案,寫入相的資訊。不需要我們手動去建立。當然瞭如果你想使用google的這個註解處理器的庫,必須加上下面的依賴。

compile 'com.google.auto.service:auto-service:1.0-rc3'

       比如下面的這段程式碼就使用上了google提供的這個註解器處理庫,會自動註冊註解處理器。

@AutoService(Processor.class)
public class FactoryProcessor extends AbstractProcessor {
    ...

}

2.2.2.2、自定義註解處理器類

       自定義的註解處理器類一定要繼承AbstractProcessor,否則找不到我們需要的註解。在這個類裡面找到我們需要的註解。做出相應的處理。

       關於AbstractProcessor裡面的一些函式我們也做一個簡單的介紹。

    /**
     * 每個Annotation Processor必須有一個空的建構函式。
     * 編譯期間,init()會自動被註解處理工具呼叫,並傳入ProcessingEnvironment引數,
     * 通過該引數可以獲取到很多有用的工具類(Element,Filer,Messager等)
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    /**
     * 用於指定自定義註解處理器(Annotation Processor)是註冊給哪些註解的(Annotation),
     * 註解(Annotation)指定必須是完整的包名+類名
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

    /**
     * 用於指定你的java版本,一般返回:SourceVersion.latestSupported()
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * Annotation Processor掃描出的結果會儲存進roundEnvironment中,可以在這裡獲取到註解內容,編寫你的操作邏輯。
     * 注意:process()函式中不能直接進行異常丟擲,否則程式會異常崩潰
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }

       註解處理器的核心是process()方法(需要重寫AbstractProcessor類的該方法),而process()方法的核心是Element元素。Element 代表程式的元素,在註解處理過程中,編譯器會掃描所有的Java原始檔,並將原始碼中的每一個部分都看作特定型別的Element。它可以代表包、類、介面、方法、欄位等多種元素種類。所有Element肯定是有好幾個子類。如下所示。

Element子類 解釋
TypeElement 類或介面元素
VariableElement 欄位、enum常量、方法或構造方法引數、區域性變數或異常引數元素
ExecutableElement 類或介面的方法、構造方法,或者註解型別元素
PackageElement 包元素
TypeParameterElement 類、介面、方法或構造方法元素的泛型引數

       關於Element類裡面的方法我們也做一個簡單的介紹:

    /**
     * 返回此元素定義的型別,int,long這些
     */
    TypeMirror asType();

    /**
     * 返回此元素的種類:包、類、介面、方法、欄位
     */
    ElementKind getKind();

    /**
     * 返回此元素的修飾符:public、private、protected
     */
    Set<Modifier> getModifiers();

    /**
     * 返回此元素的簡單名稱(類名)
     */
    Name getSimpleName();

    /**
     * 返回封裝此元素的最裡層元素。
     * 如果此元素的宣告在詞法上直接封裝在另一個元素的宣告中,則返回那個封裝元素;
     * 如果此元素是頂層型別,則返回它的包;
     * 如果此元素是一個包,則返回 null;
     * 如果此元素是一個泛型引數,則返回 null.
     */
    Element getEnclosingElement();

    /**
     * 返回此元素直接封裝的子元素
     */
    List<? extends Element> getEnclosedElements();

    /**
     * 返回直接存在於此元素上的註解
     * 要獲得繼承的註解,可使用 getAllAnnotationMirrors
     */
    List<? extends AnnotationMirror> getAnnotationMirrors();

    /**
     * 返回此元素上存在的指定型別的註解
     */
    <A extends Annotation> A getAnnotation(Class<A> var1);

關於TypeElement、VariableElement、ExecutableElement、PackageElement、TypeParameterElement每個類特有的方法我們這裡就沒有介紹了,大家可以到相應的原始碼檔案裡面去看一看。

       自定義處理器的過程中我們除了要了解Element類和他的子類的用法,還有四個幫助類也是需要我們瞭解的。Elements、Types、Filer、Messager。

註解解析器幫助類 解釋
Elements 一個用來處理Element的工具類
Types 一個用來處理TypeMirror的工具類
Filer 用於建立檔案(比如建立class檔案)
Messager 用於輸出,類似printf函式

       這四個幫助類都可以在init()函式裡面通過ProcessingEnvironment獲取到。類似如下的程式碼獲取

@AutoService(Processor.class)
public class FactoryProcessor extends AbstractProcessor {

    /**
     * 用來處理TypeMirror的工具類
     */
    private Types                              mTypeUtils;
    /**
     * 用於建立檔案
     */
    private Filer                              mFiler;
    /**
     * 用於列印資訊
     */
    private Messager                           mMessager;
    ...

    /**
     * 獲取到Types、Filer、Messager、Elements
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mTypeUtils = processingEnvironment.getTypeUtils();
        mFiler = processingEnvironment.getFiler();
        mMessager = processingEnvironment.getMessager();
        ...
    }

    ...


}

2.2.3、生成檔案

       生成檔案,通常是生成一個java檔案。直接呼叫幫助類Filer的createSourceFile()函式就可以建立一個java檔案。之後就是在這個java檔案裡面寫入我們需要的內容了。為了提高大家的開發效率推薦兩個寫java原始檔的開源庫FileWriter和JavaPoet。兩個庫用起來也很簡單,這裡我們就不深入進去了。生成檔案這一部分的內容非常的簡答。具體可以參考我們下編譯時註解例項。

JavaWrite是JavaPoet增強版。

2.2.4、編譯時註解例項

       從網上找了一個非常全面自定義編譯時註解的例子。例子來源於 https://blog.csdn.net/github_35180164/article/details/52055994 通過自定義註解實現工廠模式。每個工廠模式通常都會有一個相應的Factory的幫助類來選擇具體的工廠類,我們現在就想通過編譯時註解來自動生成這個Factory的幫助類,不用我們去手動編寫了。

Peple抽象類

public abstract class People {

    public abstract String getName();

    public abstract int getAge();

    public abstract int getSex();

}

Male類實現了People類,並且添加了@Factory註解

@Factory(id = "Male", type = People.class)
public class Male extends People{

    @Override
    public String getName() {
        return "男生";
    }

    @Override
    public int getAge() {
        return 28;
    }

    @Override
    public int getSex() {
        return 0;
    }
}

Female類實現了People類,並且添加了@Factory註解

@Factory(id = "Female", type = People.class)
public class Female extends People {

    @Override
    public String getName() {
        return "女生";
    }

    @Override
    public int getAge() {
        return 27;
    }

    @Override
    public int getSex() {
        return 1;
    }
}

根據上面新增的註解,我們會去自動生成一個PeopleFactory類,而且裡面的內容也編譯的時候自動生成的,內容如下。

public class PeopleFactory {

  public People create(String id) {
    if (id == null) {
      throw new IllegalArgumentException("id is null!");
    }
    if ("Female".equals(id)) {
      return new com.tuacy.annotationlearning.annotation.abstractprocessor.Female();
    }

    if ("Male".equals(id)) {
      return new com.tuacy.annotationlearning.annotation.abstractprocessor.Male();
    }

    throw new IllegalArgumentException("Unknown id = " + id);
  }
}

       為了實現上述功能,我們在Android Studio裡面新建一個project。然後再新建一個annotationprocess的module,新建module的時候選擇Java Library。在annotationprocess裡面寫我們註解的申明和註解的處理。

       先申明一個Factory的註解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Factory {

    /**
     * 工廠的名字
     */
    Class type();

    /**
     * 用來表示生成哪個物件的唯一id
     */
    String id();


}

       在自定義一個FactoryProcessor註解處理器繼承AbstractProcessor。FactoryProcessor程式碼裡面的內容比較多這裡我就不粘貼出來了。無非就是找到我們自定義的註解,然後做一些相應的判斷,最後生成java檔案程式碼。相應的程式碼大家可以在下面給出的DEMO裡面看到,DEMO裡面的註釋備註寫的也非常詳細。生成JAVA檔案使用的是JavaWriter庫。

       最後我們把annotationprocess module裡面的程式碼打成jar包放到我們需要的工程裡面去(同時把javawriter-2.5.1.jar也拷貝進去)。使用就和我們上面說的People工廠一樣使用就OK了。


       本文DEMO下載地址

       關於自定義註解的內容,我們就說的就這麼多,希望能給大家起到一個拋磚引玉的作用,如果大家對DEMO裡面的程式碼有什麼疑問歡迎留言指出。