Java反射詳解,學以致用,實戰案例(AOP修改引數、Mybatis攔截器實現自動填充)
highlight: atom-one-dark theme: qklhk-chocolate
持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第1天,點選檢視活動詳情
作為Java開發者,你認為反射這個知識點重要程度,在你心裡是什麼樣的呢?
以前我也只覺得反射非常重要,但總歸是聽這個文章說,聽那個朋友說,學是學了,但卻沒怎麼應用。
當我正式進入到社會當 cv 仔的時候,需要考慮的問題多了,慢慢思考問題了,就覺得反射是個力大無窮的東西,更會感覺反射是個無所不能的東西,如各種各樣的框架的底層,各種各樣的攔截器的實現,反射都是其中少不了的一部分~
如果平時著重於開發業務的話,那麼確實可能會較少使用到反射機制,但並非是說反射它不重要,反射它是Java 框架的基礎勒,可以說木有反射,Java的動態性是會受限的~
文章大致思路:
全文共 7500 字左右,案例均可執行,閱讀時間大約需要20分鐘左右,如有問題,請留言或傳送郵件([email protected])。
編寫的過程中,即使書寫完已閱讀過,但難免可能會出現遺漏,如有發現問題請及時聯絡修正,非常感謝你的閱讀,希望我們都能成為技術道路上的朋友。
一、反射是什麼?
JAVA 反射機制是在執行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意一個方法和屬性;這種動態獲取的資訊以及動態呼叫物件的方法的功能稱為java語言的反射機制。
不過要想解剖一個類,就要先獲取到該類的位元組碼檔案對應的Class型別的物件.
稍後就會講到~
"反射之所以被稱為框架的靈魂",主要是因為它賦予了我們在執行時分析類以及執行類中方法的能力,這種能力也就是我們常說的動態性,利用這種性質可以使編寫的程式更靈活更通用。
反射機制可以用來:
- 在執行時分析類的能力,如可以構造任意一個類,可以獲取任意一個類的全部資訊,
- 在執行時檢查物件,如在執行時判斷任意一個物件所屬的類
- 實現泛型陣列操作程式碼,因為在執行時可以獲取泛型資訊
- 利用Method物件,如我們經常使用的動態代理,就是使用
Method.invoke()
來實現方法的呼叫。
反射是一種功能強大且複雜的機制,在開發Java工具或框架方面,反射更是不可缺少的一部分。
二、Class 物件詳解
之前說到了,如果要分析一個類,就必須要獲取到該類的位元組碼檔案對應 Class 型別物件。
另外如果有聽到類模板物件
,這個說的其實就是Class物件
,大家不要誤會了。
2.1、如何獲取到Class物件呢?
得到Class的方式總共有四種:
- 通過物件呼叫
getClass()
方法來獲取 - 直接通過
類名.class
的方式得到 - 通過
Class
物件的forName()
靜態方法來獲取 Classloader
,通過類載入器進行獲取
/**
* @description:
* @author: Ning Zaichun
* @date: 2022年09月15日 22:49
*/
public class ClassDemo01 {
@Test
public void test1() throws Exception {
//1、通過物件呼叫 getClass() 方法來獲取
// 型別的物件,而我不知道你具體是什麼類,用這種方法
Student student = new Student();
Class studentClass1 = student.getClass();
System.out.println(studentClass1);
// out: class com.nzc.Student
//2、直接通過`類名.class` 的方式得到
// 任何一個類都有一個隱含的靜態成員變數 class
// Class studentClass2 = Student.class;
Class<?> studentClass2 = Student.class;
System.out.println(studentClass2);
// out: class com.nzc.Student
System.out.println("studentClass1和studentClass2 是否相等==>" + (studentClass1 == studentClass2));
// studentClass1和studentClass2 是否相等==>true
//3、通過 Class 物件的 forName() 靜態方法來獲取,使用的最多
// 但需要丟擲或捕獲 ClassNotFoundException 異常
Class<?> studentClass3 = Class.forName("com.nzc.Student");
System.out.println(studentClass3);
// out: class com.nzc.Student
System.out.println("studentClass1和studentClass3 是否相等==>" + (studentClass1 == studentClass3));
//studentClass1和studentClass3 是否相等==>true
//4、 使用類的載入器:ClassLoader 來獲取Class物件
ClassLoader classLoader = ClassDemo01.class.getClassLoader();
Class studentClass4 = classLoader.loadClass("com.nzc.Student");
System.out.println(studentClass4);
System.out.println("studentClass1和studentClass4 是否相等==>" + (studentClass1 == studentClass4));
//studentClass1和studentClass4 是否相等==>true
}
}
在這四種方式中,最常使用的是第三種方式,第一種都直接new物件啦,完全沒有必要再使用反射了;第二種方式也已經明確了類的名稱,相當於已經固定下來,失去了一種動態選擇載入類的效果;而第三種方式,只要傳入一個字串,這個字串可以是自己傳入的,也可以是寫在配置檔案中的。
像在學 JDBC 連線的時候,大家肯定都使用過 Class
物件的 forName()
靜態方法來獲取Class物件,再載入資料庫連線物件,但可能那時候只是匆匆而過罷了。
注意:不知道大家有沒有觀察,我把各種方式所獲取到的class
物件,都進行了一番比較,並且結果都為true
,這是因為一個類在 JVM 中只會有一個 Class 例項,為了解釋此點,我把類載入過程也簡單的做了一個陳述。
2.2、類的載入過程
當我們需要使用某個類時,如果該類還未被載入到記憶體中,則會經歷下面的過程對類進行初始化。
即類的載入 ---> 連結 ---> 初始化三個階段。
在這裡我只著眼於類的載入過程了,想要了解更為詳細的,就需要大家去找找資料看看啦~
載入過程:
1、在我們進行編譯後(javac.exe命令),會生成一個或多個位元組碼檔案(就是專案中編譯完會出現的 target 目錄下的以.class結尾的檔案)
2、接著當我們使用 java.exe 命令對某個位元組碼檔案進行解釋執行時。
3、載入過程
- 就相當於將 class 檔案位元組碼內容載入到記憶體中,並將這些靜態資料轉換成方法區的執行時資料結構;
- 並生成一個代表這個類的
java.lang.Class
物件,這個載入到記憶體中的類,我們稱為執行時類,此執行時類,就是Class
的一個例項,所有需要訪問和使用類資料只能通過這個Class
物件。 - 所謂
Class
物件,也稱為類模板物件,其實就是 Java 類在 JVM 記憶體中的一個快照,JVM 將從位元組碼檔案中解析出的常量池、 類欄位、類方法等資訊儲存到模板中,這樣 JVM 在執行期便能通過類模板而獲 取 Java 類中的任意資訊,能夠對 Java 類的成員變數進行遍歷,也能進行 Java 方法的呼叫。 - 反射的機制即基於這一基礎。如果 JVM 沒有將 Java 類的宣告資訊儲存起來,則 JVM 在執行期也無法進行反射。
4、這個載入的過程還需要類載入器參與,關於類載入器的型別大家可以去了解了解,還有雙親委派機制等,此處我便不再多言
2.3、為了更便於記憶的圖
(圖片說明:為更好的描述JVM中只有一個Class物件,而畫下此圖,希望通過這張簡圖,讓你記憶更為深刻)
2.4、Class 常用的API
通過 Class 類獲取成員變數、成員方法、介面、超類、構造方法等
getName()
:獲得類的完整名字。getFields()
:獲得類的public
型別的屬性。getDeclaredFields()
:獲得類的所有屬性。包括private
宣告的和繼承類getMethods()
:獲得類的public
型別的方法。getDeclaredMethods()
:獲得類的所有方法。包括private
宣告的和繼承類getMethod(String name, Class[] parameterTypes)
:獲得類的特定方法,name
引數指定方法的名字,parameterTypes
引數指定方法的引數型別。getConstructors()
:獲得類的public
型別的構造方法。getConstructor(Class[] parameterTypes)
:獲得類的特定構造方法,parameterTypes
引數指定構造方法的引數型別。newInstance()
:通過類的不帶引數的構造方法建立這個類的一個物件。
另外就還有反射包下的幾個常用的物件Constructor、Filed、Method
等,分別表示類的構造器、欄位屬性、方法等等
這些都會在下文慢慢陳述出來~
三、獲取執行時類完整資訊並使用
所謂執行時類,就是程式執行時所創建出來的類,你直接理解為通過反射獲取到的類也可。大體意思是如此。
在講述這一小節時,先要理解Java中一切皆物件這句話。
我們平常編寫一個類,我們會將它稱為一個Java物件,但是在反射這裡將此概念再次向上抽象了。
類的基本資訊:構造方法、成員變數,方法,類上的註解,方法註解,成員變數註解等等,這些都是Java物件,也是證明了Java中一切皆物件
這句話。
其中Constructor
就表示構造方法的物件,他包含了構造方法的一切資訊,
Field、Method
等等都是如此。
不要太過於麻煩和重複書寫,我將案例中操作的所有相關程式碼,都放在此處了,案例中的依賴全部基於此。
public interface TestService {
}
@Target({ElementType.TYPE,ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MarkAnnotation {
String value() default "";
}
public class Generic<T> {
}
public interface GenericInterface<T> {
}
// TYPE 表示可以標記在類上
// PARAMETER 表示可以標記在方法形式引數
// METHOD 方法
// FIELD 成員屬性上
@Target({ElementType.TYPE,ElementType.METHOD, ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME) // 這裡表明是執行時註解
@Documented
public @interface LikeAnnotation {
String value() default "";
String[] params() default {};
}
@Data
@MarkAnnotation
public class Person implements TestService{
private String sex;
public Integer age;
private void mySex(String sex){
System.out.println("我的性別"+sex);
}
public void myAge(Integer age){
System.out.println("我的年齡"+age);
}
}
@ToString(callSuper = true) // 增加這行是為了列印時將父類屬性也打印出來,方便檢視~
@Data
@LikeAnnotation(value = "123")
public class Student extends Person implements Serializable {
@LikeAnnotation
private String username;
@LikeAnnotation
public String school;
private Double score;
public Student() {
}
private Student(String username) {
this.username = username;
}
public Student(String username, String school) {
this.username = username;
this.school = school;
}
@LikeAnnotation
public void hello() {
System.out.println("世界,你好");
}
public void say( String username) {
System.out.println("你好,我叫" + username);
}
private void myScore(Double score) {
System.out.println("我的分數是一個私密東西," + score);
}
public void annotationTest(@LikeAnnotation String username,@MarkAnnotation String str){
System.out.println( "測試獲取方法引數中的註解資訊");
}
}
3.1、反射獲取執行時類構造方法並使用
class獲取構造方法的相關API
// 獲取所有的建構函式
Constructor<?>[] getConstructors()
// 獲取 public或 private 修飾的狗贊函式,只要引數匹配即可
Constructor<?>[] getDeclaredConstructors()
// 獲取所有 public 修飾的 建構函式
Constructor<T> getConstructor(Class<?>... parameterTypes)
//呼叫此方法,建立對應的執行時類的物件。
public T newInstance(Object ... initargs)
newInstance()
:呼叫此方法,建立對應的執行時類的物件。內部呼叫了執行時類的空參的構造器。
要想此方法正常的建立執行時類的物件,要求:
- 執行時類必須提供空參的構造器;
- 空參的構造器的訪問許可權得夠。通常,設定為 public。
為什麼要 javabean 中要求提供一個 public 的空參構造器?
原因: 1、便於通過反射,建立執行時類的物件;2、便於子類繼承此執行時類時,預設呼叫 super() 時,保證父類有此構 造器。
想要更詳細的瞭解,建議去看生成的位元組碼檔案,在那裡能夠給出你答案。
測試:
/**
* @description:
* @author: Ning Zaichun
* @date: 2022年09月17日 1:17
*/
public class ConstructorTest {
/**
* 獲取公有、私有的構造方法 並呼叫建立物件
*/
@Test
public void test1() throws Exception {
Class<?> aClass = Class.forName("com.nzc.Student");
System.out.println("======獲取全部public 修飾的構造方法=========");
Constructor<?>[] constructors = aClass.getConstructors();
for (Constructor<?> constructor : constructors) {
System.out.println(constructor);
}
System.out.println("======獲取 public、private 修飾的構造方法,只要引數匹配即可=========");
/**
* 這裡的引數 Class<?>... parameterTypes 填寫的是引數的型別,而並非某個準確的值資訊
*/
Constructor<?> constructor = aClass.getDeclaredConstructor(String.class);
System.out.println(constructor);
// 因為此建構函式是 private 修飾,如果不設定暴力反射,則沒有許可權訪問。
// 這裡setAccessible(true) 是設定暴力反射,如果不設定,則會報錯,
constructor.setAccessible(true);
// 這裡呼叫的有參構造,所以在呼叫 newInstance 方法時,也需要填寫引數
Object o1 = constructor.newInstance("寧在春");
constructor.setAccessible(false);
System.out.println("o1===>"+o1);
/**
* 如果需要獲取有參構造,只需要填寫對應的引數型別即可,
* 獲取無參構造,填null或不填都可。
*/
Constructor<?> constructor1 = aClass.getConstructor();
Constructor<?> constructor2 = aClass.getConstructor(String.class,String.class);
System.out.println("無參構造==>"+constructor1);
System.out.println("有參構造==>"+constructor2);
Object o2 = constructor1.newInstance();
Object o3 = constructor2.newInstance("寧在春2","xxxx社會");
System.out.println("o2===>"+o2);
System.out.println("o3===>"+o3);
}
}
既然能夠獲取到構造方法,那麼也就是可以使用的,用Constructor.newInstance()
方法來呼叫構造方法即可,在下列的列印資訊中,也可以看出來確實如此,如果明確要獲取為Student
物件的話,進行強轉即可。
======獲取全部public 修飾的構造方法=========
public com.nzc.Student(java.lang.String,java.lang.String)
public com.nzc.Student()
======獲取 public、private 修飾的構造方法,只要引數匹配即可=========
private com.nzc.Student(java.lang.String)
o1===>Student(username=寧在春, school=null, age=null)
無參構造==>public com.nzc.Student()
有參構造==>public com.nzc.Student(java.lang.String,java.lang.String)
o2===>Student(username=null, school=null, age=null)
o3===>Student(username=寧在春2, school=xxxx社會, age=null)
3.2、反射獲取執行時類成員變數資訊
class
物件中獲取類成員資訊使用到的API
Field[] getFields();
Field[] getDeclaredFields();
Field getDeclaredField(String name);
public native Class<? super T> getSuperclass();
這裡的 Field
類物件,其實就是表示類物件中的成員屬性,不過還有多了很多其他在反射時需要用到的屬性和API罷了~
3.2.1、獲取私有公有類成員資訊
為了有更好的對比,我先編寫了一段不使用反射時的正常操作。
/**
* 不使用反射的進行操作
*/
@Test
public void test1() {
Student student = new Student();
student.setUsername("username");
student.setSchool("xxxx社會");
student.setScore(100.0);
// 永遠三歲的小夥子 哈哈 🤡
student.setAge(3);
student.setSex("男");
System.out.println("student資訊===>" + student);
// out:student資訊===>Student(super=Person(sex=男, age=3), username=username, school=xxxx社會, score=100.0)
}
現在我再使用反射來實現上述操作~
在實現之前,還是先來看看如何獲取公有、私有的成員變數吧
@Test
public void test2() throws Exception {
Class<?> stuClass = Class.forName("com.nzc.Student");
System.out.println("========獲取所有 public 修飾的成員屬性(包括父類屬性)=====");
Field[] fields = stuClass.getFields();
for (Field field : fields) {
System.out.println(field);
}
//public java.lang.String com.nzc.Student.school
//public java.lang.Integer com.nzc.Person.age
System.out.println("========獲取所有(public、private、protected等修飾的)屬性成員(不包括父類成員屬性)=====");
Field[] fields1 = stuClass.getDeclaredFields();
for (Field field : fields1) {
System.out.println(field);
}
//private java.lang.String com.nzc.Student.username
//public java.lang.String com.nzc.Student.school
//private java.lang.Double com.nzc.Student.score
System.out.println("========通過反射,獲取物件指定的屬性=====");
Field username = stuClass.getDeclaredField("username");
System.out.println("username===>" + username);
}
但是你發現沒有,這無法獲取到父類的成員變數資訊,父類的資訊,我們該如何獲取呢?
3.2.2、獲取父類成員屬性資訊
其實一樣的,我們也是要獲取到父類的Class
物件,在Class API
中有一個getSuperClass()
方法可以獲取到父類的class
物件,其他的操作都是一樣的~
@Test
public void test5() throws ClassNotFoundException {
Class<?> stuClass = Class.forName("com.nzc.Student");
System.out.println("========獲取所有屬性成員(包括父類成員屬性)=====");
Class clazz = stuClass;
List<Field> fieldList = new ArrayList<>();
while (clazz != null) {
fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())));
clazz = clazz.getSuperclass();
}
Field[] fields2 = new Field[fieldList.size()];
fieldList.toArray(fields2);
for (Field field : fields2) {
System.out.println(field);
}
//private java.lang.String com.nzc.Student.username
//public java.lang.String com.nzc.Student.school
//private java.lang.Double com.nzc.Student.score
//private java.lang.String com.nzc.Person.sex
//public java.lang.Integer com.nzc.Person.age
}
到這裡我們已經知道如何獲取到類的成員變數資訊了,就可以來看看如何給它們設定值,或者通過反射的方式來獲取到值了。
3.2.3、設定或修改類成員變數值
@Test
public void test3() throws Exception {
Class<?> stuClass = Class.forName("com.nzc.Student");
System.out.println("獲取到 filed 後,設定值或修改值");
Constructor<?> constructor = stuClass.getConstructor();
Object o = constructor.newInstance();
//操作pubilc 屬性
Field school = stuClass.getDeclaredField("school");
school.set(o,"xxxx社會");
System.out.println(o);
//Student(super=Person(sex=null, age=null), username=null, school=xxxx社會, score=null)
// 另外既然可以設定,那麼自然也是可以獲取的
System.out.println(school.get(o));
//xxxx社會
// 操作 private 修飾的成員屬性
Field name = stuClass.getDeclaredField("username");
// setAccessible(true) 因為是獲取private 修飾的成員變數
// 不設定暴力反射 是無法獲取到的,會直接報 IllegalAccessException 異常
name.setAccessible(true);
name.set(o,"Ningzaichun");
name.setAccessible(false);
System.out.println(o);
//Student(super=Person(sex=null, age=null), username=Ningzaichun, school=xxxx社會, score=null)
}
其實看到這裡對於反射已經有個大概的印象了,並且這都是比較平常且實用的一些方法。
3.2.4、獲取成員變數註解資訊
@Test
public void test4() throws Exception {
Class<?> stuClass = Class.forName("com.nzc.Student");
Field school = stuClass.getDeclaredField("school");
LikeAnnotation annotation = school.getAnnotation(LikeAnnotation.class);
System.out.println(annotation);
//@com.nzc.LikeAnnotation(params=[], value=)
Annotation[] annotations = school.getAnnotations();
for (Annotation annotation1 : annotations) {
System.out.println(annotation1);
}
//@com.nzc.LikeAnnotation(params=[], value=)
LikeAnnotation declaredAnnotation = school.getDeclaredAnnotation(LikeAnnotation.class);
System.out.println(declaredAnnotation);
//@com.nzc.LikeAnnotation(params=[], value=)
Annotation[] declaredAnnotations = school.getDeclaredAnnotations();
for (Annotation declaredAnnotation1 : declaredAnnotations) {
System.out.println(declaredAnnotation1);
}
//@com.nzc.LikeAnnotation(params=[], value=)
}
關於getAnnotation、getDeclaredAnnotation 的分析大家可以看看下面這篇文章@Repeatable詳解-getAnnotation、getDeclaredAnnotation獲取不到物件
對於想要更詳細的瞭解Java註解的朋友,大家可以繼續找找相關資料。
3.3、反射獲取執行時類對物件方法資訊並呼叫
Class物件中使用的相關API
Method[] getMethods();
Method getMethod(String name, Class<?>... parameterTypes);
Method[] getDeclaredMethods();
Method getDeclaredMethod(String name, Class<?>... parameterTypes);
// 獲取方法返回值型別
Class<?> getReturnType();
// obj – 呼叫底層方法的物件
// args – 用於方法呼叫的引數
// return 使用引數args在obj上排程此物件表示的方法的結果
Object invoke(Object obj, Object... args)
3.3.1、獲取物件方法
獲取公有、私有方法
@Test
public void test1() throws Exception {
Class<?> stuClass = Class.forName("com.nzc.Student");
System.out.println("========獲取所有public修飾的方法======");
Method[] methods = stuClass.getMethods();
for (Method method : methods) {
System.out.println(method);
}
//public boolean com.nzc.Student.equals(java.lang.Object)
//public java.lang.String com.nzc.Student.toString()
//public int com.nzc.Student.hashCode()
//public void com.nzc.Student.hello()
//public void com.nzc.Student.say(java.lang.String)
//public java.lang.Double com.nzc.Student.getScore()
//public void com.nzc.Student.setScore(java.lang.Double)
//public void com.nzc.Student.setSchool(java.lang.String) .... 還有一些沒寫出來了
System.out.println("========獲取物件【指定的public修飾的方法】======");
// 第一個引數為方法名,此處是獲取public修飾的無參方法
Method hello = stuClass.getMethod("hello");
System.out.println("hello===>"+hello);
// 帶參方法,第二個引數填寫【引數型別】
Method say = stuClass.getMethod("say", String.class);
System.out.println("say===>"+say);
//hello===>public void com.nzc.Student.hello()
//say===>public void com.nzc.Student.say(java.lang.String)
// 引數為 方法名,此處是獲取 private 修飾的無參方法
// stuClass.getDeclaredMethod("");
Method myScore = stuClass.getDeclaredMethod("myScore", Double.class);
System.out.println("myScore==>"+myScore);
//myScore==>private void com.nzc.Student.myScore(java.lang.Double)
System.out.println("=======獲取所有的方法=======");
Method[] declaredMethods = stuClass.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
System.out.println(declaredMethod);
}
//public void com.nzc.Student.say(java.lang.String)
//private void com.nzc.Student.myScore(java.lang.Double)
//public java.lang.Double com.nzc.Student.getScore()
//public void com.nzc.Student.setScore(java.lang.Double)
//protected boolean com.nzc.Student.canEqual(java.lang.Object)
}
既然能獲取到,那麼也是能夠呼叫的啦~,
3.3.2、呼叫物件方法
還記得經常能看到的一個invoke()
方法嗎,這裡就是呼叫method.invoke()
方法來實現方法的呼叫。
@Test
public void test2() throws Exception {
Class<?> stuClass = Class.forName("com.nzc.Student");
Constructor<?> constructor = stuClass.getConstructor();
Object o = constructor.newInstance();
Method myScore = stuClass.getDeclaredMethod("myScore", Double.class);
myScore.setAccessible(true);
// 呼叫方法
myScore.invoke(o, 99.0);
// 相當於 Student student= new Student();
// student.myScore(99.0);
myScore.setAccessible(false);
}
之前闡述了 Method
它本身就記錄了方法的一切資訊,我們實現呼叫也就是第一步罷了,說它可以獲取到當時定義方法的一切都不為過,接下來一步一步來說吧。
3.3.3、獲取方法上的註解資訊
@Test
public void test2() throws Exception {
Class<?> stuClass = Class.forName("com.nzc.Student");
System.out.println("==== 獲取成員變數上指定的註解資訊===");
Field username = stuClass.getDeclaredField("username");
System.out.println(username);
//private java.lang.String com.nzc.Student.username
Annotation annotation = username.getAnnotation(LikeAnnotation.class);
System.out.println(annotation);
//@com.nzc.LikeAnnotation(params=[], value=)
Method hello = stuClass.getDeclaredMethod("hello");
LikeAnnotation annotation1 = hello.getAnnotation(LikeAnnotation.class);
System.out.println(hello+"===="+annotation1);
// public void com.nzc.Student.hello()[email protected](params=[], value=)
}
3.3.4、獲取方法引數及引數註解資訊
不過在寫專案時,有可能還會要獲取方法引數上的註解,那該如何獲取方法引數呢?又該如何獲取方法引數的註解資訊呢?
@Test
public void test3() throws Exception {
Class<?> stuClass = Class.forName("com.nzc.Student");
System.out.println("==== 獲取方法引數中的註解資訊===");
Method annotationTest = stuClass.getDeclaredMethod("annotationTest",String.class,String.class);
// 獲取方法的返回值型別
Class<?> returnType = annotationTest.getReturnType();
// 獲取許可權修飾符
System.out.println(Modifier.toString(annotationTest.getModifiers()) );
// 獲取到全部的方法引數
Parameter[] parameters = annotationTest.getParameters();
for (Parameter parameter : parameters) {
Annotation annotation = parameter.getAnnotation(LikeAnnotation.class);
if(annotation!=null){
// 引數型別
Class<?> type = parameter.getType();
// 引數名稱
String name = parameter.getName();
System.out.println("引數型別"+type+" 引數名稱==>"+name+" 引數上的註解資訊"+annotation);
//引數型別class java.lang.String 引數名稱==>arg0 引數上的註解資訊@com.nzc.LikeAnnotation(params=[], value=)
}
}
// 獲取引數上全部的註解資訊
Annotation[][] parameterAnnotations = annotationTest.getParameterAnnotations();
for (int i = 0; i < parameterAnnotations.length; i++) {
for (int i1 = 0; i1 < parameterAnnotations[i].length; i1++) {
System.out.println(parameterAnnotations[i][i1]);
}
}
// @com.nzc.LikeAnnotation(params=[], value=)
//@com.nzc.MarkAnnotation(value=)
int parameterCount = annotationTest.getParameterCount();
System.out.println("獲取引數個數==>"+parameterCount);
}
3.3.5、獲取方法返回引數和方法許可權修飾符
@Test
public void test33() throws Exception {
Class<?> stuClass = Class.forName("com.nzc.Student");
Method annotationTest = stuClass.getDeclaredMethod("annotationTest",String.class,String.class);
// 獲取方法的返回值型別
Class<?> returnType = annotationTest.getReturnType();
System.out.println(returnType);
//void
// 獲取許可權修飾符
System.out.println(Modifier.toString(annotationTest.getModifiers()) );
//public
}
3.4、反射獲取執行時類資訊、介面資訊、包資訊
3.4.1、獲取執行時類的介面資訊
/**
* 獲取執行時類實現的介面
*/
@Test
public void test5() {
Class clazz = Student.class;
Class[] interfaces = clazz.getInterfaces();
for (Class c : interfaces) {
System.out.println(c);
}
System.out.println("====================");
// 獲取執行時類的父類實現的介面
Class[] interfaces1 = clazz.getSuperclass().getInterfaces();
for (Class c : interfaces1) {
System.out.println(c);
}
}
3.4.2、獲取類所在包的資訊
/**
* 獲取執行時類所在的包
*/
@Test
public void test6() {
Class clazz = Person.class;
Package pack = clazz.getPackage();
System.out.println(pack);
// out:package com.nzc
}
3.4.3、獲取類上註解資訊
@Test
public void test1() throws Exception {
Class<?> stuClass = Class.forName("com.nzc.Student");
System.out.println("==== 獲取類上指定的註解資訊===");
LikeAnnotation annotation = stuClass.getAnnotation(LikeAnnotation.class);
System.out.println(annotation);
//@com.nzc.LikeAnnotation(params=[], value=123)
System.out.println("獲取註解上的值資訊==>"+annotation.value());
//獲取註解上的值資訊==>123
//annotation.params(); 註解有幾個屬性,就可以獲取幾個屬性
System.out.println("==== 獲取類上全部註解資訊===");
Annotation[] annotations = stuClass.getAnnotations();
for (Annotation annotation1 : annotations) {
System.out.println(annotation1);
//@com.nzc.LikeAnnotation(params=[], value=123)
}
LikeAnnotation declaredAnnotation = stuClass.getDeclaredAnnotation(LikeAnnotation.class);
System.out.println("declaredAnnotation==>"+declaredAnnotation);
//declaredAnnotation==>@com.nzc.LikeAnnotation(params=[], value=123)
Annotation[] declaredAnnotations = stuClass.getDeclaredAnnotations();
}
注意
:lombok 相關的註解是無法獲取到的,因為 lombok 註解為編譯時註解,並非是執行時註解,在編譯完成後,lombok 註解並不會保留於class
檔案中,因此是無法通過反射獲取到的。
@Data
也標明瞭它的存在級別為原始碼級別,而執行時存在註解@Retention
為@Retention(RetentionPolicy.RUNTIME)
。
3.5、反射獲取執行時類的父類的泛型資訊、介面泛型資訊
@Test
public void test8(){
Class clazz = Person.class;
// 獲取泛型父類資訊
Type genericSuperclass = clazz.getGenericSuperclass();
System.out.println(genericSuperclass);
// com.nzc.Generic<java.lang.String>
// 獲取泛型介面資訊
Type[] genericInterfaces = clazz.getGenericInterfaces();
for (Type genericInterface : genericInterfaces) {
System.out.println(genericInterface);
//interface com.nzc.TestService
//com.nzc.GenericInterface<java.lang.String>
}
}
四、反射應用場景及實戰案例
5.1、那到底什麼時候會使用反射
一句話說它的應用場景就是:確定不下來到底使用哪個類的時候
,比如你要開發一個通用工具類,為了達到通用性,傳入的引數物件,一般都是無法限制的,這個時候就是要用到反射啦~。
反射的特徵:動態性
5.2、AOP + 自定義註解 修改前端傳遞的引數資訊
需求如下:我現在引入了一個第三方 jar 包,裡面有一個 MyBatis-Plus 查詢構造器,其中構造 LIKE
條件查詢的條件是當前端傳過來的引數帶有逗號時,拼接為LIKE
查詢條件。
關鍵程式碼:
標識在物件的某個成員屬性上
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LikeAnnotation {
String value() default "";
}
標識在Controller層上,以此來判斷那些請求是需要被切入的。
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MarkAnnotation {
String value() default "";
}
一個非常簡單的Javabean
@Data
public class Login {
@LikeAnnotation(value = "username")
private String username;
private String password;
public Login() {
}
public Login(String username, String password) {
this.username = username;
this.password = password;
}
}
Controller 類
@Slf4j
@RestController
public class HelloController {
@MarkAnnotation
@GetMapping("/search")
public Login getLikeLogin(Login login){
System.out.println(login);
return login;
}
}
重點重點,切面類 LikeAnnotationAspect
,處理邏輯全部在此處
@Aspect
@Component
@Slf4j
public class LikeAnnotationAspect {
// 標記了 @MarkAnnotation 註解的才切入 降低效能消耗
@Pointcut("@annotation(com.nzc.annotation.MarkAnnotation)")
public void pointCut() {
}
// 獲取當前類及父類所有的成員屬性
private static Field[] getAllFields(Object object) {
Class<?> clazz = object.getClass();
List<Field> fieldList = new ArrayList<>();
while (clazz != null) {
fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())));
clazz = clazz.getSuperclass();
}
Field[] fields = new Field[fieldList.size()];
fieldList.toArray(fields);
return fields;
}
@Around("pointCut()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object[] args = pjp.getArgs();
// 獲取到第一個引數
Object parameter = args[0];
log.info("parameter==>{}", parameter);
if (parameter == null) {
return pjp.proceed();
}
Field[] fields = getAllFields(parameter);
for (Field field : fields) {
log.debug("------field.name------" + field.getName());
if (field.getAnnotation(LikeAnnotation.class) != null) {
try {
field.setAccessible(true);
Object username = field.get(parameter);
field.setAccessible(false);
if (username != null && !username.equals("")) {
field.setAccessible(true);
field.set(parameter, "," + username + ",");
field.setAccessible(false);
}
} catch (Exception e) {
}
}
}
// 呼叫方法
Object result = pjp.proceed();
long end = System.currentTimeMillis();
log.debug("修改耗時==>" + (end - start) + "ms");
return result;
}
}
實現效果:
5.3、Mybatis 攔截器實現自動填充建立人、修改人資訊
看似好像寫業務的我們,沒有怎麼接觸Java反射,但實際上可能處處都隱含著反射。
使用過 Mybatis-Plus 的朋友,應該知道,可以設定自動填充資料(建立時間、更新時間、建立人、更新人等),不過那個是實現MetaObjectHandler
介面進行處理的。
但是今天的話,我用Mybatis 原生的攔截器來進行一番實現,實現每次更新、新增時自動填充建立人、更新人等,表裡沒時間欄位,就沒演示時間了,但實現原理都一致。
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.binding.MapperMethod.ParamMap;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.*;
/**
* mybatis攔截器,自動注入建立人、建立時間、修改人、修改時間
* @author Ning Zaichun
*/
@Slf4j
@Component
@Intercepts({ @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }) })
public class MybatisInterceptor implements Interceptor {
public static Field[] getAllFields(Object object) {
Class<?> clazz = object.getClass();
List<Field> fieldList = new ArrayList<>();
while (clazz != null) {
fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())));
clazz = clazz.getSuperclass();
}
Field[] fields = new Field[fieldList.size()];
fieldList.toArray(fields);
return fields;
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
String sqlId = mappedStatement.getId();
log.info("------sqlId------" + sqlId);
//2022-09-17 17:16:50.714 INFO 14592 --- [nio-8080-exec-1] com.nzc.tree.commons.MybatisInterceptor : ------sqlId------com.nzc.tree.mapper.CategoryMapper.updateById
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
Object parameter = invocation.getArgs()[1];
log.info("------sqlCommandType------" + sqlCommandType);
//2022-09-17 17:16:50.714 INFO 14592 --- [nio-8080-exec-1] com.nzc.tree.commons.MybatisInterceptor : ------sqlCommandType------UPDATE
if (parameter == null) {
return invocation.proceed();
}
if (SqlCommandType.INSERT == sqlCommandType) {
Field[] fields = getAllFields(parameter);
for (Field field : fields) {
log.info("------field.name------" + field.getName());
try {
if ("createBy".equals(field.getName())) {
field.setAccessible(true);
Object local_createBy = field.get(parameter);
field.setAccessible(false);
if (local_createBy == null || local_createBy.equals("")) {
field.setAccessible(true);
field.set(parameter, "nzc-create");
field.setAccessible(false);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
if (SqlCommandType.UPDATE == sqlCommandType) {
Field[] fields = null;
if (parameter instanceof ParamMap) {
ParamMap<?> p = (ParamMap<?>) parameter;
if (p.containsKey("et")) {
parameter = p.get("et");
} else {
parameter = p.get("param1");
}
if (parameter == null) {
return invocation.proceed();
}
fields = getAllFields(parameter);
} else {
fields = getAllFields(parameter);
}
for (Field field : fields) {
log.info("------field.name------" + field.getName());
try {
if ("updateBy".equals(field.getName())) {
field.setAccessible(true);
field.set(parameter,"nzc-update");
field.setAccessible(false);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// TODO Auto-generated method stub
}
}
裡面牽扯到的一些 Mybatis 中的一些物件,我沒細說了,大家打印出來的時候都可以看到的。
測試:
執行的SQL
語句列印資訊
結果:
反射的特性,看似和我們天天寫業務沒啥關係,但是它其實一直伴隨著我們,這也是 Java 開發者的基礎知識,基礎不牢,地動山搖~
五、反射的優缺點
優點: 反射提高了程式的靈活性和擴充套件性,降低耦合性,提高自適應能力。 它允許程式建立和控制任何類的物件,無需提前硬編碼目標類;對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意一個方法;
缺點 :讓我們在執行時有了分析操作類的能力,這同樣也增加了安全問題。比如可以無視泛型引數的安全檢查(泛型引數的安全檢查發生在編譯時)。另外,反射的效能也要稍差點,不過,對於框架來說實際是影響不大的。
以下文字來自於 反射是否真的會讓你的程式效能降低嗎?
1.反射大概比直接呼叫慢50~100倍,但是需要你在執行100萬遍的時候才會有所感覺
2.判斷一個函式的效能,你需要把這個函式執行100萬遍甚至1000萬遍
3.如果你只是偶爾呼叫一下反射,請忘記反射帶來的效能影響
4.如果你需要大量呼叫反射,請考慮快取。
5.你的程式設計的思想才是限制你程式效能的最主要的因素
小結
仔細閱讀下來你會發現,
正如文中所說,所謂Class
物件,也稱為類模板物件,其實就是 Java 類在 JVM 記憶體中的一個快照,JVM 將從位元組碼檔案中解析出的常量池、 類欄位、類方法等資訊儲存到模板中,這樣 JVM 在執行期便能通過類模板而獲取 Java 類中的任意資訊,能夠對 Java 類的成員變數進行遍歷,也能進行 Java 方法的呼叫,獲取到類的任意資訊。
反射也就是這樣啦,不知道你會使用啦嗎,如果你還沒有的話,我覺得可以再讀上一遍,順帶自己驗證一遍,希望你能有所收穫。
後記
其實想寫這篇文章時間已經不短了,但懶
偶爾發作(我又經常偶爾),所以總是一拖再拖,終於把它完成了,我也不知道你會不會讀到此處,看到我發的牢騷。
只是非常簡單的希望每一位閱讀者能夠有所收穫,這應該是我持續寫文的快樂吧~
今天又是好值的一天啊~
各位下次見!
明人不說暗話,我還是想要一鍵三連的,哈哈,走過路過還是可以點點讚的啦,要是喜歡,也可以給個關注啦,有收穫可以再點點收藏的😊
掘金髮佈於 2022年9月28日,作者:寧在春
- Spring Cache 整合 Redis 做快取使用~ 快速上手~
- Idea 藉助科學上網(VPN)工具使用 Translation 外掛 (實測)
- Java反射詳解,學以致用,實戰案例(AOP修改引數、Mybatis攔截器實現自動填充)
- Jenkins Github Nginx 自動化部署 Vue 專案
- 三級分類的資料表設計和構造API資料
- Docker 安裝 Nginx 部署前端專案
- 關於目前流行的 Redis 視覺化管理工具的詳細評測
- Java註解詳解和自定義註解實戰,用程式碼講解
- 2022年大專畢業生,屬於我的心路歷程 | 2022 年中總結
- Java設計模式-橋接模式 理論程式碼相結合
- 「後端小夥伴來學前端了」Vue中 Slot 插槽的使用,同樣也可以實現父子元件之間通訊
- 「後端小夥伴來學前端了」為什麼Vue在有了全域性事件匯流排後還要引入Vuex呢?
- 「後端小夥伴來學前端了」Vue中利用全域性事件匯流排實現元件之間通訊
- Mysql 邏輯架構介紹
- Dockerfile中的保留字指令講解
- 針對 SpringSecurity 鑑權流程做了一個詳細分析,讓你明白它是如何執行的!
- UML圖 | 時序圖(順序、序列圖)繪製
- 通過簡單例子 | 快速理清 UML 中類與類的六大關係
- SpringBoot 整合 Thymeleaf & 如何使用後臺模板快速搭建專案
- Netty | 工作流程圖分析 & 核心元件說明 & 程式碼案例實踐