Java反射和註解

  • 2019 年 10 月 3 日
  • 筆記

Reflection

今天來挑戰一下如何在2000字以內把Reflection作用說明白

Reflection is commonly used by programs which require the ability to examine or modify the runtime behavior of applications running in the Java virtual machine. This is a relatively advanced feature and should be used only by developers who have a strong grasp of the fundamentals of the language. With that caveat in mind, reflection is a powerful technique and can enable applications to perform operations which would otherwise be impossible.

https://docs.oracle.com/javase/tutorial/reflect/index.html

Java Reflection反射機制:Java可以獲取/調用任意已載入類的所有資訊(欄位/方法/構造函數)。甚至改變類中成員的各種屬性(又如private改成public)。官方API說明都在java.lang.reflect

假設目前我們只有奧迪車需要測試運行速度。

package com.car.test;  // Car.java  class Car {int velocity;}  // Runnable.java  interface Runnable {public void run();}  // Audi.java  public class Audi extends Car implements Runnable {      Audi(int velocity){this.velocity = velocity;}      public void run() {        String className = this.getClass().getSimpleName();        System.out.println(className + " run " + velocity + "km/h");      }  }  // CarFactory.java  public class CarFactory {      public static void main(String[] args) {        Audi audi = new Audi(120);        audi.run();      }  }

上面因為我們知道需要測試的只有Audi這一種車型,所以在main裡面可以直接用調用對應的構造函數進行測試,但是當我們的車型增加時(特斯拉也來啦),我們就不得不再次修改main函數。

public class CarFactory {    public static void main(String[] args) {      Tesla tesla = new tesla(150);      tesla.run();    }  }

為了更好的測試不斷新加車型,同時不修改我們的工廠測試主函數。我們可以:

package com.car.test;  // Tesla.java  // 省略以前已有不變的Audi  public class Tesla extends Car implements Runnable {    Tesla(int velocity){this.velocity = velocity;}    public void run() {      String name = this.getClass().getSimpleName();      System.out.println(name + " run " + velocity + "km/h");    }  }
package com.car.test;  // CarFactory.java  import java.lang.reflect.InvocationTargetException;  import java.lang.reflect.Method;    public class CarFactory {    public static void main(String[] args) {      try {        //通過命令行把動態所需要測試的類名傳入測試主函數。        //args[0] = "com.car.test.Tesla".        //args[1] = "140".        //得到類名.此函數需catch異常 ClassNotFoundException.        Class<?> c = Class.forName(args[0]);        //找到對應的構造函數並構造出實例        int velocity = Integer.parseInt(args[1]);        Object car = c.getDeclaredConstructor(int.class).newInstance(velocity);        //找到需要測試的函數定義        Method method = c.getDeclaredMethod("run");        //執行對應函數        method.invoke(car);      } catch (ClassNotFoundException e) {         e.printStackTrace();      } catch (NoSuchMethodException e) {         e.printStackTrace();      } catch (IllegalAccessException e) {         e.printStackTrace();      } catch (InstantiationException e) {         e.printStackTrace();      } catch (InvocationTargetException e) {         e.printStackTrace();      }   }  }

這樣只要我們在命令行傳入對應的類名,就可以執行對應的測試函數啦。

完美做到新加車型,不需要修改主測試函數。這就是java反射機制在運行時的一個基本示例。但是對於一個靜態語言來說,這種動態調用太過靈活,所以需要每一步都要小心(上面的每個函數都需要catch異常)

With great power comes great responsibility.

上面就是事先不知道我們需要測試的是什麼類,只有到了運行時才能得到對應的類,這就是應用反射機制的常用場景。它在運用在真實場景中一個典型例子就是Junit,它過去枚舉了類中所有的方法getDeclaredMethods(),並把以testXXX開頭的方法假設為測試函數並執行它們。但在JUit4後使用了註解(annotations)來替換了它。

不過註解的本質也是通過反射來實現的。

Annotations

在文章的開始處寫反射是對類的屬性/方法/構造函數的操作,並沒有提到註解。但是通過Reflection API列表我們可以看到他有getAnnotation之類的函數,所以註解也是可以讀寫操作的。不過想對於上面說的,稍微複雜一點。

解析註解的方式有兩種,編譯期檢查和運行期反射

1.編譯期檢查

