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

【進階】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";
        }