1. 程式人生 > 實用技巧 >java中的反射(三)

java中的反射(三)

目錄
一、反射
1、class類
2、訪問欄位
3、呼叫方法
4、呼叫構造方法
5、獲取繼承物件
6、動態代理
二、sping中的反射

本篇轉自:
本篇內容
spring中的反射

java中的反射(一):
java中的反射(二):

一、Spring中的反射

1.1、建立 Bean 例項時的反射

// 通過類載入器,根據 class 路徑,得到其類物件
Class<?> clz = Thread.currentThread().getContextClassLoader().loadClass("org.deppwang.litespring.v1.service.PetStoreService");
// 根據類物件生成 Bean 例項
return clz.newInstance();

反射體現在 clz.newInstance(); 中,核心程式碼可分為兩部分:

1、利用反射獲取當前類 PetStoreService 的所有構造方法資訊(Constructor 物件)

// java.lang.Class.java
// 呼叫 native 方法,此時 publicOnly 為 false
res = getDeclaredConstructors0(publicOnly);
// native 方法,從 jvm 中的 class 檔案中獲取構造方法資訊,再轉換為 Constructor 物件
private native Constructor<T>[] getDeclaredConstructors0(boolean publicOnly);

2、利用反射通過預設構造方法生成例項

// sun.reflect.NativeConstructorAccessorImpl.java
// 呼叫 native 方法,var1 代表構造方法的引數,此時為 null
return newInstance0(this.c, var1);
// native 方法,真正生成例項的方法,執行 class 檔案的構造方法 <init>
private static native Object newInstance0(Constructor<?> var0, Object[] var1);

1.2、構造方法依賴注入時的反射

// 通過反射獲取當前類所有的構造方法資訊(Constructor 物件)
Constructor<?>[] candidates = beanClass.getDeclaredConstructors();
// 設定構造方法引數例項
Object[] argsToUse = new Object[parameterTypes.length];
argsToUse[i] = getBean(beanNames.get(i));
// 使用帶有引數的 Constructor 物件實現例項化 Bean。此時使用反射跟上面一樣(newInstance0),只是多了引數
return constructorToUse.newInstance(argsToUse);

1.3、setter() 方法依賴注入時的反射

// 通過反射獲取當前類所有的方法資訊(Method 物件)
Method[] methods = bean.getClass().getDeclaredMethods();
// 獲得方法引數例項
Object propertyBean = getBean(propertyName);
// 通過反射執行呼叫 setter() 方法。invoke:呼叫方法,propertyBean 作為方法的引數
method.invoke(bean, propertyBean);

bean.getClass().getDeclaredMethods(); 中的核心程式碼:

// java.lang.Class.java
// 呼叫 native 方法,publicOnly 為 false
getDeclaredMethods0(publicOnly);
// native 方法,從 jvm 中的 class 檔案中獲取方法資訊,再轉換為 Method
private native Method[]      getDeclaredMethods0(boolean publicOnly);

method.invoke(bean, propertyBean); 中的核心程式碼:

// sun.reflect.NativeMethodAccessorImpl.java
// 呼叫 native 方法,var1: bean、var2: propertyBean
return invoke0(this.method, var1, var2);
// native 方法,執行 class 檔案中的位元組碼指令
private static native Object invoke0(Method var0, Object var1, Object[] var2);

1.4、@Autowired 依賴注入時的反射

// 通過反射得到當前類所有的欄位資訊(Field 物件)
Field[] fields = bean.getClass().getDeclaredFields();
// 判斷欄位是否有 @Autowired 註解
Annotation ann = field.getAnnotation(Autowired.class);
// 設定欄位可連線,相當於將非 public(private、default、protect)更改為 public
field.setAccessible(true);
// 通過反射設定欄位的值
field.set(bean, getBean(field.getName()));

bean.getClass().getDeclaredFields(); 中的核心程式碼:

// java.lang.Class.java
// 呼叫 native 方法,此時 publicOnly 為 false
getDeclaredFields0(publicOnly);
// native 方法,從 jvm 中獲取 class 檔案的欄位資訊,再轉換為 Field
private native Field[]       getDeclaredFields0(boolean publicOnly);

field.set(bean, getBean(field.getName())); 中的核心程式碼:

// sun.reflect.UnsafeObjectFieldAccessorImpl.java
// 呼叫 native 方法,將目標物件 var1 指定偏移量 fieldOffset 處的欄位值設定(修改)為 var2。var1 為 bean, var2 為引數例項
unsafe.putObject(var1, this.fieldOffset, var2);

// sun.misc.Unsafe.java
// native 方法,直接修改堆中物件欄位的資料
public native void putObject(Object var1, long var2, Object var4);

二、class 檔案與類物件

class 檔案由 java 檔案編譯而來,class 檔案包含欄位表、方法表、 方法(構造方法)等。

當類載入器將 class 檔案載入進虛擬機器元空間(Meta-space,jdk 1.8)時,虛擬機器在元空間中建立一個與之對應的類物件(Class 例項)。並將 class 檔案由存放在磁碟的靜態結構轉換為存放在記憶體的執行時結構。

我們可以認為一個類(class 檔案)對應一個類物件,當前類的所有物件共用一個類物件。類物件作為訪問存放在 jvm 的 class 檔案的入口。

package java.lang;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Constructor;

public final class Class<T> {
    private native Field[]       getDeclaredFields0(boolean publicOnly);
    private native Method[]      getDeclaredMethods0(boolean publicOnly);
    private native Constructor<T>[] getDeclaredConstructors0(boolean publicOnly);

    // ReflectionData 快取反射物件
    private static class ReflectionData<T> {
        volatile Field[] declaredFields;
        volatile Field[] publicFields;
        volatile Method[] declaredMethods;
        volatile Method[] publicMethods;
        volatile Constructor<T>[] declaredConstructors;
        volatile Constructor<T>[] publicConstructors;
        ...
    }
}

2.1、獲得類物件的方式

// 1、通過物件
Class cls = object.getClass();
// Object.java
public final native Class<?> getClass();

// 2、通過類載入器
Class cls = Thread.currentThread().getContextClassLoader().loadClass("org.deppwang.litespring.v1.service.PetStoreService");

// 3、通過 Class 類,本質上也是通過類載入器
Class cls = Class.forName("org.deppwang.litespring.v1.service.PetStoreService");
// Class.java
private static native Class<?> forName0(String name, boolean initialize,
                                            ClassLoader loader,
                                            Class<?> caller)

三、反射方法

以下是常用的反射方法。

3.1、Feild 相關

Field[] fields = cls.getFields(); // 獲取所有公共的 Field(包括父類)
Field[] fields = cls.getDeclaredFields(); // 獲取當前類的所有 Field(不包括父類),包括公共和非公共
Field field = cls.getDeclaredField("fieldName"); // 指定獲取當前類某個 Field
field.set(Object, Object); // 設定(修改)欄位值
field.get(Object); // 獲取欄位值

field.get(Object) 核心程式碼:

// 呼叫 native 方法,獲取欄位對應的值
return unsafe.getObject(var1, this.fieldOffset);

// native 方法,從堆中獲取物件指定位置的物件
public native Object getObject(Object var1, long var2);

3.2、Method 相關

Method[] methods = cls.getMethods(); // 獲取所有公共的 Method(包括父類)
Method[] methods = cls.getDeclaredMethods(); // 獲取當前類的所有 Method(不包括父類),包括公共和非公共
method.invoke(Object instance, Object... parameters); // 執行方法

執行方法使用場景:要麼是修改物件的資料,如 void setter() 方法;要麼是獲得執行方法的返回結果。

String result = method.invoke().toString();

3.3、Constructor 相關

Constructor<?>[] constructors = cls.getConstructors(); // 獲取所有公共的 Constructor(包括父類)
Constructor<?>[] constructors = cls.getDeclaredConstructors(); // 獲取當前類的所有Constructor(不包括父類),包括公共和非公共
constructor.newInstance(Object... parameters); // 執行構造方法

