【進階】Spring中的註解與反射

2022-06-08 12:01:37

【進階】Spring中的註解與反射

前言

註解(Annotation)不是程式,但可以對程式作出解釋,也可以被其它程式(如編譯器)讀取。

註解的格式:以@註釋名在程式碼中存在,還可以新增一些引數值例如@SuppressWarnings(value="unchecked")。

註解可在package、class、method、field等上面使用,作用是為它們新增了額外的輔助資訊,從而可以通過反射機制實現對這些後設資料的存取。

一、內建(常用)註解

1.1@Overrode

表示某方法旨在覆蓋超類中的方法宣告,該方法將覆蓋或實現在超類中宣告的方法。

1.2@RequestMapping

@RequestMapping註解的主要用途是將Web請求與請求處理類中的方法進行對映,注意有以下幾個屬性:

  • value:對映的請求URL或者其別名
  • value:對映的請求URL或者其別名
  • params:根據HTTP引數的存在、預設或值對請求進行過濾

1.3@RequestBody

@RequestBody在處理請求方法的參數列中使用,它可以將請求主體中的引數繫結到一個物件中,請求主體引數是通過HttpMessageConverter傳遞的,根據請求主體中的引數名與物件的屬性名進行匹配並繫結值。此外,還可以通過@Valid註解對請求主體中的引數進行校驗。

1.4@GetMapping

@GetMapping註解用於處理HTTP GET請求,並將請求對映到具體的處理方法中。具體來說,@GetMapping是一個組合註解,它相當於是@RequestMapping(method=RequestMethod.GET)的快捷方式。

1.5@PathVariable

@PathVariable註解是將方法中的引數繫結到請求URI中的模板變數上。可以通過@RequestMapping註解來指定URI的模板變數,然後使用@PathVariable註解將方法中的引數繫結到模板變數上。

1.6@RequestParam

@RequestParam註解用於將方法的引數與Web請求的傳遞的引數進行繫結。使用@RequestParam可以輕鬆的存取HTTP請求引數的值。

1.7@ComponentScan

@ComponentScan註解用於設定Spring需要掃描的被元件註解註釋的類所在的包。可以通過設定其basePackages屬性或者value屬性來設定需要掃描的包路徑。value屬性是basePackages的別名。

1.8@Component

@Component註解用於標註一個普通的元件類,它沒有明確的業務範圍,只是通知Spring被此註解的類需要被納入到Spring Bean容器中並進行管理。

1.9@Service

@Service註解是@Component的一個延伸(特例),它用於標註業務邏輯類。與@Component註解一樣,被此註解標註的類,會自動被Spring所管理。

1.10@Repository

@Repository註解也是@Component註解的延伸,與@Component註解一樣,被此註解標註的類會被Spring自動管理起來,@Repository註解用於標註DAO層的資料持久化類。


二、元註解

4個元個元註解分別是:@Target、@Retention、@Documented、@Inherited 。

再次強調下元註解是java API提供,是專門用來定義註解的註解。

  1. @Target

    描述註解能夠作用的位置,ElementType取值:

    • ElementType.TYPE,可以作用於類上
    • ElementType.METHOD,可以作用於方法上
    • ElementType.FIELD,可以作用在成員變數上
  2. @Retention

    表示需要在什麼級別儲存該註釋資訊(生命週期):

    • RetentionPolicy.RUNTIME:記憶體中的位元組碼,VM將在執行時也保留註解,因此可以通過反射機制讀取註解的資訊
  3. @Documented

    描述註解是否被抽取到api檔案中。

  4. @Inherited

    描述註解是否被子類繼承。


三、自定義註解

學習自定義註解對於理解Spring框架十分有好處,即使在實際專案中可能不需要使用自定義註解,但可以幫助我們掌握Spring的一些底層原理,從而提高對整體專案的把握。

/**
 * 自定義註解
 * @author Created by zhuzqc on 2022/5/31 23:03
 */
public class CustomAnnotation {

    /**
     * 註解中可以為引數賦值,如果沒有預設值,那麼必須為註解的引數賦值
     * */
    @MyAnnotation(value = "解釋")
    public void test(){
    }
}
/**
 * @author zhuzqc
 */
//自定義註解必須的元註解target,指明註解的作用域(此處指明的是在類和方法上起作用)
@Target({ElementType.TYPE,ElementType.METHOD})
//元註解retention宣告該註解在何時起作用(此處指明的是在執行時起作用)
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation{

    //註解中需宣告引數,格式為:引數型別 + 引數名();
    String value() default "";

}

