反射機制及應用

黑米麵包派發表於2021-04-09

反射機制及應用

什麼是反射

在程式執行狀態中,對於任意一個類或物件,都能夠獲取到這個類的所有屬性和方法(包括私有屬性和方法),這種動態獲取資訊以及動態呼叫物件方法的功能就稱為反射機制。簡單來講,通過反射,類對我們是完全透明的,想要獲取任何東西都可以。

Class物件

我們建立的每一個類也都是物件,即類本身是java.lang.Class類的例項物件。這個例項物件稱之為類物件,也就是Class物件。

  • Class類的例項物件表示正在執行的Java應用程式中的類和介面,也就是JVM中的每一個例項,每一個類都有唯一的Class物件
  • Class類沒有公共的構造方法,Class物件是在載入類時由Java虛擬機器自動構造的
  • Class 物件用於提供類本身的資訊,比如有幾種構造方法, 有多少屬性,有哪些普通方法

獲取Class物件的三種方式

  1. 可以在原始碼階段進行獲取。Class.forName(“全類名”)。將位元組碼檔案載入進記憶體,返回Class物件,多用於配置檔案,將類名定義的配置檔案中。
  2. Class類物件階段獲取:類名.class。此方式多用於引數的傳遞
  3. Runtime階段獲取:new 類名().getClass()。該方法是定義在Object類中的(所有類均繼承自Object)方法,因此所有的類都會繼承該方法。多用於物件獲取位元組碼的方式。

獲取Class物件案例

public class GetClass {

    @Test
    public void classForName() throws ClassNotFoundException {
        // 方式一 Class.forName("全類名")
        final Class<?> clazz1 = Class.forName("cn.wujiwen.basic.reflect.ExampleClass");
        System.out.println("class1:" + clazz1);
    }

    @Test
    public void nameClass(){
        // 方式二 類名.class
        final Class<ExampleClass> clazz2 = ExampleClass.class;
        System.out.println("class2:" + clazz2);
    }

    @Test
    public void ObjectGetClass(){
        // 方式三 例項物件.getClass()
        ExampleClass exampleClass = new ExampleClass();
        Class<?> clazz3 = exampleClass.getClass();
        System.out.println("class3:" + clazz3);
    }

    // 同一個節碼檔案(*.class)在一次程式執行過程中,只會被載入一次,無論通過哪一種方式獲取的Class物件都是同一個。
    @Test
    public void all() throws ClassNotFoundException {
        Assertions.assertSame(Class.forName("cn.wujiwen.basic.reflect.ExampleClass") , ExampleClass.class);
    }
}

Class物件的功能

這裡將簡單記錄三種常用的功能,更多方法可完整閱讀class原始碼

1、獲取成員變數

//獲取所有public修飾的成員變數
Field[] getFields() 
//獲取指定名稱的public修飾的成員變數         
Field getField(String name)  
//獲取所有的成員變數,不考慮修飾符
Field[] getDeclaredFields() 
//獲取指定的成員變數,不考慮修飾符 
Field getDeclaredField(String name)  

2、獲取構造方法

//獲取所有public修飾的建構函式
Constructor<?>[] getConstructors()
//獲取指定的public修飾的建構函式
Constructor<T> getConstructor(類<?>... parameterTypes)
//獲取所有的建構函式,不考慮修飾符
Constructor<?>[] getDeclaredConstructors() 
//獲取指定的建構函式,不考慮修飾符 
Constructor<T> getDeclaredConstructor(類<?>... parameterTypes)  //獲取指定的建構函式,不考慮修飾符

3、獲取成員方法

//獲取所有public修飾的成員方法
Method[] getMethods()   
//獲取指定名稱的public修飾的成員方法        
Method getMethod(String name, 類<?>... parameterTypes) 
//獲取所有的成員方法,不考慮修飾符
Method[] getDeclaredMethods()  
//獲取指定名稱的成員方法,不考慮修飾符
Method getDeclaredMethod(String name, 類<?>... parameterTypes) 

下面我們一個一個類測試一下,先準備以上功能方法所需要用到的類、屬性資訊

public class ExampleClass {
    // public修飾的屬性
    public String publicField;
    // private 修飾的屬性
    private String privateField;
    // 預設
    String defaultField;
    // protected 修飾的戶型
    protected String protectedField;

    // protected 無引數構造,與public無引數構造衝突,會發生覆蓋操作
    protected ExampleClass(){

    }
    // public 一個引數的建構函式
    public ExampleClass(String privateField){
        this.privateField = privateField;
    }
    
    public void publicMethod(){

    }

    private void privateMethod(){

    }

    protected void protectedMethod(){

    }

    void defaultMethod(){

    }
		
		public void noParamsMethod(){
        System.out.println("this is noParamsMethod");
    }
    
    public void paramsMethod(String params){
        System.out.println("this is paramsMethod,params is " + params);
    }
}

下面我們將測試一下每一個功能的使用和答應資訊

獲取成員變數

@Test
public void getField() throws NoSuchFieldException {
    Class<ExampleClass> exampleClassClass = ExampleClass.class;
    
		Field[] fields = exampleClassClass.getFields();
    System.out.println(Arrays.asList(fields));
		// 返回了僅有的一個public屬性資訊
		// [public java.lang.String cn.wujiwen.basic.reflect.ExampleClass.publicField]

    Field publicField = exampleClassClass.getField("publicField");
    System.out.println(publicField);
		// 能夠獲取到,如果設定成其他的屬性引數,將會報錯
		// public java.lang.String cn.wujiwen.basic.reflect.ExampleClass.publicField

    Field[] declaredFields = exampleClassClass.getDeclaredFields();
    System.out.println(Arrays.asList(declaredFields));
		// 返回了所有的屬性資訊,不考慮修飾符資訊
		// [public java.lang.String cn.wujiwen.basic.reflect.ExampleClass.publicField, private java.lang.String cn.wujiwen.basic.reflect.ExampleClass.privateField, java.lang.String cn.wujiwen.basic.reflect.ExampleClass.defaultField, protected java.lang.String cn.wujiwen.basic.reflect.ExampleClass.protectedField]

    Field privateField = exampleClassClass.getDeclaredField("privateField");
    System.out.println(Arrays.asList(privateField));
		// private java.lang.String cn.wujiwen.basic.reflect.ExampleClass.privateField
}

獲取建構函式

@Test
public void getConstructor() throws NoSuchMethodException {
    Class<ExampleClass> exampleClassClass = ExampleClass.class;
    
    Constructor<?>[] constructors = exampleClassClass.getConstructors();
    System.out.println(Arrays.toString(constructors));
    // [public cn.wujiwen.basic.reflect.ExampleClass(java.lang.String)]

    Constructor<ExampleClass> constructor = exampleClassClass.getConstructor(String.class);
    System.out.println(constructor);
		// public cn.wujiwen.basic.reflect.ExampleClass(java.lang.String)

    Constructor<?>[] declaredConstructors = exampleClassClass.getDeclaredConstructors();
    System.out.println(Arrays.toString(declaredConstructors));
    // [public cn.wujiwen.basic.reflect.ExampleClass(java.lang.String), protected cn.wujiwen.basic.reflect.ExampleClass()]
    
		Constructor<ExampleClass> declaredConstructor = exampleClassClass.getDeclaredConstructor(null);
    System.out.println(declaredConstructor);
		// protected cn.wujiwen.basic.reflect.ExampleClass()
		
}

獲取成員方法

@Test
public void getMethod() throws Exception {
    Class<ExampleClass> exampleClassClass = ExampleClass.class;
    Method[] methods = exampleClassClass.getMethods();
    System.out.println(Arrays.toString(methods));
		// 返回所有的public 包括Object類中的wait等方法資訊
		// [public void cn.wujiwen.basic.reflect.ExampleClass.publicMethod(), public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException, public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException, public final void java.lang.Object.wait() throws java.lang.InterruptedException, public boolean java.lang.Object.equals(java.lang.Object), public java.lang.String java.lang.Object.toString(), public native int java.lang.Object.hashCode(), public final native java.lang.Class java.lang.Object.getClass(), public final native void java.lang.Object.notify(), public final native void java.lang.Object.notifyAll()]
    
		Method publicMethod = exampleClassClass.getMethod("publicMethod");
    System.out.println(publicMethod);
		// public void cn.wujiwen.basic.reflect.ExampleClass.publicMethod()
		
    Method[] declaredMethods = exampleClassClass.getDeclaredMethods();
    System.out.println(Arrays.toString(declaredMethods));
		// 只返回本類中的所有方法
		// [private void cn.wujiwen.basic.reflect.ExampleClass.privateMethod(), public void cn.wujiwen.basic.reflect.ExampleClass.publicMethod(), protected void cn.wujiwen.basic.reflect.ExampleClass.protectedMethod(), void cn.wujiwen.basic.reflect.ExampleClass.defaultMethod()]

    Field protectedField = exampleClassClass.getDeclaredField("protectedField");
    System.out.println(protectedField);
		// protected java.lang.String cn.wujiwen.basic.reflect.ExampleClass.protectedField

    Method enclosingMethod = exampleClassClass.getEnclosingMethod();
    System.out.println(enclosingMethod);
		// null 沒有內部匿名方法 orz
}

Field物件的功能

常用的功能通常有獲取屬性值,賦予新值等功能

@Test
public void modifyField() throws Exception {
    ExampleClass exampleClass = new ExampleClass();
    Class<? extends ExampleClass> classClass = exampleClass.getClass();
    Field publicField = classClass.getField("publicField");
    Object publicFieldValue = publicField.get(exampleClass);
    System.out.println(publicFieldValue);

    Field privateField = classClass.getDeclaredField("privateField");
    // // 忽略訪問許可權的檢查,暴力反射
    privateField.setAccessible(true);
    // 獲取值
    Object privateFieldVale = privateField.get(exampleClass);
    System.out.println(privateFieldVale);
    // 賦新值
    privateField.set(exampleClass,"newPrivateValue");
    Object newValue = privateField.get(exampleClass);
    System.out.println(newValue);
}

Method物件的功能

  • 執行方法 Object invoke(Object obj,Object... args);
  • 獲取引數 getParameters()
@Test
public void methodFun() throws Exception {
    ExampleClass exampleClass = new ExampleClass();
    Class<? extends ExampleClass> classClass = exampleClass.getClass();
    Method noParamsMethod = classClass.getMethod("noParamsMethod");
    // 執行方法
    noParamsMethod.invoke(exampleClass);

    Method paramsMethod = classClass.getMethod("paramsMethod", String.class);
    // 獲取引數
    Parameter[] parameters = paramsMethod.getParameters();
    System.out.println(Arrays.toString(parameters));
    // 執行方法
    paramsMethod.invoke(exampleClass,"這是引數");
}

擴充套件

在整理Field,Method,Constructor的過程中,我們發現都繼承自AccessibleObject類,上面提到的setAccessible(true);方法就是該類提供的一個功能。下面還介紹一種框架中非常常用的註解功能,在反射中我們可以獲取到上述三個物件的註解,進而完成更多的自定義功能

首先我們在ExampleClass新增jdk中自帶的註解資訊並嘗試通過反射的方式進行註解資訊的獲取

@Deprecated
public class ExampleClass {
    // public修飾的屬性
    @Deprecated
    public String publicField;
    @Deprecated
    public void publicMethod(){

    }
}

我們分別在類名屬性及方法上新增類一個常見的過期註解

@Test
public void getAnnotation() throws Exception {
    ExampleClass exampleClass = new ExampleClass();
    Class<? extends ExampleClass> classClass = exampleClass.getClass();
    Annotation[] classAnnotations = classClass.getDeclaredAnnotations();
    System.out.println(Arrays.toString(classAnnotations));

    Field publicField = classClass.getField("publicField");
    Annotation[] fieldAnnotations = publicField.getAnnotations();
    System.out.println(Arrays.toString(fieldAnnotations));

    Method publicMethod = classClass.getMethod("publicMethod");
    Deprecated annotation = publicMethod.getAnnotation(Deprecated.class);
    System.out.println(annotation);
}

這樣我們就可以獲取到相關物件上的註解資訊。

實戰應用

上面我們結合案例列舉了很多反射中常用的功能和用法,下面我們將結合一個簡單的功能需求將上面的功能應用在開發實戰中,由於是簡單應用,不做實際開發參考。

功能描述

現在我們有一組已經解析完整的資料(通過excel讀取或者xml方式等等),需要根據要求,組裝到對應的物件中,進行後期的應用,比如:我們有幾組統計資料,一組是學校的學生資訊資料,另一組是學校的教師資訊的資料,已經通過解析工具從三方表格中解析出來並封裝到了一個陣列集合中,陣列索引所對應的資料已知,如index=0的代表姓名

我們先將資料準備好

List<List<Object>> teachers = new ArrayList<>();
List<List<Object>> students = new ArrayList<>();

@BeforeEach
public void data(){
    List<Object> teacher1 = new ArrayList<>();
    teacher1.add("張老師");
    teacher1.add(20);
    teacher1.add("13011111111");
    teacher1.add("語文");

    List<Object> teacher2 = new ArrayList<>();
    teacher2.add("李老師");
    teacher2.add(40);
    teacher2.add("13811111111");
    teacher2.add("數學");

    teachers.add(teacher1);
    teachers.add(teacher2);

    List<Object> student1 = new ArrayList<>();
    student1.add("李華");
    student1.add(10);
    student1.add("三年級一班");

    List<Object> student2 = new ArrayList<>();
    student2.add("趙鐵柱");
    student2.add(11);
    student2.add("四年級二班");

    students.add(student1);
    students.add(student2);
}
[[張老師, 20, 13011111111, 語文], [李老師, 40, 13811111111, 數學]]
[[李華, 10, 三年級一班], [趙鐵柱, 11, 四年級二班]]

現在有兩個類,分別是StudentTeacher類需要接收相應的資料資訊查

public class Student {
    /**
     * 學生姓名
     */
    private String studentName;
    /**
     * 班級名稱
     */
    private String className;
    /**
     * 年齡
     */
    private Integer age;
		
		// setter getter ...
}

public class Teacher {
    private String name;
    private String mobile;
    private String subject;
    private Integer age;
		// setter getter ...
}

下面我們將通過反射的方式將上面的兩組輸入放入到對應的物件中,並準確對應在各自的屬性中

我們可以通過自定義註解的方式,將註解內容作用到每一個欄位上,並標記好該欄位在陣列中的具體索引值,然後賦值給該欄位即可。

自定義一個欄位索引註解

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {FIELD})
public @interface FieldIndex {
    /**
     * 對應的索引
     * @return
     */
    int index();
}

改造Student 和Teacher類

public class Student {
    /**
     * 學生姓名
     */
    @FieldIndex(index = 0)
    private String studentName;
    /**
     * 班級名稱
     */
    @FieldIndex(index = 2)
    private String className;
    /**
     * 年齡
     */
    @FieldIndex(index = 1)
    private Integer age;
}

public class Teacher {
    @FieldIndex(index = 0)
    private String name;
    @FieldIndex(index = 2)
    private String mobile;
    @FieldIndex(index = 3)
    private String subject;
    @FieldIndex(index = 1)
    private Integer age;
}

定義一個工具方法

/**
 *
 * @param t 需要繫結的類
 * @param data 處理的資料
 * @param <T>
 * @return
 */
public <T> void bind(T t,List<Object> data){
    Class<?> clazz = t.getClass();
    Field[] fields = clazz.getDeclaredFields();
    for (Field field : fields) {
        // 獲取欄位的註解
        FieldIndex annotation = field.getAnnotation(FieldIndex.class);
        // 有效註解
        if (annotation != null && annotation.index() >= 0){
            // 獲取註解中的索引值
            int index = annotation.index();
            // 獲取data中的資料
            Object value = data.get(index);
            // 忽略檢查
            field.setAccessible(true);
            // 複製給該欄位
            try {
                field.set(t,value);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
}

測試

@Test
public void dataBind(){
    List<Teacher> teacherList = new ArrayList<>();
    for (List<Object> teacherArr : teachers) {
        Teacher teacher = new Teacher();
        bind(teacher,teacherArr);
        teacherList.add(teacher);
    }
    List<Student> studentList = new ArrayList<>();
    for (List<Object> studentArr : students) {
        Student student = new Student();
        bind(student,studentArr);
        studentList.add(student);
    }
    System.out.println(teacherList);
    System.out.println(studentList);

}

這樣我們就完成了一個簡單的資料繫結了,類似功能的擴充套件還有很多,比如在次基礎上進行資料的校驗。在我們的實際開發過程中也有很多這樣的處理方式,利用反射機制能大大方便我們進行開發。

相關文章