Java學習之註解Annotation實現原理

總李寫程式碼發表於2016-07-15

前言:

   最近學習了EventBus、BufferKinfe、GreenDao、Retrofit 等優秀開源框架,它們新版本無一另外的都使用到了註解的方式,我們使用在使用的時候也嚐到不少好處,基於這種想法我覺得有必要對註解有個更深刻的認識,今天中午把公司的專案搞完了,晚上加個班學習總結一下Java的註解。

什麼是註解?

      對於很多初次接觸的開發者來說應該都有這個疑問?Annontation是Java5開始引入的新特徵,中文名稱叫註解。它提供了一種安全的類似註釋的機制,用來將任何的資訊或後設資料(metadata)與程式元素(類、方法、成員變數等)進行關聯。為程式的元素(類、方法、成員變數)加上更直觀更明瞭的說明,這些說明資訊是與程式的業務邏輯無關,並且供指定的工具或框架使用。Annontation像一種修飾符一樣,應用於包、型別、構造方法、方法、成員變數、引數及本地變數的宣告語句中。

註解的用處:

      1、生成文件。這是最常見的,也是java 最早提供的註解。常用的有@param @return 等

      2、跟蹤程式碼依賴性,實現替代配置檔案功能。比如Dagger 2依賴注入,未來java開發,將大量註解配置,具有很大用處;

      3、在編譯時進行格式檢查。如@override 放在方法前,如果你這個方法並不是覆蓋了超類方法,則編譯時就能檢查出。

元註解:

java.lang.annotation提供了四種元註解,專門註解其他的註解:

   @Documented –註解是否將包含在JavaDoc中
   @Retention –什麼時候使用該註解
   @Target –註解用於什麼地方
   @Inherited – 是否允許子類繼承該註解

  1.)@Retention– 定義該註解的生命週期
  •   RetentionPolicy.SOURCE : 在編譯階段丟棄。這些註解在編譯結束之後就不再有任何意義,所以它們不會寫入位元組碼。@Override, @SuppressWarnings都屬於這類註解。
  •   RetentionPolicy.CLASS : 在類載入的時候丟棄。在位元組碼檔案的處理中有用。註解預設使用這種方式
  •   RetentionPolicy.RUNTIME : 始終不會丟棄,執行期也保留該註解,因此可以使用反射機制讀取該註解的資訊。我們自定義的註解通常使用這種方式。

  舉例:bufferKnife 8.0 中@BindView 生命週期為CLASS

@Retention(CLASS) @Target(FIELD)
public @interface BindView {
  /** View ID to which the field will be bound. */
  @IdRes int value();
}
  2.)Target – 表示該註解用於什麼地方。預設值為任何元素,表示該註解用於什麼地方。可用的ElementType引數包括
  • ElementType.CONSTRUCTOR:用於描述構造器
  • ElementType.FIELD:成員變數、物件、屬性(包括enum例項)
  • ElementType.LOCAL_VARIABLE:用於描述區域性變數
  • ElementType.METHOD:用於描述方法
  • ElementType.PACKAGE:用於描述包
  • ElementType.PARAMETER:用於描述引數
  • ElementType.TYPE:用於描述類、介面(包括註解型別) 或enum宣告

 舉例Retrofit 2 中@Field 作用域為引數

@Documented
@Target(PARAMETER)
@Retention(RUNTIME)
public @interface Field {
  String value();

  /** Specifies whether the {@linkplain #value() name} and value are already URL encoded. */
  boolean encoded() default false;
}
 3.)@Documented–一個簡單的Annotations標記註解,表示是否將註解資訊新增在java文件中。
 4.)@Inherited – 定義該註釋和子類的關係

     @Inherited 元註解是一個標記註解,@Inherited闡述了某個被標註的型別是被繼承的。如果一個使用了@Inherited修飾的annotation型別被用於一個class,則這個annotation將被用於該class的子類。

常見標準的Annotation:

  1.)Override

      java.lang.Override是一個標記型別註解,它被用作標註方法。它說明了被標註的方法過載了父類的方法,起到了斷言的作用。如果我們使用了這種註解在一個沒有覆蓋父類方法的方法時,java編譯器將以一個編譯錯誤來警示。

  2.)Deprecated

     Deprecated也是一種標記型別註解。當一個型別或者型別成員使用@Deprecated修飾的話,編譯器將不鼓勵使用這個被標註的程式元素。所以使用這種修飾具有一定的“延續性”:如果我們在程式碼中通過繼承或者覆蓋的方式使用了這個過時的型別或者成員,雖然繼承或者覆蓋後的型別或者成員並不是被宣告為@Deprecated,但編譯器仍然要報警。

 3.)SuppressWarnings

     SuppressWarning不是一個標記型別註解。它有一個型別為String[]的成員,這個成員的值為被禁止的警告名。對於javac編譯器來講,被-Xlint選項有效的警告名也同樣對@SuppressWarings有效,同時編譯器忽略掉無法識別的警告名。

