Spring系列20:注解詳解和Spring注解增強(基礎內功)


有部分小伙伴反饋說前面基於注解的Spring中大量使用注解,由於對Java的注解不熟悉,有點難受。建議總結一篇的Java注解的基礎知識,那么,它來了!

本文內容

  1. 什么是注解?
  2. 如何定義注解
  3. 如何使用注解
  4. 如何獲取注解信息
  5. Spring 中對注解做了什么增強?

什么是注解?

什么是代碼中寫的注釋?那是給開發者看的,但是編譯之后的字節碼文件中是沒有注釋信息的,也就是說注釋對於java編譯器和JVM來說是沒有意義的,看不到!

類比注釋是給人看的,注解則是給java編譯器和JVM看的一些標識,編譯器和虛擬機在運行的過程中可以獲取注解信息來做一些處理。

如何定義注解

注解定義的語法如下:

public	@interface 注解類名{
    參數類型 參數名稱1() [default 參數默認值];
    參數類型 參數名稱2() [default 參數默認值];
}

參數名稱可以沒有,也可以定義多個,定義細節如下:

  • 參數類型只能是基本類型、String、Class、枚舉類型、注解類型以及對應的一維數組
  • 如果注解參數只有1個,建議定義名稱為value,方便使用時缺省參數名
  • default 可以指定默認值,如果沒有默認值使用注解時必須給定參數值

定義注解時候需要考慮2個主要問題:

  • 注解可以使用在哪里也就是使用范圍?
  • 注解保留到什么階段,源碼階段,還是運行階段?

java提供了一部分的元注解來解決上面的問題。

@Target指定注解的使用范圍

來看下源碼,主要是指定了可以應用注釋類型的元素種類的數組參數。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    // 返回可以應用注釋類型的元素種類的數組
    ElementType[] value();
}
/*注解的使用范圍*/
public enum ElementType {
    /*類、接口、枚舉、注解上面*/
    TYPE,
    /*字段上*/
    FIELD,
    /*方法上*/
    METHOD,
    /*方法的參數上*/
    PARAMETER,
    /*構造函數上*/
    CONSTRUCTOR,
    /*本地變量上*/
    LOCAL_VARIABLE,
    /*注解上*/
    ANNOTATION_TYPE,
    /*包上*/
    PACKAGE,
    /*類型參數上 1.8之后*/
    TYPE_PARAMETER,
    /*類型名稱上 1.8之后*/
    TYPE_USE
}

@Retention指定注解的保留策略

指示要保留帶注釋類型的注釋多長時間。如果注釋類型聲明中不存在保留注釋,則保留策略默認為 RetentionPolicy.CLASS

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
  
    RetentionPolicy value();
}
public enum RetentionPolicy {
    // 源碼階段,注解被編譯器丟棄。
    SOURCE,

    // 注釋將由編譯器記錄在類文件中,但不需要在運行時由 VM 保留。這是默認行為。
    CLASS,

    // 注釋將由編譯器記錄在類文件中,並在運行時由 VM 保留,因此可以反射性地讀取它們。
    RUNTIME
}

綜合上面2個注解,自定義一個保留到運行期的僅用在方法上的注解如下。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DemoAnnotation {
    String name() default "";
    Class targetClazz();
}

如何使用注解

使用語法

@注解類(參數1=值1,參數2=值2,參數3=值3,參數n=值n)
目標對象

使用前一節的注解來個簡單的案例

public class MyBean {
    @DemoAnnotation(name = "xxx", targetClazz = MyBean.class)
    public void m() {

    }
}

來一個綜合案例,注解位置包括類上、方法上、構造函數上、方法參數上、字段上、本地變量上、泛型類型參數和類型名稱上。

/**
 * 綜合案例
 * @author zfd
 * @version v1.0
 * @date 2022/1/24 13:31
 * @關於我 請關注公眾號 螃蟹的Java筆記 獲取更多技術系列
 */
