Java 基礎(十七)註解

diamond_lin發表於2017-11-22

定義(What)

註解,是原始碼的後設資料。

兩個本質:

  • 它就是一個附屬品,依賴於其他元素存在
  • 本身沒有任何作用,在恰當的時候由外部程式解析產生作用。

作用(Why)

  • 簡化配置
  • 增加程式碼可讀性
  • 提升系統可維護性

型別(How)

按定義分類:

分為內建註解和自定義註解。

內建註解如下:

  • @Override
  • @Deprecated
  • @SuppressWarnings

自定義註解?android.support.annotation包下全是自定義註解,不知道大家注意過沒。貼個圖~

按生命週期分類(重點)

  • SOURCE:表示在編譯時這個註解會被移除,不會包含在編譯後產生的 class 檔案中
  • CLASS:邊上這個註解會被包含在 class 檔案中,但執行時會被移除
  • RUNTIME:表示這個註解會被保留到執行時,在執行時可以 JVM 訪問到,我們可以在執行時通過反射解析到這個註解。

可能有同學會問:不管我是用於編譯時程式碼生成還是執行時反射處理,我直接對所有註解申明RetentionPolicy.RUNTIME不就好了嗎?或者即使我想在編譯時程式碼生成我也用RetentionPolicy.SOURCE,也是可以的吧?

沒錯,RetentionPolicy.RUNTIME是優先順序最大的修飾,但為什麼不建議呢?這個的原因同修飾類成員時用的private還是public得道理一樣。

元註解(重點)

磨刀不誤砍柴工,先弄清楚“元註解”,然後我們再來學習自定義註解。

Retention定義註解生命週期,可選為:source、class、runtime(生命週期介紹如上)

Documented 文件化註解,會被 Javadoc 工具文件化

Inherited註解自動整合的,想讓一個類和他的子類都包含某個註解,就可以使用他來修飾這個註解。

Target說明了被修飾的註解的應用範圍,包括:

  • TYPE:表示可以用來修飾類、介面、註解型別或列舉型別
  • PACKAGE:可以用來修飾包
  • PARAMETER:可以用來修飾引數
  • ANNOTATION_TYPE:可以用來修飾註解型別
  • METHOD:可以用來修飾方法
  • FIELD:可以用來修飾屬性(包含列舉常量)
  • CONSTRUCTOR:可以用來修飾構造器
  • LOCAL_VARLABLE:可以用來修飾區域性變數

敲黑板,Retention 和 Target 是重點,METHOD 是 Target 的重點。

自定義註解

首先我們來建立一個註解類,其實就是一個介面前面加了一個@符號。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface CacheResult {

    String key();

    String cacheName();

    String backupKey() default "";//備份快取

    boolean needBloomFilter() default false;//解決快取擊穿

    boolean needLock() default false;//是否開啟讀取快取執行緒鎖

}複製程式碼

然後我們用Retention給註解設定生命週期為 Runtime,用 Target 為註解設定這個註解可以用來修飾類和方法。然後我們給註解新增了五條屬性,其中前兩條沒有預設值(使用的時候需要手動設定,格式如下 demo)、後三條有預設值(可選設定值)。
以上註解是從 spring 框架中 copy 的,那5條屬性沒看懂沒事哈,都是自定義的,可以隨便刪,我們主要是學習註解怎麼用。

public class TestAnnotation {

    public static void main(String[] args) {
        Class<TestBean> testBeanClass = TestBean.class;
        Class<CacheResult> cacheResultClass = CacheResult.class;
        if (testBeanClass.isAnnotationPresent(cacheResultClass)) {
            CacheResult annotation = testBeanClass.getAnnotation(cacheResultClass);
            System.out.println(annotation.key());
            System.out.println(annotation.cacheName());
            Method[] methods = testBeanClass.getMethods();
            for (Method m : methods) {
                if (m.isAnnotationPresent(cacheResultClass)) {
                    CacheResult a = m.getAnnotation(cacheResultClass);
                    System.out.println(a.key());
                    System.out.println(a.cacheName());
                }
            }
        }
    }