@SuppressWarnings("unchecked") 

自定義註解:

這裡模擬一個滿足網路請求介面,以及如何獲取介面的註解函式,引數執行請求。

1.)定義註解:


@ReqType 請求型別

@Documented
@Target(METHOD)
@Retention(RUNTIME)
public @interface ReqType {

    /**
     * 請求方式列舉
     *
     */
    enum ReqTypeEnum{ GET,POST,DELETE,PUT};

    /**
     * 請求方式
     * @return
     */
    ReqTypeEnum reqType() default ReqTypeEnum.POST;
}

@ReqUrl 請求地址

@Documented
@Target(METHOD)
@Retention(RUNTIME)
public @interface ReqUrl {
    String reqUrl() default "";
}

@ReqParam 請求引數

@Documented
@Target(PARAMETER)
@Retention(RUNTIME)
public @interface ReqParam {
    String value() default "";
}

從上面可以看出註解引數的可支援資料型別有如下:

           1.所有基本資料型別(int,float,boolean,byte,double,char,long,short)
    2.String型別
    3.Class型別
    4.enum型別
    5.Annotation型別
    6.以上所有型別的陣列

 而且不難發現@interface用來宣告一個註解,其中的每一個方法實際上是宣告瞭一個配置引數。方法的名稱就是引數的名稱,返回值型別就是引數的型別(返回值型別只能是基本型別、Class、String、enum)。可以通過default來宣告引數的預設值。

2.)如何使用自定義註解
public interface IReqApi {
    
    @ReqType(reqType = ReqType.ReqTypeEnum.POST)//宣告採用post請求
    @ReqUrl(reqUrl = "www.xxx.com/openApi/login")//請求Url地址
    String login(@ReqParam("userId") String userId, @ReqParam("pwd") String pwd);//引數使用者名稱 密碼
    
}
3.)如何獲取註解引數

 這裡強調一下,Annotation是被動的後設資料,永遠不會有主動行為,但凡Annotation起作用的場合都是有一個執行機制/呼叫者通過反射獲得了這個後設資料然後根據它採取行動。

通過反射機制獲取函式註解資訊

      Method[] declaredMethods = IReqApi.class.getDeclaredMethods();
        for (Method method : declaredMethods) {
            Annotation[]  methodAnnotations = method.getAnnotations();
            Annotation[][] parameterAnnotationsArray = method.getParameterAnnotations();
        }

也可以獲取指定的註解

ReqType reqType =method.getAnnotation(ReqType.class);
 4.)具體實現註解介面呼叫

這裡採用Java動態代理機制來實現,將定義介面與實現分離開,這個後期有時間再做總結。

    private void testApi() {
        IReqApi api = create(IReqApi.class);
        api.login("whoislcj", "123456");
    }

    public <T> T create(final Class<T> service) {
        return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[]{service},
                new InvocationHandler() {

                    @Override
                    public Object invoke(Object proxy, Method method, Object... args)
                            throws Throwable {// Annotation[]  methodAnnotations = method.getAnnotations();//拿到函式註解陣列
                        ReqType reqType = method.getAnnotation(ReqType.class);
                        Log.e(TAG, "IReqApi---reqType->" + (reqType.reqType() == ReqType.ReqTypeEnum.POST ? "POST" : "OTHER"));
                        ReqUrl reqUrl = method.getAnnotation(ReqUrl.class);
                        Log.e(TAG, "IReqApi---reqUrl->" + reqUrl.reqUrl());
                        Type[] parameterTypes = method.getGenericParameterTypes();
                        Annotation[][] parameterAnnotationsArray = method.getParameterAnnotations();//拿到引數註解
                        for (int i = 0; i < parameterAnnotationsArray.length; i++) {
                            Annotation[] annotations = parameterAnnotationsArray[i];
                            if (annotations != null) {
                                ReqParam reqParam = (ReqParam) annotations[0];
                                Log.e(TAG, "reqParam---reqParam->" + reqParam.value() + "==" + args[i]);
                            }
                        }
                        //下面就可以執行相應的網路請求獲取結果 返回結果
                        String result = "";//這裡模擬一個結果

                        return result;
                    }
                });
    }

列印結果:

以上通過註解定義引數,通過動態代理方式執行函式,模擬了最基本的Retrofit 2中網路實現原理。

相關文章