Java反射詳解,學以致用,實戰案例(AOP修改參數、Mybatis攔截器實現自動填充)

語言: CN / TW / HK

highlight: atom-one-dark theme: qklhk-chocolate


持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第1天,點擊查看活動詳情

作為Java開發者,你認為反射這個知識點重要程度,在你心裏是什麼樣的呢?

以前我也只覺得反射非常重要,但總歸是聽這個文章説,聽那個朋友説,學是學了,但卻沒怎麼應用。

當我正式進入到社會當 cv 仔的時候,需要考慮的問題多了,慢慢思考問題了,就覺得反射是個力大無窮的東西,更會感覺反射是個無所不能的東西,如各種各樣的框架的底層,各種各樣的攔截器的實現,反射都是其中少不了的一部分~

如果平時着重於開發業務的話,那麼確實可能會較少使用到反射機制,但並非是説反射它不重要,反射它是Java 框架的基礎勒,可以説木有反射,Java的動態性是會受限的~

文章大致思路:

image.png

全文共 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、類的加載過程

當我們需要使用某個類時,如果該類還未被加載到內存中,則會經歷下面的過程對類進行初始化。

image.png 即類的加載 ---> 鏈接 ---> 初始化三個階段。

在這裏我只着眼於類的加載過程了,想要了解更為詳細的,就需要大家去找找資料看看啦~

加載過程

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、為了更便於記憶的圖

image.png (圖片説明:為更好的描述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文件中,因此是無法通過反射獲取到的。

image.png

@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;     }  ​  }

實現效果:

image.png

image.png

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 中的一些對象,我沒細説了,大家打印出來的時候都可以看到的。

測試:

image.png

執行的SQL語句打印信息

image.png

結果:

image.png

反射的特性,看似和我們天天寫業務沒啥關係,但是它其實一直伴隨着我們,這也是 Java 開發者的基礎知識,基礎不牢,地動山搖~

五、反射的優缺點

優點: 反射提高了程序的靈活性和擴展性,降低耦合性,提高自適應能力。 它允許程序創建和控制任何類的對象,無需提前硬編碼目標類;對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個對象,都能夠調用它的任意一個方法

缺點 :讓我們在運行時有了分析操作類的能力,這同樣也增加了安全問題。比如可以無視泛型參數的安全檢查(泛型參數的安全檢查發生在編譯時)。另外,反射的性能也要稍差點,不過,對於框架來説實際是影響不大的。

以下文字來自於 反射是否真的會讓你的程序性能降低嗎?

1.反射大概比直接調用慢50~100倍,但是需要你在執行100萬遍的時候才會有所感覺

2.判斷一個函數的性能,你需要把這個函數執行100萬遍甚至1000萬遍

3.如果你只是偶爾調用一下反射,請忘記反射帶來的性能影響

4.如果你需要大量調用反射,請考慮緩存。

5.你的編程的思想才是限制你程序性能的最主要的因素

小結

仔細閲讀下來你會發現,

正如文中所説,所謂Class對象,也稱為類模板對象,其實就是 Java 類在 JVM 內存中的一個快照,JVM 將從字節碼文件中解析出的常量池、 類字段、類方法等信息存儲到模板中,這樣 JVM 在運行期便能通過類模板而獲取 Java 類中的任意信息,能夠對 Java 類的成員變量進行遍歷,也能進行 Java 方法的調用,獲取到類的任意信息。

反射也就是這樣啦,不知道你會使用啦嗎,如果你還沒有的話,我覺得可以再讀上一遍,順帶自己驗證一遍,希望你能有所收穫。

後記

其實想寫這篇文章時間已經不短了,但偶爾發作(我又經常偶爾),所以總是一拖再拖,終於把它完成了,我也不知道你會不會讀到此處,看到我發的牢騷。

只是非常簡單的希望每一位閲讀者能夠有所收穫,這應該是我持續寫文的快樂吧~

今天又是好值的一天啊~

各位下次見!

明人不説暗話,我還是想要一鍵三連的,哈哈,走過路過還是可以點點讚的啦,要是喜歡,也可以給個關注啦,有收穫可以再點點收藏的😊

掘金髮佈於 2022年9月28日,作者:寧在春