    @CacheResult(key = "class", cacheName = "className")
    public class TestBean {

        @CacheResult(key = "method", cacheName = "methodName")
        public void test() {

        }
    }
}複製程式碼

列印結果

class
className
method
methodName

Process finished with exit code 0複製程式碼

通過以上 demo,我們要記住註解的兩個本質:

  • 它就是一個附屬品,依賴於其他元素存在
  • 本身沒有任何作用,在恰當的時候由外部程式解析產生作用。

結合 Retrofit

我們都記得 Retrofit 的Api 是基於註解的,我們來看看 Retrofit 是怎麼讀取註解的

上圖這個過程是Retrofit 在讀取 AppApiService介面的這個方法。

/**
 * 獲取首頁圖片
 *
 * @param size 獲取圖片張數
 */
@GET("http://lab.zuimeia.com/wallpaper/category/1/")
Observable<HttpResultV1<ImageData>> getImage(@Query("page_size") int size);複製程式碼

我們來簡單看一下ServiceMethod.Builder 類的構造方法做了些什麼事

public Builder(Retrofit retrofit, Method method) {
  this.retrofit = retrofit;
  this.method = method;
  this.methodAnnotations = method.getAnnotations();//獲取方法上的註解
  this.parameterTypes = method.getGenericParameterTypes();//獲取方法引數列表
  this.parameterAnnotationsArray = method.getParameterAnnotations();//獲取方法引數的二維陣列,為什麼是二維陣列呢,因為一個引數可以有多個註解呀。
}複製程式碼

然後就是 build 方法解析註解了。

public ServiceMethod build() {
    for (Annotation annotation : methodAnnotations) {
    parseMethodAnnotation(annotation);
  }
    int parameterCount = parameterAnnotationsArray.length;
  parameterHandlers = new ParameterHandler<?>[parameterCount];
  for (int p = 0; p < parameterCount; p++) {
    Type parameterType = parameterTypes[p];
    if (Utils.hasUnresolvableType(parameterType)) {
      throw parameterError(p, "Parameter type must not include a type variable or wildcard: %s",
          parameterType);
    }

    Annotation[] parameterAnnotations = parameterAnnotationsArray[p];
    if (parameterAnnotations == null) {
      throw parameterError(p, "No Retrofit annotation found.");
    }

    parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations);
  }
}複製程式碼

以上就是 build 方法中解析註解的部分程式碼,細節就不去仔細看了。具體 Retrofit 是怎麼解析這些引數以及解析的這些引數是幹嘛的不是我們這裡關心的內容。

自定義註解(重點)

上面我們學了

  • 使用jdk 反射獲取註解資訊
  • 在 Retrofit 中是怎麼獲取註解資訊的

這裡我丟擲兩個問題:

  • 註解好像僅僅是用於解析自定義註解資訊,好像並沒有什麼卵用啊
  • 你用的最多的自定義標籤有哪些?

在我們常用的 ButterKnife 中,就用了一個BindView 註解,就可以成功給 View 初始化

@BindView(R.id.tv_welcome)
TextView mTvWelcome;複製程式碼

這裡我們來研究一下究竟是怎麼辦到的。

首先我們來看 BindView 的程式碼

@Retention(CLASS) @Target(FIELD)
public @interface BindView {
    /** View ID to which the field will be bound. */
    @IdRes int value();
}複製程式碼

原始碼中這些資訊應該都看得懂吧,前面都講了的,我們來簡單看一下@IdRes 的返回值限定

@Documented
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})
public @interface IdRes {
}複製程式碼

這裡只是一種型別限定,限定了返回型別只能說 Res 的資源。

而BindView的value 方法是沒有 default 值的,而 value 的值又限定死了只能是 int 型的 Res 資源。所以@BindView(R.id.tv_welcome)括號裡面需要給註解賦值。

然後問題來了,前文我們強調過註解的本質,我們再來回顧一下:

  • 它就是一個附屬品,依賴於其他元素存在
  • 本身沒有任何作用,在恰當的時候由外部程式解析產生作用。

那麼被 BindView標註的 TextView mTvWelcome 是怎麼被賦值的呢,我們來看看ButterKnife.bind() 方法,這個方法一般是在 onCreate 方法裡面呼叫,傳參一般是用 this。呼叫了這個方法,然後mTvWelcome欄位有了BindView 註解就被賦初始值了。那麼我們來看看 bind 方法裡面的執行吧。

public static Unbinder bind(@NonNull Activity target) {
    View sourceView = target.getWindow().getDecorView();
    return createBinding(target, sourceView);
}複製程式碼

這個程式碼很簡單,直接跳過了。

這個程式碼也簡單,findBindingConstructorForClass方法找到了一個叫LaunchActivity_ViewBinding 的類,然後再呼叫構造方法。然後,然後就結束了?這個 LaunchActivity_ViewBinding 類特麼是個什麼鬼。。。
然後我在 build 檔案裡面找到了這個類~

在這裡,我們找到了 findViewById 的方法給mTvWelcome做了初始化操作。

然後還剩下一個問題,這個 LaunchActivity_ViewBinding 到底是從哪裡來的。

這個問題好像有點超綱了,我簡單介紹一下吧,LaunchActivity_ViewBinding 是由APT 生成的,不知道在引入 ButterKnife 的時候大家是否還記得在 gradle 裡面加了一行這樣的程式碼apt com.jakewharton:butterknife-compiler:8.4.0.沒錯,這些build 檔案裡面的程式碼都是 apt 生成的。

APT

APT英文全稱:Android annotation process tool是一種處理註釋的工具,它對原始碼檔案進行檢測找出其中的Annotation,使用Annotation進行額外的處理。

Annotation處理器在處理Annotation時可以根據原始檔中的Annotation生成額外的原始檔和其它的檔案(檔案具體內容由Annotation處理器的編寫者決定),APT還會編譯生成原始檔和原來的原始檔,將它們一起生成class檔案。簡言之:APT可以把註解,在編譯時生成程式碼。

ButterKnife 中 apt的大致工作流程就是這樣:程式碼編寫完,在編譯的時候掃描java 檔案,對包含某些特定註解的 java 檔案進行掃描獲取欄位及其註解的值,然後使用諸如“javapoet”之類的程式碼生成工具生成build 目錄中的***Activity_ViewBinding 檔案。

自定義註解實現編譯時檢查程式碼

如圖,使用了 NotNull 註解標識的引數,如果傳 null 會報黃色警告。

納尼,註解的本質我都能背得下來了,我背給你看~

它就是一個附屬品,依賴於其他元素存在
本身沒有任何作用,在恰當的時候由外部程式解析產生作用。

說好的沒有任何作用呢~ 看了@NotNull 的原始碼,好像原始碼用的幾個關鍵字也沒有讓程式碼報黃色警告的意思啊。

於是,不服氣的我把 @NotNull 程式碼 copy 出來,寫了一個新的自定義註解 @No。

What the Fuck!!!為什麼我自己寫的註解沒有黃色警告。心涼了三分鐘~

好吧,通過上面的測試不得不承認,這個黃色警告跟程式碼沒關係,大概是編譯器的功能吧。於是,經過一番 Baidu 和諮詢好友。找到了這個功能~

然後你也可以修改警告級別。

同樣用於程式碼提示的註解還有 @Override(標記重寫父類的方法)、@Deprecated(過期方法)、@SuppressWarnings(忽略警告)。

註解就到這裡吧,高階玩法可以去研究一下 Retrofit 或者 ButterKnife 的 apt。java Srping 框架的註解玩也玩很溜,對後臺感興趣的可以去學習。

相關文章