@StrongAnnotation(value = "用在類上", elementType = ElementType.TYPE)
public class UseStrongAnnotation<@StrongAnnotation(value = "用在類型參數上T0", elementType = ElementType.TYPE_PARAMETER) T0,
        @StrongAnnotation(value = "用在類型名稱上T1", elementType = ElementType.TYPE_USE) T1> {
    @StrongAnnotation(value = "用在字段上", elementType = ElementType.FIELD)
    private String field;

    @StrongAnnotation(value = "構造方法上", elementType = ElementType.CONSTRUCTOR)
    public UseStrongAnnotation(@StrongAnnotation(value = "用在方法參數上", elementType = ElementType.PARAMETER) String field) {
        this.field = field;
    }

    @StrongAnnotation(value = "用在普通方法上", elementType = ElementType.METHOD)
    public void m(@StrongAnnotation(value = "方法參數上", elementType = ElementType.PARAMETER) String name) {
        @StrongAnnotation(value = "用在本地變量上", elementType = ElementType.LOCAL_VARIABLE)
        String prefix = "hello ";
        System.out.println(prefix + name);
    }

    public <@StrongAnnotation(value = "方法的類型參數T2上", elementType = ElementType.TYPE_PARAMETER) T2> void m1() {

    }
    public <@StrongAnnotation(value = "方法的類型名稱T3上", elementType = ElementType.TYPE_USE) T3> void m2() {

    }

    private Map<@StrongAnnotation(value = "Map后面的尖括號也是類型名稱", elementType = ElementType.TYPE_USE) String ,
    @StrongAnnotation(value = "Map后面的尖括號也是類型名稱", elementType = ElementType.TYPE_PARAMETER)Object> map;
}

如何獲取注解信息

java.lang.reflect.AnnotatedElement接口表示當前在此 VM 中運行的程序的注解元素。 該接口允許以反射方式讀取注解。 此接口中方法返回的所有注解都是不可變的和可序列化的。 該接口的方法返回的數組可以被調用者修改,而不影響返回給其他調用者的數組。其獲取注解的主要方法如下,見名知意。

image-20220124150250851

主要的實現類或接口圖如下

image-20220124150441074

對應的實現的含義也很明確:

  • Package:用來表示包的信息
  • Class:用來表示類的信息
  • Constructor:用來表示構造方法信息
  • Field:用來表示類中屬性信息
  • Method:用來表示方法信息
  • Parameter:用來表示方法參數信息
  • TypeVariable:用來表示類型變量信息,如:類上定義的泛型類型變量,方法上面定義的泛型類型變量

綜合案例

來一個綜合案例來解析上一節的注解使用UseStrongAnnotation。測試用例和結果如下,建議多實戰敲敲代碼。

package com.crab.spring.ioc.demo17;

import com.sun.xml.internal.bind.v2.model.core.ID;
import org.junit.Test;

import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.Arrays;

import static org.junit.Assert.*;

/**
 * @author zfd
 * @version v1.0
 * @date 2022/1/24 13:52
 * @關於我 請關注公眾號 螃蟹的Java筆記 獲取更多技術系列
 */
public class UseStrongAnnotationTest {
    @Test
    public void test_annotated_class() {
        System.out.println("解析類上注解:");
        Arrays.stream(UseStrongAnnotation.class.getAnnotations())
                .forEach(System.out::println);
    }
    // 解析類上注解:
    // @com.crab.spring.ioc.demo17.StrongAnnotation(value=用在類上, elementType=TYPE)

    @Test
    public void test_annotated_class_type_parameter() {
        TypeVariable<Class<UseStrongAnnotation>>[] typeParameters = UseStrongAnnotation.class.getTypeParameters();
        for (TypeVariable<Class<UseStrongAnnotation>> typeParameter : typeParameters) {
            System.out.println(typeParameter.getName() + " 1.8變量參數或變量名稱注解信息:");
            Annotation[] annotations = typeParameter.getAnnotations();
            Arrays.stream(annotations).forEach(System.out::println);
        }
    }
    // T0 1.8變量參數或變量名稱注解信息:
    // @com.crab.spring.ioc.demo17.StrongAnnotation(value=用在類型參數上T0, elementType=TYPE_PARAMETER)
    // T1 1.8變量參數或變量名稱注解信息:
    // @com.crab.spring.ioc.demo17.StrongAnnotation(value=用在類型名稱上T1, elementType=TYPE_USE)

    @Test
    public void test_annotated_field() throws NoSuchFieldException {
        Field field = UseStrongAnnotation.class.getDeclaredField("field");
        Arrays.stream(field.getAnnotations()).forEach(System.out::println);
    }
    // @com.crab.spring.ioc.demo17.StrongAnnotation(value=用在字段上, elementType=FIELD)
    

    @Test
    public void test_annotated_constructor() {
        Constructor<?> constructor = UseStrongAnnotation.class.getDeclaredConstructors()[0];
        for (Annotation annotation : constructor.getAnnotations()) {
            System.out.println(annotation);
        }
    }
    // @com.crab.spring.ioc.demo17.StrongAnnotation(value=構造方法上, elementType=CONSTRUCTO

