Spring中的反射與反射的原理,案例詳解


 

image source: https://coderius.biz.ua/blog/article/vvedenie-v-php-reflection-api

 

造輪子:實現一個簡易的 Spring IoC 容器一文中提到 Spring 在創建 Bean 實例和依賴注入時使用了反射,本文來具體分析一下 Spring 中的反射以及反射的原理。

一、Spring 中的反射

1.1、創建 Bean 實例時的反射

// 通過類加載器,根據 class 路徑,得到其類對象 Class<?> clz = Thread.currentThread().getContextClassLoader().loadClass("org.deppwang.litespring.v1.service.PetStoreService"); // 根據類對象生成 Bean 實例 return clz.newInstance(); 這里也要注意光理論是不夠的,記住:Java架構項目經驗永遠是核心,如果你沒有最新JAVA架構實戰教程及大廠30k+面試寶典,可以去小編的Java架構學習.裙 :七吧傘吧零而衣零傘 (數字的諧音)轉換下可以找到了,里面很多新JAVA架構項目教程,還可以跟老司機交流討教! 

反射體現在 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 文件包含字段表、方法表、<init> 方法(構造方法)等。

當類加載器將 class 文件加載進虛擬機元數據區(方法區,jdk1.7)時,虛擬機創建一個與之對應的類對象(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 編譯器將為該類構建一個默認構造函數 <init>

四、native 方法

Java 1.1 新增「Java 本地接口」(Java Native Interface,JNI),JNI 是一種包容極廣的編程接口,允許我們從 Java 應用程序里調用 native 方法,native 方法由其它語言(C 、C++ 或匯編語言等)編寫。native 方法用於實現 Java 無法處理的功能。

4.1、簡單示例

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

  • 環境:jdk8、macOS 10.15
// 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); } 復制代碼

4.3、參考

五、總結

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

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

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

  • 獲取字段、方法、構造方法對象,native() 方法實現
  • 獲取字段值、設置修改字段值,native() 方法實現
  • 運行方法,native() 方法實現
  • 運行構造方法,native() 方法實現

我們可以得出結論,反射由 native 方法實現。

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

  • 通過反射方法實現
  • 通過反射 API 實現
  • 通過 native 方法實現

 

 

反射是一種非常規(native 方法實現)方式獲取 class 文件信息、運行 class 文件字節碼指令和操作對象數據的能力。

一句話總結 :反射是一種運行時獲取和修改對象數據的能力。

關於運行時:Java 是靜態語言,先編譯,后運行。編譯時不執行代碼,代碼都是運行時執行。

  • 最后注意光理論是不夠的,記住:Java架構項目經驗永遠是核心,如果你沒有最新JAVA架構實戰教程及大廠30k+面試寶典,可以去小編的Java架構學習.裙 :七吧傘吧零而衣零傘 (數字的諧音)轉換下可以找到了,里面很多新JAVA架構項目教程,還可以跟老司機交流討教! 

    本文的文字及圖片來源於網絡加上自己的想法,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯系我們以作處理


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM