天天看點

重識 Java 注解

如何了解注解?

注解(Annotation)作為一種中繼資料,由 Java 1.5 開始引入。和 Java 中的注釋有所不同,注釋是給人閱讀的,而注解的資訊可以在程式運作時擷取到。Java 注解也可以作為 XML 或者 Properties 的補充,如 Spring 3.x 開始引入注解,在配置類中标記了 @Bean 的方法的傳回對象可以作為 Spring 中的 Bean 對象。

注解的使用場景有哪些?

注解主要有以下3個使用場景。

  1. 提供給 Java 編譯器使用。例如 Java 編譯器在編譯時可以檢查标記了 @Override 注解的方法的父類中是否具有相同簽名的方法,如果沒有則會編譯失敗。
  2. 提供給注解處理器使用。例如開源架構 lombok 和 mapstruct 就使用了注解處理器。lombok 簡化了代碼,減少了代碼中的 set/get/toString 等方法。而 mapstruct 則将運作時通過反射擷取成員成員變量進行 copy 對象的行為提前到了編譯器,減少了反射的調用,提高了性能。
  3. 通過反射在運作期擷取注解資訊,改變程式行為。如 Spring 中 使用注解可以取代 xml 中對 bean 的配置,減少了瑣碎的配置,提高了開發效率。

注解的定義文法

  • 本質: Java 注解相關的類所在的包為

    java.lang.annotation

    ,本質是一個繼承了

    java.lang.annotation.Annotation

    的接口。
  • 元注解:注解可以用在類、成員變量、方法等,同時注解也可以用在注解上,用在注解上的注解就是元注解。如 @Documented 可用于生成文檔。

注解基本文法

注解基本文法如下:

[modifier] @interface AnnotationName {
	
}
           
  • modifier 是可選的通路權限修飾符,和類的通路權限修飾符一樣,如 public。
  • @interface 是 Java 注解定義固定的文法。
  • AnnotationName 是注解的辨別符,遵循 Java 命名規則。
  • AnnotationBody 是注解體,可以用來定義一些屬性。

自定義注解示例如下:

public @interface MyAnnotation {

}
           

為注解定義屬性

為了給注解攜帶其他資訊,需要為注解定義屬性。一個注解可以有0個到多個的屬性。每個屬性的文法如下:

  • public 為可選的通路權限修飾符,預設為 public 。
  • PropertyType為屬性的類型。可以使用的類型有:基本類型、String、Class、枚舉類型、注解類型、上述類型的數組。
  • PropertyName 為屬性名稱,遵循 Java 命名規範,後面的括号為固定的文法。
  • default 表示屬性的預設值,值由 DefaultValue 指定。

示例如下:

public @interface MyAnnotation {

    public String name();

    public int age() default 1;
}
           

注解目标

定義了上述的注解後,注解仍不能直接使用,因為并未指明注解可以使用的範圍,為了達到這個目的,則需要使用 Java 提供的元注解

@Target

使用方式如下:

@Target(ElementType.TYPE)
public @interface MyAnnotation {

    public String name();

    public int age() default 1;
}
           

其中 @Target 注解又有自己的 類型為 ElementType 的 value 屬性。ElementType 源碼如下。

public enum ElementType {
    /** 類、接口(包括注解)、枚舉 */
    TYPE,

    /** 字段 */
    FIELD,

    /** 方法 */
    METHOD,

    /** 方法參數 */
    PARAMETER,

    /** 構造方法 */
    CONSTRUCTOR,

    /** 本地變量 */
    LOCAL_VARIABLE,

    /** 注解類型 */
    ANNOTATION_TYPE,

    /** 包 */
    PACKAGE,

    /**
     * 類型參數
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * 類型名稱
     *
     * @since 1.8
     */
    TYPE_USE
}
           

注解保留政策

注解除了可以指定使用的目标,還具有自己的生命周期。注解的生命周期使用元注解

@Retention

指定。使用方式如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyAnnotation {

    public String name();

    public int age() default 1;
}
           

@Retention 有一個類型為 RetentionPolicy 的 value 屬性。RetentionPolicy 源碼如下:

public enum RetentionPolicy {
    /**
     * 注解保留在源代碼中,位元組碼不包含注解資訊
     */
    SOURCE,

    /**
     * 注解保留在源代碼和位元組碼中,運作時無法擷取到
     */
    CLASS,

    /**
     * 注解在源代碼、位元組碼、運作時都存在
     */
    RUNTIME
}
           

注解的繼承

注解本身并不可以被繼承,這裡說的繼承是子類繼承了使用具有 @Inherited 标記的注解的父類,則子類中可以擷取到父類中對應的注解資訊。

@Inherited 源碼如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}
           

可以看到,@Inherited 隻能用于注解類型。@Inherited 使用示例如下:

public class AnnotationTest {

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Inherited  //表示子類可以繼承父類中該注解
    public @interface InheritedAnnotation {

        int value();
    }

    @InheritedAnnotation(1)    //隻有在父類中标記的注解才會被繼承,接口中标記的不會被繼承
    static class Super{

    }

    // 子類繼承了使用 @Inherited 标記的注解的父類
    // @InheritedAnnotation(2) 子類可以覆寫父類中的注解
    static class Child extends Super{

    }

    public static void main(String[] args) {
        System.out.println(Child.class.getAnnotation(InheritedAnnotation.class));
    }

}
           

列印結果如下:

@InheritedAnnotation 注解被 @Inherited 注解标注,父類中标注了 @InheritedAnnotation 注解,子類成功擷取了注解資訊。

可重複注解

可重複注解是 Java 8 新增的一個特性,相同的注解可用在同一個元素中。在 Java 8 之前,為了達到這樣的效果,通常定義一個包含屬性名為value,類型為注解數組類型的注解,具體如下:

@MyAnnotations(value = {@MyAnnotation,@MyAnnotation})
public class AnnotationTest {

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface MyAnnotation {

    }

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface MyAnnotations {

        MyAnnotation[] value();
    }

}
           

Java 8 及之後,添加了元注解 @Repeatable ,@Repeatable的value值為注解的容器,這樣标記了 @Repeatable 注解的注解就可以用在相同的元素上,具體如下:

@MyAnnotation
@MyAnnotation
// 标記多個時擷取到的是該注解的容器注解
public class AnnotationTest {

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Repeatable(MyAnnotations.class)	//MyAnnotations為MyAnnotation的容器
    public @interface MyAnnotation {

    }

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface MyAnnotations {

        MyAnnotation[] value();
    }

    public static void main(String[] args) {
        Arrays.stream(AnnotationTest.class.getAnnotations()).forEach(System.out::println);
    }
}
           

程式列印如下:

可以看到,可重複注解隻是文法糖,如果标記了多個相同的注解,則擷取到的為該注解的容器注解。

将注解改為一個,即

@MyAnnotation
//@MyAnnotation
public class AnnotationTest {
	...
}
           

則列印結果為:

如果隻标記了一個注解,則擷取的直接和标記的注解相同。

标記一個注解和标記多個相同的注解在運作時擷取到的實際注解類型有所不同,這點需要進行留意。

Java 内建的注解

除了上述提到用于建立注解的元注解 @Documented、@Inherited、@Retention、@Repeatable ,Java 還提供了一些其他的注解。具體如下:

注解名稱 作用目标 生效範圍 作用
@Override METHOD SOURCE 父類如果不存在相同簽名的方法則編譯失敗
@Deprecated CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE RUNTIME 表示标記的元素已經過時,未來可能會被移除
@SuppressWarnings TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE SOURCE 抑制編譯器産生的警告
@SafeVarargs CONSTRUCTOR,METHOD RUNTIME Java 7 新增,表示構造器或方法不會對變長參數執行不安全的操作
@FunctionalInterface TYPE RUNTIME java 8 新增,表示接口為函數式接口

注解與反射

注解作為中繼資料,需要通過反射擷取注解的資訊。前面的文章 Java 基礎知識之 Java 反射 對注解進行了簡單說明,這裡再次進行總結。

可注解元素具體如下圖所示,可注解的元素都實作了接口 AnnotatedElement ,可以看到,包、類、成員變量、成員方法、構造方法、方法參數、類型變量等都可以進行注解。

重識 Java 注解

AnnotatedElement 源碼如下:

public interface AnnotatedElement {

	// 給定的注解是否存在
    default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
        return getAnnotation(annotationClass) != null;
    }
    
    //擷取給定類型的注解
    <T extends Annotation> T getAnnotation(Class<T> annotationClass);    

	//擷取所有的注解
    Annotation[] getAnnotations();

	//java 8 新增,根據類型擷取可重複注解的數組,如果不是可重複注解或可重複注解隻存在一個,則傳回長度為1的數組
    default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) {
         ... 省略部分代碼
     }

	// 擷取直接定義給定類型的注解
    default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) {
         ... 省略部分代碼
     }

	// java 8 新增,擷取給定類型的直接定義的可重複注解數組
    default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) {
        ... 省略部分代碼
    }

	//擷取直接定義的注解數組
    Annotation[] getDeclaredAnnotations();
    
}
           

透過位元組碼看注解

定義注解如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyAnnotation {

    String value() default "hello";

}
           

使用 javap 反編譯後的代碼如下:

public interface com.zzuhkp.MyAnnotation extends java.lang.annotation.Annotation {
  public abstract java.lang.String value();
}
           

可以看到,我們自己定義了注解編譯後變成了繼承 Annotation 的一個接口,并且屬性也變成了抽象方法。可以想到,我們擷取到的注解執行個體是 jvm 生成的注解的實作。

注解應用執行個體

沒有使用泛型的情況下,直接根據 AnnotatedElement 類上的方法擷取注解即可。如果需要擷取泛型中的注解,則需要先擷取泛型的類型參數,然後擷取類型參數上的注解。示例如下:

public class AnnotationTest {

    @MyAnnotation("commonField")
    private static String commonField;

    @MyAnnotation("genericField")
    private static Map<@MyAnnotation("map key") String, @MyAnnotation("map value") Integer> genericField;

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD, ElementType.TYPE_USE})
    public @interface MyAnnotation {

        String value() default "hello";

    }

    public static void main(String[] args) throws NoSuchFieldException {
        System.out.println("commonField上的注解如下:");
        Field commonField = AnnotationTest.class.getDeclaredField("commonField");
        Arrays.stream(commonField.getDeclaredAnnotations()).forEach(System.out::println);

        System.out.println("genericField上的注解如下:");
        Field genericField = AnnotationTest.class.getDeclaredField("genericField");
        Arrays.stream(genericField.getDeclaredAnnotations()).forEach(System.out::println);
        Type[] actualTypeArguments = ((ParameterizedType) genericField.getGenericType()).getActualTypeArguments();
        AnnotatedType[] annotatedActualTypeArguments = ((AnnotatedParameterizedType) genericField.getAnnotatedType())
            .getAnnotatedActualTypeArguments();
        for (int i = 0; i < actualTypeArguments.length; i++) {
            System.out.println("genericField 上泛型參數 " + actualTypeArguments[i].getTypeName() + " 上的注解如下:");
            Arrays.stream(annotatedActualTypeArguments[i].getAnnotations()).forEach(System.out::println);
        }
    }

}
           

列印結果為:

commonField上的注解如下:
@com.zzuhkp.AnnotationTest$MyAnnotation(value=commonField)
genericField上的注解如下:
@com.zzuhkp.AnnotationTest$MyAnnotation(value=genericField)
genericField 上泛型參數 java.lang.String 上的注解如下:
@com.zzuhkp.AnnotationTest$MyAnnotation(value=map key)
genericField 上泛型參數 java.lang.Integer 上的注解如下:
@com.zzuhkp.AnnotationTest$MyAnnotation(value=map value)