四、反射機制概述

4.1動態語言與靜態語言

4.1.1動態語言
  • 是一種在執行時可以改變其結構的語言,例如新的函數、物件甚至程式碼可以被引進,已有的函數可以被刪除或是進行其它結構上的變化。

  • 主要的動態語言有:Object-C、C#、PHP、Python、JavaScript 等。

  • 以 JavaScript 語言舉例:

    /**
     * 由於未指定var的具體型別,函數在執行時間可以改變var的型別
     * */
    function f(){
        var x = "var a = 3; var b = 5; alert(a+b)";
        eval(x)
    }
    
4.2.2靜態語言
  • 與動態語言相對的、執行時結構不可變的語言就是靜態語言,如 Java、C、C++ 等。
  • Java 不是動態語言,但 Java 可以稱為」準動態語言「。即 Java 有一定的動態性,可以利用反射機制獲得類似於動態語言的特性,從而使得 Java 語言在程式設計時更加靈活。

4.2Java Reflection(Java 反射)

Reflection(反射)是 Java 被視為準動態語言的關鍵:反射機制允許程式在執行期間藉助 Reflection API 獲取任何類的內部資訊,並能直接操作任意物件的內部屬性及方法。

Class c = Class.forName("java.lang.String")

載入完類後,在堆記憶體的方法區就產生了一個Class型別的物件(一個類只有一個Class物件),這個類就包含了完整的類的結構資訊。我沒可以通過這個物件,像鏡子一樣看到類的結構,這個過程形象地被稱之為反射。

通過程式碼更易於理解:

/**
 * 反射的概念
 * @author Created by zhuzqc on 2022/6/1 17:40
 */
public class ReflectionTest extends Object{
    public static void main(String[] args) throws ClassNotFoundException {
        //通過反射獲取類的Class物件
        Class c = Class.forName("com.dcone.zhuzqc.demo.User");
        //一個類在記憶體中只有唯一個Class物件
        System.out.println(c.hashCode());

    }
}

/**
 * 定義一個實體類entity
 * */
@Data
class User{
    private String userName;
    private Long userId;
    private Date loginTime;
}

由於該類繼承 Object,在 Object 類中有 getClass() 方法,該方法被所有子類繼承:

@HotSpotIntrinsicCandidate
public final native Class<?> getClass();

注:該方法的返回值型別是一個 Class 類,該類是 Java 反射的源頭。

反射的優點執行期型別的判斷、動態載入類、提高程式碼靈活度

4.2.1反射機制主要功能
  • 在執行時判斷、呼叫任意一個類的物件資訊(成員變數和方法等);
  • 在執行時獲取泛型資訊;
  • 在執行時處理註解;
  • 生成動態代理。
4.2.2主要API
  • java.lang.Class:代表一個類

  • java.lang.reflect.Field:代表類的成員變數

  • java.lang.reflect.Method:代表類的方法

  • java.lang.reflect.Constructor:代表類的構造器


五、理解Class類並獲取Class範例

5.1Class類

前面提到,反射後可以得到某個類的屬性、方法和構造器、實現的介面。

  • 對於每個類而言,JRE都為其保留一個不變的 Class 型別的物件;
  • 一個載入的類在 JVM 中只會有一個 Class 範例;
  • Class 類是Reflection的根源,想要通過反射獲得任何動態載入的、執行的類,都必須先獲取相應的 Class 物件。

5.2獲取Class類範例

有以下5種方式可以獲取Class類的範例:

  1. 若已知具體的類,可以通過類的class屬性獲取,該fang'shi最為安全可靠,且程式效能最高。

    //類的class屬性
    Class classOne = User.class;
    
   
2. 已知某個類的範例,通過呼叫該範例的getClass方法獲取Class物件。

   ```java
   //已有類物件的getClass方法
   Class collatz = user.getClass();
  1. 已知一個類的全類名,且該類在類路徑下,可以通過靜態方法forName()獲取。

    Class c = Class.forName("com.dcone.zhuzqc.demo.User");
    
  2. 內建基本資料型別可以直接使用類名.Type獲取。

    //內建物件才有的TYPE屬性,較大的侷限性
    Class<Integer> type = Integer.TYPE;
    
  3. 利用ClassLoader(類載入器)獲取。

5.3可獲得Class物件的型別

  1. class:外部類、成員(成員內部類,靜態內部類),區域性內部類,匿名內部類;

    //類可以反射
        Class c1 = Person.class;
    
  2. interface:所有介面;

    //介面可以反射
         Class c2 = Comparable.class;
    
  3. []:陣列;

    //陣列可以反射
         Class c3 = String[].class;
         Class c4 = int[][].class;
    
  4. enum:列舉;

    //列舉可以反射
         Class c6 = ElementType.class;
    
  5. annotation:註解(@interface);

    //註解可以反射
         Class c5 = Data.class;
    
  6. 基本資料型別;

    //基本資料型別(包裝類)可以反射
         Class c7 = int.class;
         Class c8 = Integer.class;
    
  7. void。

    //void可以反射
         Class c9 = void.class;
    

六、類的載入與ClassLoader

6.1類的載入過程

當程式主動使用某個類時,如果該類還未被載入到記憶體中,則系統會通過如下3個步驟來對該類進行初始化。

  1. 類的載入(Load):將類的 class 檔案位元組碼內容讀入記憶體,並將這些靜態資料轉換成方法區執行時的資料結構,同時建立一個java.lang.Class物件,此過程由類載入器完成;

  2. 類的連結(Link):將類的二進位制資料合併到 JRE 中,確保載入的類資訊符合 JVM 規範,同時 JVM 將常數池內的參照替換為地址。

  3. 類的初始化(Initialize):JVM 負責對類進行初始化,分為類的主動參照和被動參照。

    • 類的主動參照
      • 虛擬器啟動時,先初始化main方法所在的類;
      • new 類的物件;
      • 呼叫類的靜態(static)成員和靜態(static)方法;
      • 使用java.lang.reflect包的方法對類進行反射呼叫;
      • 如果該類的父類別沒有被初始化,則會先初始化它的父類別。
    • 類的被動參照
      • 當存取到一個靜態域時,只有真正宣告這個域的類才會被初始化;
      • 通過陣列定義類的參照,不會觸發此類的初始化;
      • 參照常數不會觸發此類的初始化

6.2類載入器

JVM支援兩種型別的類載入器,分別為引導類載入器(BootstrapClassLoader)和自定義類載入器(User-Defined ClassLoader)。

從概念上來講,自定義類載入器一般指的是程式中由開發人員自定義的一類,類載入器。

但是Java虛擬機器器規範卻沒有這麼定義,而是將所有派生於抽象類ClassLoader的類載入器都劃分為自定義類載入器。

無論類載入器的型別如何劃分,在程式中我們最常見的類載入器始終只有3個,具體如下圖所示:

類載入器

所以具體為引導類載入器(BootstrapClassLoader)和自定義類載入器(包括ExtensionClassLoader、Application ClassLoader(也叫System ClassLoader)、User Defined ClassLoader)。

public class Test03 {
    public static void main(String[] args) {
        //獲取系統類的載入器
        ClassLoader sysLoader = ClassLoader.getSystemClassLoader();
        System.out.println(sysLoader);

        //獲取系統類的父類別載入器
        ClassLoader parent = sysLoader.getParent();
        System.out.println(parent);
    }
}

七、獲取執行時類的完整物件

通過反射獲取執行時類的完整結構:Field、Method、Constructor、Superless、Interface、Annotation等。

即:實現的全部介面、所繼承的父類別、全部的構造器、全部的方法、全部的成員變數(區域性變數)、註解等。

/**
 * @author Created by zhuzqc on 2022/6/5 0:16
 */
public class Test04 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class c1 = Class.forName("com.dcone.zhuzqc.demo.User");
        //獲取所有屬性
        Field field[];
        field = c1.getDeclaredFields();
        for (Field f:field){
            System.out.println(f);
        }
        //獲得類的方法
        Method method[];
        method = c1.getDeclaredMethods();
        for (Method m:method){
            System.out.println(m);
        }
    }
}

八、反射獲取泛型資訊

Java 中採用泛型擦除的機制來引入泛型,Java 中的泛型僅僅是給編譯器 javac 使用的,目的是確保資料的安全性以及免去強制型別轉換的問題。一旦編譯完成,所有和泛型相關的型別全部擦除。

在Java中可以通過反射獲取泛型資訊的場景有如下三個:

  • (1)成員變數的泛型
  • (2)方法引數的泛型
  • (3)方法返回值的泛型

在Java中不可以通過反射獲取泛型資訊的場景有如下兩個:

  • (1)類或介面宣告的泛型
  • (2)區域性變數的泛型

要獲取泛型資訊,必須要注意ParameterizedType類,該類中的getActualTypeArguments()方法可以有效獲取泛型資訊。

下面以獲取成員方法引數的泛型型別資訊為例:

public class Demo {
    public static void main(String[] args) throws NoSuchMethodException, NoSuchFieldException {
 
        // 獲取成員方法引數的泛型型別資訊
        getMethodParametricGeneric();
    }
 /**
     * 獲取方法引數的泛型型別資訊
     *
     * @throws NoSuchMethodException
     */
    public static void getMethodParametricGeneric() throws NoSuchMethodException {
        // 獲取MyTestClass類中名為"setList"的方法
        Method setListMethod = MyClass.class.getMethod("setList", List.class);
        // 獲取該方法的引數型別資訊(帶有泛型)
        Type[] genericParameterTypes = setListMethod.getGenericParameterTypes();
        // 但我們實際上需要獲取返回值型別中的泛型資訊,所以要進一步判斷,即判斷獲取的返回值型別是否是引數化型別ParameterizedType
        for (Type genericParameterType : genericParameterTypes) {
            ParameterizedType parameterizedType = (ParameterizedType) genericParameterType;
            // 獲取成員方法引數的泛型型別資訊
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            for (Type actualTypeArgument : actualTypeArguments) {
                Class realType = (Class) actualTypeArgument;
                System.out.println("成員方法引數的泛型資訊:" + realType);
            }
        }
    }

九、反射獲取註解資訊

在開發中可能會遇到這樣的場景:獲取類的屬性釋義,這些釋義定義在類屬性的註解中。

/**
 * 定義一個實體類entity
 * */
@Data
class User{
    @ApiModelProperty(value = "姓名")
    private String userName;

    @ApiModelProperty(value = "使用者id")
    private Long userId;

    @ApiModelProperty(value = "登入時間")
    private Date loginTime;
}

那麼可以如何獲取註解中的屬性資訊呢?

解決方案:

這裡我們使用反射,以及java.lang下的兩個方法:

//如果指定型別的註釋存在於此元素上,  方法返回true 
java.lang.Package.isAnnotationPresent(Class<? extends Annotation> annotationClass) 
//如果是該型別的註釋, 方法返回該元素的該型別的註釋
java.lang.Package.getAnnotation(Class< A > annotationClass) 
    public static void main(String[] args) throws ClassNotFoundException {
        Class c1 = Class.forName("com.dcone.zhuzqc.demo.User");

        if(User.class.isAnnotationPresent(ApiModel.class)){
            System.out.println(User.class.getAnnotation(ApiModel.class).value());
        }
        // 獲取類變數註解
        Field[] fields = User.class.getDeclaredFields();
        for (Field f : fields) {
            if(f.isAnnotationPresent(ApiModelProperty.class)){
                System.out.print(f.getAnnotation(ApiModelProperty.class).name() + ",");
            }
        }
    }
  • 拓展1:獲取方法上的註解

        @Bean("sqlSessionFactory")
        public String test(@RequestBody User user) throws ClassNotFoundException {
            Class c2 = Class.forName("com.dcone.zhuzqc.demo.User");
            // 獲取方法註解:
            Method[] methods = User.class.getDeclaredMethods();
            for(Method m : methods){
                if (m.isAnnotationPresent((Class<? extends Annotation>) User.class)) {
                    System.out.println(m.getAnnotation(ApiModelProperty.class).annotationType());
                }
            }
            return "test";
        }
    
  • 拓展2:獲取方法引數上的註解

        @Bean("sqlSessionFactory")
        public String test(@RequestBody User user) throws ClassNotFoundException {
            Class c2 = Class.forName("com.dcone.zhuzqc.demo.User");
            // 獲取方法引數註解
            Method[] methods2 = User.class.getDeclaredMethods();
            for (Method m : methods2) {
                // 獲取方法的所有引數
                Parameter[] parameters = m.getParameters();
                for (Parameter p : parameters) {
                    // 判斷是否存在註解
                    if (p.isAnnotationPresent(ApiModelProperty.class)) {
                        System.out.println(p.getAnnotation(ApiModelProperty.class).name());
                    }
                }
            }
            return "test";
        }