當沒有明確編寫構造方法,Java 編譯器將為該類構建一個預設建構函式

四、native 方法

Java 1.1 新增「Java 本地介面」(Java Native Interface,JNI),JNI 是一種包容極廣的程式設計介面,允許我們從 Java 應用程式裡呼叫 native 方法,native 方法由其它語言(C 、C++ 或組合語言等)編寫。native 方法用於實現 Java 無法處理的功能。

4.1、簡單示例

一個在 Java 中使用 Java 本地介面(JNI)的簡單示例。

// Main.java
public class Main {
    public native int intMethod(int i);
    static {
        // 啟動時載入 libMain.dylib
        System.loadLibrary("Main");
    }
    public static void main(String[] args) {
        System.out.println(new Main().intMethod(2));
    }
}
// Main.c:
// 將 Main.h 引入
#include "Main.h"

// 相當於繼承 "Main.h" 的 Java_Main_intMethod
JNIEXPORT jint JNICALL Java_Main_intMethod(
    JNIEnv *env, jobject obj, jint i)
{
    return i * i;
}

編譯與執行:

// 同時生成 Main.class 和 Main.h
javac Main.java -h .
// 根據 Main.c 生成 libMain.dylib
gcc -dynamiclib -O3 \
    -I/usr/include \
    -I$JAVA_HOME/include \
    -I$JAVA_HOME/include/darwin \
    Main.c -o libMain.dylib
// 指定 library 的路徑為當前路徑
java -cp . -Djava.library.path=$(pwd) Main

輸出

4
/* Main.h .h 作為標頭檔案*/
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Main */

#ifndef _Included_Main
#define _Included_Main
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     Main
 * Method:    intMethod
 * Signature: (I)I
 */
JNIEXPORT jint JNICALL Java_Main_intMethod
  (JNIEnv *, jobject, jint);

#ifdef __cplusplus
}
#endif
#endif
javac Main.java -h .
// 可拆分為兩個命令
javac Main.java
javah -jni Main

4.2、原理

執行 Main.class 時,將 libMain.dylib 載入虛擬機器,JVM 呼叫 libMain.dylib 的 Java_Main_intMethod,傳入引數,libMain.dylib 由系統直接執行,返回結果。

*env 用於將 java 型別資料與本地(此處為 C 語言)型別資料之間的轉換
jint 還是 Java 資料型別,Java 基本資料型別可以對映(使用),不用通過 *env 轉換

/*C code*/
JNIEXPORT void JNICALL Java_ClassName_MethodName
  (JNIEnv *env, jobject obj, jstring javaString)
{
    /*Get the native string from javaString*/
    const char *nativeString = (*env)->GetStringUTFChars(env, javaString, 0);

    /*Do something with the nativeString*/

    /*DON'T FORGET THIS LINE!!!*/
    (*env)->ReleaseStringUTFChars(env, javaString, nativeString);
}

總結

反射反射,哪裡體現反射字面意思?

可以這麼理解,通過 native 方法得到反射物件,操作反射物件,像鏡子一樣,將反射到原物件上。

我們發現,反射和 native 方法的關係:

獲取欄位、方法、構造方法物件,native() 方法實現
獲取欄位值、設定修改欄位值,native() 方法實現
執行方法,native() 方法實現
執行構造方法,native() 方法實現
我們可以得出結論,反射由 native 方法實現。

我們說通過反射實現一個功能,我們也可以說:

  1. 通過反射方法實現
  2. 通過反射 API 實現
  3. 通過 native 方法實現

    反射是一種非常規(native 方法實現)方式獲取 class 檔案資訊、執行 class 檔案位元組碼指令和操作物件資料的能力。

一句話總結 :反射是一種執行時獲取和修改物件資料的能力。

關於執行時:Java 是靜態語言,先編譯,後執行。編譯時不執行程式碼,程式碼都是執行時執行。