天天看點

Java Review(三十五、注解)基本注解JDK 的元注解自定義注解提取注解資訊使用注解執行個體

文章目錄

注解能被用來為程式元素( 類、 方法、 成員變量等) 設定中繼資料。 值得指出的是, 注解不影響程式代碼的執行, 無論增加、 删除注解, 代碼都始終如一地執行。 如果希望讓程式中的注解在運作時起一定的作用, 隻有通過某種配套的工具對注解中的資訊進行通路和處理, 通路和處理注解的工具統稱 APT( Annotation Processing Tool )。

Java 提供了5 個基本注解:

  • @Override:讓編譯器檢查該方法是否正确地實作了覆寫。
  • @Deprecated:用于表示某個程式元素( 類、 方法等) 己過時, 當其他程式使用己過時的類、 方法時,編譯器将會給出警告。
  • @SuppressWamings:告訴編譯器忽略此處代碼産生的警告。
  • @SafeVarargs:在聲明具有模糊類型(比如:泛型)的可變參數的構造函數或方法時,Java編譯器會報unchecked警告。鑒于這些情況,如果程式員斷定聲明的構造函數和方法的主體不會對其varargs參數執行潛在的不安全的操作,可使用@SafeVarargs進行标記,這樣的話,Java編譯器就不會報unchecked警告。
  • @FunctionalInterface:Java 8 規定: 如果接口中隻有一個抽象方法( 可以包含多個預設方法或多個 static方法), 該接口就是函數式接口。 @FunctionalInterface 就是用來指定某個接口必須是函數式接口。

JDK 除在 java.lang下提供 5 個基本的注解之外, 還在 java.lang.annotation 包下提供了 6 個 Meta 注解 ( 元注解), 其中有 5 個元注解都用于修飾其他的注解定義。

@Retention 隻能用于修飾注解定義, 用于指定被修飾的注解可以保留多長時間, @Retention 包含一個 RetentionPolicy 類型的 value 成員變量, 是以使用@Retention 時必須為該 value 成員變量指定值。

value 成員變量的值隻能是如下三個:

  • RetentionPolicy.CLASS: 編譯器将把注解記錄在 class 檔案中。 當運作 Java 程式時, JVM 不可擷取注解資訊。 這是預設值。
  • RetentionPolicy.RUNTIME: 編譯器将把注解記錄在 class 檔案中。 當運作 Java 程式時, JVM 也可擷取注解資訊, 程式可以通過反射擷取該注解資訊。
  • RetentionPolicy.SOURCE: 注解隻保留在源代碼中, 編譯器直接丢棄這種注解。

如 果 需 要 通 過 反 射 獲 取 注 解 信 息 , 就 需 要 使 用 value 屬 性 值 為 RetentionPolicy.RUNTIME 的@Retention。

使用@Retention 元注解可釆用如下代碼為 value 指定值:

// 定義下面的(testable 注解保留到運作時
@Retention(value= RetentionPolicy.RUNTIME)
public @interface Testable{}      

也可采用如下代碼來為 value 指定值:

// 定義下面的(testable 注解将被編譯器直接丢棄
@Retention(RetentionPolicy.SOURCE)
public @interface Testable{}      
java.lang.annotation.Retention

@Target 也隻能修飾注解定義, 它用于指定被修飾的注解能用于修飾哪些程式單元。 @Target 元注解也包含一個名為 value 的成員變量, 該成員變量的值隻能是如下幾個:

  • ElementType.ANNOTATION_TYPE 指定該政策的注解隻能修飾注解。
  • ElementType.CONSTRUCTOR 指定該政策的注解隻能修飾構造器。
  • ElementType.FIELD 指定該政策的注解隻能修飾成員變量。
  • ElementType.LOCAL_VARIABLE 指定該政策的注解隻能修飾局部變量。
  • ElementType.METHOD 指定該政策的注解隻能修飾方法定義。
  • ElementType.PACKAGE 指定該政策的注解隻能修飾包定義。
  • ElementType.PARAMETER 指定該政策的注解可以修飾參數。
  • ElementType.TYPE 指定該政策的注解可以修飾類、 接口(包括注解類型) 或枚舉定義。