常見到的就是@Override,編譯器就會檢查當前方法的方法簽名是否真正重寫了父類的某個方法,也就是比較父類中是否具有一個同樣的方法簽名。比如我們在上面的Car類中增加一個方法得到名字:

package com.car.test;  //car.java  class Car {    int velocity;    public String getName() {return "Car";}  }  // Tesla.java  public class Tesla extends Car implements Runnable {    Tesla(int velocity){this.velocity = velocity;}    // @Override    public get_name() { return "Tesla";}    // 省略以前有的   }  }

Tesla繼承了Car,並想重寫它的getName函數。但不小心手誤寫成了get_name,這時Tesla就同時有了這兩個函數。為了避免這種低級錯誤,就使用@Overide,這告訴編譯器,此方法是重寫父類方法的,如果方法的定義(名字/返回值/參數)與父類不一致,則編譯不通過。PS: 打開上面的注釋,你就會得到一個編譯報錯。

可見@Override作用於方法,只在編譯期解析,編譯結束後,使命就完成了。不會把資訊存到位元組碼中。

其它內置的註解還有

  • @Deprecated標記當前類/方法/欄位不再被推薦使用,下次版本可能會不在支援它。
  • @SuppressWarnings明確告訴編譯器這個警告我已發現了,你不用再來煩我。

2.元註解

為了在註解定義時規定生命周期(編譯期/永久保存etc),作用範疇(欄位/方法etc),又引入了註解的註解,也就是元註解,它是主要用於修飾註解的註解。比如在@override的定義中

@Target(ElementType.METHOD)  @Retention(RetentionPolicy.SOURCE)  public @interface Override {  }
  • @Target表示作用目標,METHOD作用於方法,還有其它的FIELD,PARAMETER之類的。
  • @Retention表示生命周期SOURCE編譯器可見,不寫入class文件;CLASS類載入時丟棄,會寫入class文件,RUNTIME永久保存,可以通過反射讀取。
  • @Documented是否在JavaDoc文檔中出現。
  • Inherited是否允許子類繼承該註解。

3.運行期註解

下面我們稍微改造一個上面car的例子來說明一下運行期的註解操作。

通過新建一個註解(@DriveAccess來表示控制可以允許運行run函數進行函數(當然,你可以有更好的方法來做這件事,這裡只是為了用來演示註解如何工作)。

package com.car.test;  // DriveAccess.java  import java.lang.annotation.Documented;  import java.lang.annotation.ElementType;  import java.lang.annotation.Retention;  import java.lang.annotation.RetentionPolicy;  import java.lang.annotation.Target;    @Retention(RetentionPolicy.RUNTIME) // 永久保存  @Target(ElementType.METHOD) //作用於方法  public @interface DriveAccess {    public boolean canDrive() default false; //默認返回false  }  // Tesla.java  public class Tesla extends Car implements Runnable {    Tesla(int velocity) { this.velocity = velocity; }      @Override    public String getName() { return "Tesla"; }      @DriveAccess(canDrive = true)    public void run() {      String name = this.getClass().getSimpleName();      System.out.println(name + " run " + velocity + "km/h");      }  }  // CarFactory.java  import java.lang.reflect.Method;    public class CarFactory {    public static void main(String[] args) throws Exception {      Class<?> car = Class.forName(args[0]);      for (Method method : car.getDeclaredMethods()) {        if (method.isAnnotationPresent(DriveAccess.class)) {          DriveAccess access = method.getAnnotation(DriveAccess.class);          String methodName = method.toGenericString();          if (access.canDrive()) {            System.out.println(methodName + " method can be accessed... ");            Object c = car.getDeclaredConstructor(int.class).newInstance(100);            method.invoke(c);          } else {            System.out.println(methodName + " method can not be accessed... ");          }        }else {         System.out.println(methodName + " don't have DriveAccess Annotation...");       }     }  }  }  }

運行 java CarFactory com.car.test.Tesla得到

public void com.car.test.Tesla.run() method can be accessed...  Tesla run 100km/h  public java.lang.String com.car.test.Tesla.getName() don't have DriveAccess Annotation...

如果Tesla中的canDirve改成false則:

public void com.car.test.Tesla.run() method can not be accessed...  public java.lang.String com.car.test.Tesla.getName() don't have DriveAccess Annotation...

Summary

運用Java Reflection API可以讀取/操作類中所有的元素。非常靈活強大,因為靈活,也會帶來很多不確定的危險。所以如果可以用其它方法實現的,最好不要用反射。