    @Test
    public void test_annotated_normal_method() throws NoSuchMethodException {
        Method method = UseStrongAnnotation.class.getDeclaredMethod("m", String.class);
        System.out.println("方法注解:");
        for (Annotation annotation : method.getAnnotations()) {
            System.out.println(annotation);
        }
        System.out.println("方法參數注解:");
        Parameter[] parameters = method.getParameters();
        for (Parameter parameter : parameters) {
            System.out.println(parameter.getName() + " 參數注解:");
            for (Annotation annotation : parameter.getAnnotations()) {
                System.out.println(annotation);
            }
        }
    }
    // 方法注解:
    // @com.crab.spring.ioc.demo17.StrongAnnotation(value=用在普通方法上, elementType=METHOD)
    // 方法參數注解:
    // name 參數注解:
    // @com.crab.spring.ioc.demo17.StrongAnnotation(value=方法參數上, elementType=PARAMETER)

    @Test
    public void test_annotated_map_type() throws NoSuchFieldException {
        Field field = UseStrongAnnotation.class.getDeclaredField("map");
        // 返回一個 Type 對象,該對象表示此 Field 對象表示的字段的聲明類型。
        // 如果 Type 是參數化類型,則返回的 Type 對象必須准確反映源代碼中使用的實際類型參數。
        Type genericType = field.getGenericType();
        // 獲取返回表示此類型的實際類型參數的 Type 對象數組
        Type[] actualTypeArguments = ((ParameterizedType) genericType).getActualTypeArguments();

        // 返回一個 AnnotatedType 對象,該對象表示使用一個類型來指定此 Field 表示的字段的聲明類型。
        AnnotatedType annotatedType = field.getAnnotatedType();
        // 獲取此參數化類型的可能帶注釋的實際類型參數數組
        AnnotatedType[] annotatedActualTypeArguments =
                ((AnnotatedParameterizedType) annotatedType).getAnnotatedActualTypeArguments();
        int index = 0;
        for (AnnotatedType annotatedActualTypeArgument : annotatedActualTypeArguments) {
            Type actualTypeArgument = actualTypeArguments[index++];
            System.out.println(annotatedActualTypeArgument.getType());
            System.out.println(actualTypeArgument.getTypeName() + " 類型上的注解:");
            for (Annotation annotation : annotatedActualTypeArgument.getAnnotations()) {
                System.out.println(annotation);
            }
        }

    }
    // T0 1.8變量參數或變量名稱注解信息:
    // @com.crab.spring.ioc.demo17.StrongAnnotation(value=用在類型參數上T0, elementType=TYPE_PARAMETER)
    // T1 1.8變量參數或變量名稱注解信息:
    // @com.crab.spring.ioc.demo17.StrongAnnotation(value=用在類型名稱上T1, elementType=TYPE_USE)


}

@Inherited實現子類繼承父類的注解

@Inherited指示注解類型是自動繼承的。注意針對的父類的注解,接口是無效的

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

來看一個案例,父類和接口上都有可繼承的注解,觀察下子類的上的注解情況。

/**
 * 測試父類注解的繼承
 * 注意:是類,不是接口,接口無效
 * @author zfd
 * @version v1.0
 * @date 2022/1/24 17:15
 * @關於我 請關注公眾號 螃蟹的Java筆記 獲取更多技術系列
 */
public class TestInherited {

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @interface Annotation1{}

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @interface Annotation2{}


    @Annotation1
    interface Interface1{}
    @Annotation2
    static class SupperClass{}

    // 繼承 SupperClass 實現 Interface1,觀察其注解繼承情況
    static class SubClass extends SupperClass implements Interface1{}

    public static void main(String[] args) {
        for (Annotation annotation : SubClass.class.getAnnotations()) {
            System.out.println(annotation);
        }
    }
    // 輸出
    // @com.crab.spring.ioc.demo17.TestInherited$Annotation2()
    // 只繼承了父類注解 無法繼承接口上的注解
}

@Repeatable重復注解

常規情況下同一個目標上是無法使用同一個注解多個重復標記的。如果自定義注解需要實現可重復注解,則在定義的時候可以使用 @Repeatable來聲明的注解類型是可重復的。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
    // 指定容器注解類型
    Class<? extends Annotation> value();
}

模擬 @ComponentScan @ComponentScans來提供一個案例。

/**
 * 測試 @Repeatable 注解重復使用
 * @author zfd
 * @version v1.0
 * @date 2022/1/24 17:30
 * @關於我 請關注公眾號 螃蟹的Java筆記 獲取更多技術系列
 */
public class TestRepeatable {
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Repeatable(ComponentScans.class)
    @interface  ComponentScan{}

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @interface ComponentScans{
        // 注意: 必須定義value參數,其類型是子重復注解的數組類型
        ComponentScan[] value();
    }

    // 重復注解方式1
    @ComponentScan
    @ComponentScan
    static class MyComponent{}

    // 重復注解方式2
    @ComponentScans({@ComponentScan, @ComponentScan})
    static class MyComponentB{}

