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

有時候轉過頭看回一些基礎知識,才發現原來當時候本身以爲很難的東西都是從基礎知識衍生而來的,忽然會有點豁然開朗的感受。譬如說咱們今天要講的知識點———註解。java

初識,無處不在的註解

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

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

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

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。api

  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、Dagger二、DBFlow、Retrofit、JUnit數組

以上都是總結了大部分在《Android高級進階》中出現的註解的地方,不少註解沒一個個解釋,有興趣的同窗能夠本身去搜索一下本身想知道的註解的具體用途。咱們能夠看到註解不管在java和Android中都是使用很普遍的,並且慢慢變得必不可少。下面咱們就進入咱們的主題,分別用兩種方式去自定義註解。bash

那麼咱們先列出一個簡單的題目,而後用兩種不一樣的方式去實現:框架

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

1、運行時自定義註解:

運行時註解通常和反射機制配合使用,相比編譯時註解性能比較低,但靈活性好,實現起來比較簡單,因此咱們先來用這個去實現。dom

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.使用

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

1、編譯時自定義註解:

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

著名的第三方框架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 - 優雅地生成代碼

更多文章: 個人簡書

相關文章
相關標籤/搜索