如下代碼指定@ActionListenerFor 注解隻能修飾成員變量:

©Target(ElementType.FIELD)
public @interface ActionListenerFor{}      

如下代碼片段指定@Testable 注解隻能修飾方法:

@Target(ElementType.METHOD)
public @interface Testable { }      
java.lang.annotation.Target

©Documented 用于指定被該元注解修飾的注解類将被 javadoc 工具提取成文檔, 如果定義注解類時使用了©Documented 修飾, 則所有使用該注解修飾的程式元素的 API 文檔中将會包含該注解說明。

java.lang.annotation.Documented

©Inherited 元注解指定被它修飾的注解将具有繼承性—如果某個類使用7@Xxx 注解( 定義該注解時使用了@Inherited 修飾) 修飾, 則其子類将自動被@Xxx 修飾。

java.lang.annotation.Inherited

Java語言使用@interface文法來定義注解(Annotation),格式如下:

/ / 定義一個簡單的注解類型
public @interface Test{

}      

在預設情況下, 注解可用于修飾任何程式元素, 包括類、 接口、 方法等, 如下程式使用@Test 來修飾方法:

public class MyClass{
  // 使用@Test 注解修飾方法
  @Test
  public void info(){
  
  }
}      

注解不僅可以是這種簡單的注解, 還可以帶成員變量, 成員變量在注解定義中以無形參的方法形式來聲明, 其方法名和傳回值定義了該成員變量的名字和類型。

如下代碼可以定義一個有成員變量的注解:

public @interface MyTag{
  // 定義帶兩個成員變量的注解
  // 注解中的成員變量以方法的形式來定義
  String name();
  int age();
}       

注解的參數類似無參數方法,可以用default設定一個預設值(強烈推薦)。最常用的參數應當命名為value:

public @interface MyTag{
  // 定義帶兩個成員變量的注解
  // 注解中的成員變量以方法的形式來定義
  String name() default "牛鋼鐵";
  int age() 666;
}       

也可以在使用 MyTag 注解時為成員變量指定值, 如果為 MyTag 的成員變量指定了值, 則預設值不會起作用:

public class MyTest{
  @MyTag(name="麻球", age=6)
  public void info(){

  }
}      

通常會用元注解去修飾自定義注解,如上文所示。

例如,使用@Target可以定義Annotation能夠被應用于源碼的哪些位置:

//定義MyTag注解應用于方法上
@Target(ElementType.METHOD)    
public @interface MyTag{
  // 定義帶兩個成員變量的注解
  // 注解中的成員變量以方法的形式來定義
  String name() default "牛鋼鐵";
  int age() 666;
}       

使用注解修飾了類、 方法、 成員變量等成員之後, 這些注解不會自己生效, 必須由開發者提供相應的工具來提取并處理注解資訊。

因為注解定義後也是一種class,所有的注解都繼承自 java.lang.annotation.Annotation,是以,讀取注解,需要使用反射API。

Java提供的使用反射API讀取Annotation的方法包括:

判斷某個注解是否存在于Class、Field、Method或Constructor:

  • Class.isAnnotationPresent(Class)
  • Field.isAnnotationPresent(Class)
  • Method.isAnnotationPresent(Class)
  • Constructor.isAnnotationPresent(Class)

例如:

// 判斷@Report是否存在于Person類:
Person.class.isAnnotationPresent(Report.class);      

使用反射API讀取Annotation:

  • Class.getAnnotation(Class)
  • Field.getAnnotation(Class)
  • Method.getAnnotation(Class)
  • Constructor.getAnnotation(Class)
// 擷取Person定義的@Report注解:
Report report = Person.class.getAnnotation(Report.class);
int type = report.type();
String level = report.level();      

使用反射API讀取Annotation有兩種方法。方法一是先判斷Annotation是否存在,如果存在,就直接讀取:

Class cls = Person.class;
if (cls.isAnnotationPresent(Report.class)) {
    Report report = cls.getAnnotation(Report.class);
    ...
}      

第二種方法是直接讀取Annotation,如果Annotation不存在,将傳回null:

Class cls = Person.class;
Report report = cls.getAnnotation(Report.class);
if (report != null) {
   ...
}      

讀取方法、字段和構造方法的Annotation和Class類似。但要讀取方法參數的Annotation就比較麻煩一點,因為方法參數本身可以看成一個數組,而每個參數又可以定義多個注解,是以,一次擷取方法參數的所有注解就必須用一個二維數組來表示。例如,對于以下方法定義的注解:

public void hello(@NotNull @Range(max=5) String name, @NotNull String prefix) {
}      

要讀取方法參數的注解,我們先用反射擷取Method執行個體,然後讀取方法參數的所有注解:

// 擷取Method執行個體:
Method m = ...
// 擷取所有參數的Annotation:
Annotation[][] annos = m.getParameterAnnotations();
// 第一個參數(索引為0)的所有Annotation:
Annotation[] annosOfName = annos[0];
for (Annotation anno : annosOfName) {
    if (anno instanceof Range) { // @Range注解
        Range r = (Range) anno;
    }
    if (anno instanceof NotNull) { // @NotNull注解
        NotNull n = (NotNull) anno;
    }
}      

注解@Testable 沒有任何成員變量,僅是一個标記注解,它的作用是标記哪些方法是可測試的:

Testable.java

import java.lang.annotation.*;

// 使@Retention指定注解的保留到運作時
@Retention(RetentionPolicy.RUNTIME)
// 使用@Target指定被修飾的注解可用于修飾方法
@Target(ElementType.METHOD)
// 定義一個标記注解,不包含任何成員變量,即不可傳入中繼資料
public @interface Testable
{
}      

如下 MyTest 測試用例中定義了 8 個方法, 這 8 個方法沒有太大的差別, 其中 4 個方法使用@Testable注解來标記這些方法是可測試的:

public class MyTest
{
    // 使用@Testable注解指定該方法是可測試的
    @Testable
    public static void m1()
    {
    }
    public static void m2()
    {
    }
    // 使用@Testable注解指定該方法是可測試的
    @Testable
    public static void m3()
    {
        throw new IllegalArgumentException("參數出錯了!");
    }
    public static void m4()
    {
    }
    // 使用@Testable注解指定該方法是可測試的
    @Testable
    public static void m5()
    {
    }
    public static void m6()
    {
    }
    // 使用@Testable注解指定該方法是可測試的
    @Testable
    public static void m7()
    {
        throw new RuntimeException("程式業務出現異常!");
    }
    public static void m8()
    {
    }
}      

@Range注解,我們希望用它來定義一個String字段的規則——字段長度滿足@Range的參數定義:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {
    int min() default 0;
    int max() default 255;
}      

在某個JavaBean中,使用注解:

public class Person {
    @Range(min=1, max=20)
    public String name;

    @Range(max=10)
    public String city;
}      

定義了注解,本身對程式邏輯沒有任何影響。必須編寫代碼來使用注解。這裡,編寫一個Person執行個體的檢查方法,它可以檢查Person執行個體的String字段長度是否滿足@Range的定義:

void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
    // 周遊所有Field:
    for (Field field : person.getClass().getFields()) {
        // 擷取Field定義的@Range:
        Range range = field.getAnnotation(Range.class);
        // 如果@Range存在:
        if (range != null) {
            // 擷取Field的值:
            Object value = field.get(person);
            // 如果值是String:
            if (value instanceof String) {
                String s = (String) value;
                // 判斷值是否滿足@Range的min/max:
                if (s.length() < range.min() || s.length() > range.max()) {
                    throw new IllegalArgumentException("Invalid field: " + field.getName());
                }
            }
        }
    }
}      

參考:

【1】:《瘋狂Java講義》

【2】:

廖雪峰的官方網站:使用注解

【3】:

春晨:@SafeVarargs注解的使用

【4】:

廖雪峰的官方網站:自定義注解

【5】:

廖雪峰的官方網站:處理注解