    public static void main(String[] args) {
        for (Annotation annotation : MyComponent.class.getAnnotations()) {
            System.out.println(annotation);
        }
        for (Annotation annotation : MyComponentB.class.getAnnotations()) {
            System.out.println(annotation);
        }
    }
    // 輸出
    // @com.crab.spring.ioc.demo17.TestRepeatable$ComponentScans(value=[@com.crab.spring.ioc.demo17
    // .TestRepeatable$ComponentScan(), @com.crab.spring.ioc.demo17.TestRepeatable$ComponentScan()])
    // @com.crab.spring.ioc.demo17.TestRepeatable$ComponentScans(value=[@com.crab.spring.ioc.demo17
    // .TestRepeatable$ComponentScan(), @com.crab.spring.ioc.demo17.TestRepeatable$ComponentScan()])

}

Spring 中@AliasFor對注解的增強

注解的定義參數是不能繼承,如注解A上面有注解B,但是實際在使用B注解在目標類C的過程中想要設置A的參數是做不到的。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationA {
    String name() default "";
    int value() default -1;
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@AnnotationA
public @interface AnnotationB {
    String name() default "";
    int value() default 1;
    String aliasForName() default "";
}

@AnnotationB(name = "xxx", value = 1) // 無法設置AnnotiaonA的參數值
public class ClassC {
}

Spring 中 提供了@AliasFor 元注解,用於聲明注解屬性的別名,主要的使用場景:

  • 注解中的顯式別名:在單個注解中, @AliasFor可以在一對屬性上聲明,以表明它們是彼此可互換的別名
  • 元注解中屬性的顯式別名:如果@AliasFor的annotation屬性設置為與聲明它的注解不同的注解,則該attribute被解釋為元注解中屬性的別名(即顯式元注解屬性覆蓋)。 這可以精確控制注解層次結構中覆蓋的屬性。
  • 注解中的隱式別名:如果注解中的一個或多個屬性被聲明為相同元注解屬性的屬性覆蓋(直接或傳遞),則這些屬性將被視為彼此的一組隱式別名,從而導致類似於注解中顯式別名的行為。

源碼簡單過一下

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface AliasFor {

	@AliasFor("attribute")
	String value() default "";

	@AliasFor("value")
	String attribute() default "";

	// 聲明別名屬性的注解類型。默認為 Annotation,這意味着別名屬性在與此屬性相同的注解中聲明。
	Class<? extends Annotation> annotation() default Annotation.class;

}

綜合案例

來使用@AliasFor 改造下 AnnotationB。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@AnnotationA
public @interface AnnotationB {
    // 注解AnnotationB內部顯式別名
    @AliasFor(value = "aliasForName")
    String name() default "";

    int value() default 1;

    // 注解AnnotationB內部顯式別名
    @AliasFor(annotation = AnnotationB.class, attribute = "name")
    String aliasForName() default "";

    // 元注解AnnotationA屬性name顯式別名
    @AliasFor(annotation = AnnotationA.class, value = "name")
    String aliasForAnnotationAName() default "";

    // 元注解AnnotationA屬性name顯式別名2
    @AliasFor(annotation = AnnotationA.class, value = "name")
    String aliasForAnnotationAName2() default "";

    // 元注解AnnotationA屬性value顯式別名
    @AliasFor(annotation = AnnotationA.class, value = "value")
    int aliasForAnnotationAValue() default -1;
}

使用AnnotationB 注解,注意:互為別名的屬性設置時只能設置其中一個,否則設置多個會報錯。

@AnnotationB(value = 100,
        name = "xx",
        aliasForAnnotationAName = "a1",
        aliasForAnnotationAValue = -100
)
public class ClassC2 {
    public static void main(String[] args) {
        //spring提供一個查找注解的工具類AnnotatedElementUtils
        System.out.println(AnnotatedElementUtils.getMergedAnnotation(ClassC2.class, AnnotationB.class));
        System.out.println(AnnotatedElementUtils.getMergedAnnotation(ClassC2.class, AnnotationA.class));
    }
}

輸出結果顯示AnnotationB 通過別名設置AnnotationA中屬性成功。

@com.crab.spring.ioc.demo17.AnnotationB(aliasForAnnotationAName=a1, aliasForAnnotationAName2=a1, aliasForAnnotationAValue=-100, aliasForName=xx, name=xx, value=100)
@com.crab.spring.ioc.demo17.AnnotationA(name=a1, value=-100)

總結

本文詳解了注解的概念,如何定義注解、使用注解、獲取注解;並介紹了元注解@Target、@Retention、@Inherited、@Repeatable 的使用;重點講解了Spring 中 @AliasFor 注解來為元注解屬性設置別名的增強處理。

本篇源碼地址: https://github.com/kongxubihai/pdf-spring-series/tree/main/spring-series-ioc/src/main/java/com/crab/spring/ioc/demo17
知識分享,轉載請注明出處。學無先后,達者為先!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM