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、申明注解

       申明注解步驟:

    通過@Retention(RetentionPolicy.RUNTIME)元注解確定我們注解是在運行的時候使用。
    通過@Target確定我們注解是作用在什么上面的(變量、函數、類等)。
    確定我們注解需要的參數。

       比如下面一段代碼我們聲明了一個作用在變量上的BindString運行時注解。

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

    int value();

}


2.1.2、注解解析

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

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

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、聲明注解

       編譯時注解的聲明和運行時注解的聲明一樣也是三步:

    通過@Retention(RetentionPolicy.TYPE)元注解確定我們注解是在編譯的時候使用。
    通過@Target確定我們注解是作用在什么上面的(變量、函數、類等)。
    確定我們注解需要的參數。

       比如下面的代碼我們自定義了一個作用在類上的編譯時注解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文件)。

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

    注冊注解處理器(這個注解器就是我們第二步自定義的類)。
    自定義注解處理器類繼承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'

    1

       比如下面的這段代碼就使用上了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里面的代碼有什么疑問歡迎留言指出。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM