一個例子帶你瞭解兩種自定義註解

19snow93發表於2019-03-04

有時候轉過頭看回一些基礎知識,才發現原來當時候自己覺得很難的東西都是從基礎知識衍生而來的,突然會有點豁然開朗的感覺。譬如說我們今天要講的知識點———註解。

初識,無處不在的註解

從Java1.5就開始引入,在註解中,我們很容易就看到了Java的理念,”Write Once,Run Anywhere”。平時開發的時候我們看到最多的註解莫過於是Java三種內建註解之一的@Override。

@Override——當我們想要複寫父類中的方法時,我們需要使用該註解去告知編譯器我們想要複寫這個方法。這樣一來當父類中的方法移除或者發生更改時編譯器將提示錯誤資訊。

其實在Android開發中,我們在很多第三方庫中會經常看到註解,下面我就列舉介紹一下在《Android高階進階》中看到的一些關於運用到註解的例子:

1.標準註解:

Java API中預設定義的註解我們稱之為標準註解,他們定義在java.lang,java.lang.annotation和javax.annotation包中,按照不同場景分為三類:

  1. 編譯時相關注解:編譯相關的註解是給編譯器使用,如
    @Override、@Deprecated、SuppressWarnings、@SafeVarags、@Generated、@FunctionalInterface…
  2. 資源相關注解:一般用在JavaEE領域,Android開發中應該不會使用到,如@PostConstruct、@PreDestroy、@Resource、@Resources…
  3. 元註解:用來定義和實現註解的註解,如@Target、@Retention、@Documented、@Inherited、@Repeatable

2.Support Annotation Library:

Support Annotation Library是從Android Support Library19.1開始引入的一個全新的函式包,它包含了一系列有用的元註解,用來幫助開發者在編譯期間發現可能存在的Bug。

  1. Nullness註解:如@Nullable、@NonNull
  2. 資源型別註解:如@AnimatorRes、@AttrRes、@LayoutRes…
  3. 型別定義註解:如@IntDef…
  4. 執行緒註解:如@UiThread、@MainThread、@WorkerThread、@BinderThread…
  5. RGB顏色值註解:如@ColorRes
  6. 值範圍註解:如@Size、@IntRange、@FloatRange…
  7. 許可權註解:如@RequirdsPermission
  8. 重寫函式註解:如@CallSuper
  9. 返回值註解:如@CheckResult
  10. @VisibleForTesting
  11. @Keep

3.一些著名的第三方庫:

如Butterknife、Dagger2、DBFlow、Retrofit、JUnit

以上都是總結了大部分在《Android高階進階》中出現的註解的地方,很多註解沒一個個解釋,有興趣的同學可以自己去搜尋一下自己想知道的註解的具體用途。我們可以看到註解無論在java和Android中都是使用很廣泛的,而且慢慢變得必不可少。下面我們就進入我們的主題,分別用兩種方式去自定義註解。

那麼我們先列出一個簡單的題目,然後用兩種不同的方式去實現:

題目:用註解實現兩數相加的運算

一、執行時自定義註解:

執行時註解一般和反射機制配合使用,相比編譯時註解效能比較低,但靈活性好,實現起來比較簡單,所以我們先來用這個去實現。

1. 我們先去建立檔案和寫一個註解

/* 用來指明註解的訪問範圍
*  1.原始碼級註解SOURCE,該型別的註解資訊會留在.java原始碼中,
*    原始碼編譯後,註解資訊會被丟棄,不會保留在編譯好的.class檔案中;
*  2.編譯時註解CLASS,註解資訊會保留在.java原始碼裡和.class檔案中,
*    在執行的時候,會被Java虛擬機器丟棄不回家再到虛擬機器中;
*  3.執行時註解RUNTIME,java原始碼裡,.class檔案中和Java虛擬機器在執行期也保留註解資訊,
*    可通過反射讀取
*/
@Retention(RUNTIME)
//是一個ElementType型別的陣列,用來指定註解所使用的物件範圍
@Target(value = FIELD)
public @interface Add {
    float ele1() default 0f;
    float ele2() default 0f;
}
複製程式碼

可以看到,因為是執行時註解,所以我們定義了@Retention是Runtime,定義了ele1,ele2兩個看上去像函式的變數(在註解裡這樣寫算是變數而不是方法或函式)

2. 下面我們使用反射去告訴這個註解你應該做什麼

public class InjectorProcessor {
    public void process(final Object object) {
        
        Class class1 = object.getClass();
        //找到類裡所有變數Field
        Field[] fields = class1.getDeclaredFields();
        //遍歷Field陣列
        for(Field field:fields){
            //找到相應的擁有Add註解的Field
            Add addMethod = field.getAnnotation(Add.class);
            if (addMethod != null){
                if(object instanceof Activity){
                    //獲取註解中ele1和ele2兩個數字,然後把他們相加
                    double d = addMethod.ele1() + addMethod.ele2();
                    try {
                        //把相加結果的值賦給該Field
                        field.setDouble(object,d);
                    }catch (Exception e){

                    }

                }
            }
        }

    }
}
複製程式碼

就這樣,我們利用了反射,告訴了Add這個註解,在程式碼裡找到你的時候,你該做什麼,把工作做好,你就有飯吃。

3.使用

一個例子帶你瞭解兩種自定義註解
一個例子帶你瞭解兩種自定義註解

很快,我們就用第一種方式實現了給出的題目;確實,在程式碼量上這種方式比較簡單粗暴,但是這種方式並不常用。

一、編譯時自定義註解:

有不常用的方式,肯定就有常用的方式,下面我們就來介紹這個常用的方式——註解處理器

著名的第三方框架ButterKnife也就是用這種方式去實現註解繫結控制元件的功能的。

註解處理器是(Annotation Processor)是javac的一個工具,用來在編譯時掃描和編譯和處理註解(Annotation)。你可以自己定義註解和註解處理器去搞一些事情。一個註解處理器它以Java程式碼或者(編譯過的位元組碼)作為輸入,生成檔案(通常是java檔案)。這些生成的java檔案不能修改,並且會同其手動編寫的java程式碼一樣會被javac編譯。看到這裡加上之前理解,應該明白大概的過程了,就是把標記了註解的類,變數等作為輸入內容,經過註解處理器處理,生成想要生成的java程式碼。

我們可以看到所有註解都會在編譯的時候就把程式碼生成,而且高效、避免在執行期大量使用反射,不會對效能造成損耗。
下面我們就看看怎麼去實現一個註解處理器:

1. 建立工程:

  1. 首先建立一個project;
  2. 建立lib_annotations,
    這是一個純java的module,不包含任何android程式碼,只用於存放註解。
  3. 建立lib_compiler,
    這同樣是一個純java的module。該module依賴於步驟2建立的module_annotation,處理註解的程式碼都在這裡,該moduule最終不會被打包進apk,所以你可以在這裡匯入任何你想要的任意大小依賴庫。
  4. 建立lib_api,
    對該module不做要求,可以是android library或者java library或者其他的。該module用於呼叫步驟3生成的輔助類方法。
一個例子帶你瞭解兩種自定義註解
一個例子帶你瞭解兩種自定義註解

為什麼我們要新建這麼多module呢,原因很簡單,因為有些庫在編譯時起作用,有些在執行時起作用,把他們放在同一個module下會報錯,所以我們秉著各司其職的理念把他們都分開了。

2.在module的lib_annotations建立Add註解

一個例子帶你瞭解兩種自定義註解

跟第一種方法不同,我們在@Retention選擇的是CLASS,雖然選擇RUNTIME也是可以的,但是為了顯示區別,我們還是作了修改。

3.寫註解處理器

在寫註解處理器之前我們必須在lib_compiler中引入兩個庫輔助我們成就大業:

一個例子帶你瞭解兩種自定義註解
  1. auto-service:
    AutoService會自動在META-INF資料夾下生成Processor配置資訊檔案,該檔案裡就是實現該服務介面的具體實現類。而當外部程式裝配這個模組的時候,
    就能通過該jar包META-INF/services/裡的配置檔案找到具體的實現類名,並裝載例項化,完成模組的注入。
    基於這樣一個約定就能很好的找到服務介面的實現類,而不需要再程式碼裡制定,方便快捷。
  2. javapoet:JavaPoet是square推出的開源java程式碼生成框架,提供Java Api生成.java原始檔。這個框架功能非常有用,我們可以很方便的使用它根據註解、資料庫模式、協議格式等來對應生成程式碼。通過這種自動化生成程式碼的方式,可以讓我們用更加簡潔優雅的方式要替代繁瑣冗雜的重複工作。

我們先在lib_compiler中建立一個基類

一個例子帶你瞭解兩種自定義註解
public class AnnotatedClass {
    
    public Element mClassElement;
    /**
     * 元素相關的輔助類
     */
    public Elements mElementUtils;

    public TypeMirror elementType;

    public Name elementName;
    
    //加法的兩個值
    private float value1;
    private float value2;


    public AnnotatedClass(Element classElement) {

        this.mClassElement = classElement;
        this.elementType = classElement.asType();
        this.elementName = classElement.getSimpleName();

        value1 = mClassElement.getAnnotation(Add.class).ele1();
        value2 = mClassElement.getAnnotation(Add.class).ele2();
    }

    Name getElementName() {
        return elementName;
    }

    TypeMirror getElementType(){
        return elementType;
    }

    Float getTotal(){
        return (value1 + value2);
    }


    /**
     * 包名
     */
    public String getPackageName(TypeElement type) {
        return mElementUtils.getPackageOf(type).getQualifiedName().toString();
    }
    /**
     * 類名
     */
    private static String getClassName(TypeElement type, String packageName) {
        int packageLen = packageName.length() + 1;
        return type.getQualifiedName().toString().substring(packageLen).replace(`.`, `$`);
    }
}
複製程式碼

然後我們的主角就要出場了——註解處理器
我們建立一個檔案,然後繼承AbstractProcessor

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

    private static final String ADD_SUFFIX = "_Add";
    private static final String TARGET_STATEMENT_FORMAT = "target.%1$s = %2$s";
    private static final String CONST_PARAM_TARGET_NAME = "target";

    private static final char CHAR_DOT = `.`;

    private Messager messager;
    private Types typesUtil;
    private Elements elementsUtil;
    private Filer filer;
     /** 
     * 解析的目標註解集合,一個類裡可以包含多個註解,所以是Map<String, List<AnnotatedClass>>
     */  
    Map<String, List<AnnotatedClass>> annotatedElementMap = new LinkedHashMap<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        messager = processingEnv.getMessager();
        typesUtil = processingEnv.getTypeUtils();
        elementsUtil = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
    }

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

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


    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        因為該方法可能會執行多次,所以每次進來必須clear
        annotatedElementMap.clear();
        //1.遍歷每個有Add註解的Element,
        //2.然後把它加入Map裡面,一個類裡可以包含多個註解,所以是Map<String, List<AnnotatedClass>>,
        //3.賦予它工作任務,告訴他你該做什麼,
        //4.然後生成Java檔案
        for (Element element : roundEnv.getElementsAnnotatedWith(Add.class)) {
            //判斷被註解的型別是否符合要求
            if (element.getKind() != ElementKind.FIELD) {
                messager.printMessage(Diagnostic.Kind.ERROR, "Only FIELD can be annotated with @%s");
            }
            TypeElement encloseElement = (TypeElement) element.getEnclosingElement();
            String fullClassName = encloseElement.getQualifiedName().toString();
            AnnotatedClass annotatedClass = new AnnotatedClass(element);
            //把類名和該類裡面的所有關於Add註解的註解放到Map裡面
            if(annotatedElementMap.get(fullClassName) == null){
                annotatedElementMap.put(fullClassName, new ArrayList<AnnotatedClass>());
            }
            annotatedElementMap.get(fullClassName).add(annotatedClass);

        }
        //因為該方法會執行多次,所以size=0時返回true結束
        if (annotatedElementMap.size() == 0) {
            return true;
        }

        //用javapoet生成類檔案
        try {
            for (Map.Entry<String, List<AnnotatedClass>> entry : annotatedElementMap.entrySet()) {
                MethodSpec constructor = createConstructor(entry.getValue());
                TypeSpec binder = createClass(getClassName(entry.getKey()), constructor);
                JavaFile javaFile = JavaFile.builder(getPackage(entry.getKey()), binder).build();
                javaFile.writeTo(filer);
            }

        } catch (IOException e) {
            messager.printMessage(Diagnostic.Kind.ERROR, "Error on creating java file");
        }

        return true;
    }


    //以下是javapoet建立各種方法的實現方式
    private MethodSpec createConstructor(List<AnnotatedClass> randomElements) {
        AnnotatedClass firstElement = randomElements.get(0);
        MethodSpec.Builder builder = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(TypeName.get(firstElement.mClassElement.getEnclosingElement().asType()), CONST_PARAM_TARGET_NAME);
        for (int i = 0; i < randomElements.size(); i++) {
            addStatement(builder, randomElements.get(i));
        }
        return builder.build();
    }

    private void addStatement(MethodSpec.Builder builder, AnnotatedClass randomElement) {
        builder.addStatement(String.format(
                TARGET_STATEMENT_FORMAT,
                randomElement.getElementName().toString(),
                randomElement.getTotal())
        );
    }

    private TypeSpec createClass(String className, MethodSpec constructor) {
        return TypeSpec.classBuilder(className + ADD_SUFFIX)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(constructor)
                .build();
    }

    private String getPackage(String qualifier) {
        return qualifier.substring(0, qualifier.lastIndexOf(CHAR_DOT));
    }

    private String getClassName(String qualifier) {
        return qualifier.substring(qualifier.lastIndexOf(CHAR_DOT) + 1);
    }

}
複製程式碼

我們可以看到註解處理器總共有四個方法,他們分別的作用是:

  1. init() 可選
    在該方法中可以獲取到processingEnvironment物件,藉由該物件可以獲取到生成程式碼的檔案物件, debug輸出物件,以及一些相關工具類

  2. getSupportedSourceVersion()
    返回所支援的java版本,一般返回當前所支援的最新java版本即可

  3. getSupportedAnnotationTypes()
    你所需要處理的所有註解,該方法的返回值會被process()方法所接收

  4. process() 必須實現
    掃描所有被註解的元素,並作處理,最後生成檔案。該方法的返回值為boolean型別,若返回true,則代表本次處理的註解已經都被處理,不希望下一個註解處理器繼續處理,否則下一個註解處理器會繼續處理。

4.使用

好了,打了這麼多程式碼,我們先看下編譯時生成的程式碼和檔案是怎麼樣的,就會使用了:

一個例子帶你瞭解兩種自定義註解

我們可以看到,我們在註解處理器裡面寫了那麼多程式碼,就是為了生成Build目錄下的.class檔案,是自動生成的。

看到了生成的AnnotationActivity_Add的檔案,我們下面就去寫一個注入方法,把我們想要結果拿出來展示:

一個例子帶你瞭解兩種自定義註解

我們看到Util裡面我們實現了想要的東西,把AnnotationActivity_Add的結果找出來再賦值給相應的變數。

一個例子帶你瞭解兩種自定義註解
一個例子帶你瞭解兩種自定義註解

總結

我們成功的用兩種不同的註解方式實現了兩數相加的運算,1.運用的是反射,2.運用的是註解處理器。雖然看上去註解處理器的方式比較繁瑣,但是使用比較普遍,而且有很多好處,這裡就不一一述說。如果有興趣學習的同學可以下載原始碼去學習一下,互相交流,共同學習。原始碼下載連結

參考文章:

使用Android註解處理器,解放勞動生產力

JavaPoet – 優雅地生成程式碼

更多文章:
我的簡書

相關文章