【進階】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提供,是專門用來定義註解的註解。
-
@Target
描述註解能夠作用的位置,ElementType取值:
- ElementType.TYPE,可以作用於類上
- ElementType.METHOD,可以作用於方法上
- ElementType.FIELD,可以作用在成員變數上
-
@Retention
表示需要在什麼級別儲存該註釋資訊(生命週期):
RetentionPolicy.RUNTIME
:記憶體中的位元組碼,VM將在執行時也保留註解,因此可以通過反射機制讀取註解的資訊
-
@Documented
描述註解是否被抽取到api文件中。
-
@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類的例項:
-
若已知具體的類,可以通過類的class屬性獲取,該fang'shi最為安全可靠,且程式效能最高。
//類的class屬性 Class classOne = User.class;
2. 已知某個類的例項,通過呼叫該例項的getClass方法獲取Class物件。
```java
//已有類物件的getClass方法
Class collatz = user.getClass();
-
已知一個類的全類名,且該類在類路徑下,可以通過靜態方法forName()獲取。
Class c = Class.forName("com.dcone.zhuzqc.demo.User");
-
內建基本資料型別可以直接使用類名.Type獲取。
//內建物件才有的TYPE屬性,較大的侷限性 Class<Integer> type = Integer.TYPE;
-
利用ClassLoader(類載入器)獲取。
5.3可獲得Class物件的型別
-
class:外部類、成員(成員內部類,靜態內部類),區域性內部類,匿名內部類;
//類可以反射 Class c1 = Person.class;
-
interface:所有介面;
//介面可以反射 Class c2 = Comparable.class;
-
[]:陣列;
//陣列可以反射 Class c3 = String[].class; Class c4 = int[][].class;
-
enum:列舉;
//列舉可以反射 Class c6 = ElementType.class;
-
annotation:註解(@interface);
//註解可以反射 Class c5 = Data.class;
-
基本資料型別;
//基本資料型別(包裝類)可以反射 Class c7 = int.class; Class c8 = Integer.class;
-
void。
//void可以反射 Class c9 = void.class;
六、類的載入與ClassLoader
6.1類的載入過程
當程式主動使用某個類時,如果該類還未被載入到記憶體中,則系統會通過如下3個步驟來對該類進行初始化。
-
類的載入(Load):將類的 class 檔案位元組碼內容讀入記憶體,並將這些靜態資料轉換成方法區執行時的資料結構,同時建立一個java.lang.Class物件,此過程由類載入器完成;
-
類的連結(Link):將類的二進位制資料合併到 JRE 中,確保載入的類資訊符合 JVM 規範,同時 JVM 將常量池內的引用替換為地址。
-
類的初始化(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"; }