老司機帶你深入淺出Java反射
反射,它就像是一種魔法,引入執行時自省能力,賦予了 Java 語言令人意外的活力,通過執行時操作元資料或物件,Java 可以靈活地操作執行時才能確定的資訊
這裡筆者就深入淺出總結下Java反射,若有不正確地方,感謝評論區指正交流~ 建議開啟idea,寫一個Java反射的demo,跟著除錯,效果會更好 :)
反射的概念是由Smith在1982年首次提出的,主要是指程式可以訪問、檢測和修改它本身狀態或行為的一種能力。有了反射,使Java相對於C、C++等語言就有了很強大的操作物件屬性及其方法的能力,注意,反射與直接呼叫物件方法和屬性相比,效能有一定的損耗,但是如果不是用在對效能有很強的場景下,反射都是一個很好且靈活的選擇。
說到反射,首先要了解什麼是Class。每個類都會產生一個對應的Class物件,一般儲存在.class檔案中。所有類都是在對其第一次使用時,動態載入到JVM的,當程式建立一個對類的靜態成員的引用時,就會載入這個類。Class物件僅在需要的時候才會載入,static初始化是在類載入時進行的。類載入時,類載入器首先會檢查這個類的Class物件是否已被載入過,如果尚未載入,預設的類載入器就會根據類名查詢對應的.class檔案。
class檔案
任何一個Class檔案都對應著唯一一個類或介面的資訊(這裡的類包括抽象類哈),但反過來,類或介面資訊並不一定都定義在檔案裡(比如類或介面可能動態生成,Spring中AOP的實現中就有可能動態生成代理類)。Class檔案是一組以8位元組為基礎單位的二進位制檔案,各個資料項嚴格按照順序緊湊著排列,中間幾乎沒有任何分隔符,也就是說整個Class檔案儲存的幾乎都是程式執行所需的必要資料。
想在執行時使用型別資訊,必須獲取物件(比如類Base物件)的Class物件的引用,使用Class.forName(“Base”)可以實現該目的,或者使用Base.class。注意,有一點很有趣,使用”.class”來建立Class物件的引用時,不會自動初始化該Class對應類,使用forName()會自動初始化該Class對應類。使用”.class”不會自動初始化是因為被延遲到了對靜態方法(構造器隱私地是靜態的)或者非常數靜態域進行首次引用時才進行。
為了使用類而做的準備工作一般有以下3個步驟:
載入:由類載入器完成,找到對應的位元組碼,建立一個Class物件
連結:驗證類中的位元組碼,為靜態域分配空間
初始化:如果該類有超類,則對其初始化,執行靜態初始化器和靜態初始化塊
如果不知道某個物件的確切型別,RTTI可以告訴你,但是有一個前提:這個型別在編譯時必須已知,這樣才能使用RTTI來識別它。而Class類與java.lang.reflect類庫一起對反射進行了支援,該類庫包含Field、Method和Constructor類,這些類的物件由JVM在啟動時建立,用以表示未知類裡對應的成員。
反射機制並沒有什麼神奇之處,當通過反射與一個未知型別的物件打交道時,JVM只是簡單地檢查這個物件,看它屬於哪個特定的類。因此,那個類的.class對於JVM來說必須是可獲取的,要麼在本地機器上,要麼從網路獲取。所以對於RTTI和反射之間的真正區別只在於:
RTTI:編譯器在編譯時開啟和檢查.class檔案
反射:執行時開啟和檢查.class檔案
反射應用實踐
反射獲取物件中所有屬性值:
public class Person {
private String name;
private int age;
// ...
}
Person person = new Person();
person.setName("luo");
person.setAge(25);
try {
Class clazz = person.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
System.out.println(field.getType() + " | " + field.getName() + " = " + field.get(person));
}
// 通過反射獲取某一個方法
Method method = clazz.getMethod("setName", String.class);
method.invoke(person, "bei");
} catch (Exception e) {
e.printStackTrace();
}
平常的專案開發基本很少與反射打交道,因為框架已經幫我們做了很多的事情了。但這不能說明反射機制沒有用,實際上有很多設計、開發都與反射機制有關,例如模組化的開發,通過反射去呼叫對應的位元組碼;動態代理設計模式也採用了反射機制,還有我們日常使用的 Spring/Hibernate 等框架,也是利用CGLIB 反射機制才得以實現。
反射技術在在框架和中介軟體技術應用較多,有一句老話就是反射是Java框架的基石。典型的使用就是Spring的IoC實現,不管物件誰管理建立,只要我能用就行。再比如RPC技術可以藉助於反射來實現,本地主機將要遠端呼叫的物件方法等資訊傳送給遠端主機,這些資訊包括class名、方法名、方法引數型別、方法入參等,遠端主機接收到這些資訊後就可以藉助反射來獲取並執行物件方法,然後將結果返回即可。
說了那麼多,那麼Java反射是如何實現的呢?簡單來說Java反射就是靠JVM和Class相關類來實現的,Class相關類包括Field、Method和Constructor類等。類載入器載入完成一個類之後,會生成類對應的Class物件、Field物件、Method物件、Constructor物件,這些物件都儲存在JVM(方法區)中,這也說明了反射必須在載入類之後進行的原因。使用反射時,其實就是與上述所說的這幾個物件打交道呀(貌似Java反射也就這麼一回事哈)。
既然瞭解了Java反射原理,可以試想一下C++為什麼沒有反射呢,想讓C++擁有反射該如何做呢?Java相對於C++實現反射最重要的差別就是Java可以依靠JVM這一悍將,可以由JVM儲存物件的相關資訊,然後應用程式使用時直接從JVM中獲取使用。但是C++編譯後直接變成了機器碼了,貌似類或者物件的啥資訊都沒了。。。 其實想讓C++有用反射能力,就需要儲存能夠操作類方法、類構造方法、類屬性的這些資訊,這些資訊要麼由應用程式自己來做,要麼由第三方工具來儲存,然後應用程式使用從它那裡獲取,這些資訊可以通過(函式)指標來記錄,使用時通過指標來呼叫。
反射機制
這裡我們以Method.invoke流程來分析反射流程:
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
// 其他setter/getter方法
}
public static void main(String[] args) throws Exception {
Person person = new Person();
person.setName("luo");
person.setAge(26);
for (int i = 0; i < 20; i++) {
Method method = Person.class.getMethod("getName");
System.out.println(method.invoke(person));
}
}
以上程式碼通過反射呼叫person物件中的方法,下面跟著原始碼看下Method.invoke的執行流程:
// Method
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
// 這裡會呼叫reflectionFactory.newMethodAccessor(this)建立一個新的MethodAccessor
// 並賦值給methodAccessor,下次就不會進入到這裡了
// ma實際型別是DelegatingMethodAccessorImpl,代理對目標方法的呼叫
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
private MethodAccessorImpl delegate;
DelegatingMethodAccessorImpl(MethodAccessorImpl var1) {
this.setDelegate(var1);
}
public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
return this.delegate.invoke(var1, var2);
}
void setDelegate(MethodAccessorImpl var1) {
this.delegate = var1;
}
}
// NativeMethodAccessorImpl
public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
// ReflectionFactory.inflationThreshold()預設15,如果某一個Method反射呼叫超過15次,
// 則自動生成GeneratedMethodAccessor賦值給DelegatingMethodAccessorImpl.delegate
if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
// 通過asm自動生成MethodAccessorImpl的實現類GeneratedMethodAccessor
MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
this.parent.setDelegate(var3);
}
return invoke0(this.method, var1, var2);
}
// native方法,jni方式呼叫對應方法,呼叫的是對應的java方法
private static native Object invoke0(Method var0, Object var1, Object[] var2);
下面分別看下NativeMethodAccessorImpl和GeneratedMethodAccessor的呼叫棧資訊:
Java預設在執行Method.invoke超過15次時(通過-Dsun.reflect.inflationThreshold可更改次數值,每個方法對應一個Method物件),JVM會通過asm生成GeneratedMethodAccessor類,由該類呼叫對應的method方法。執行NativeMethodAccessorImpl.invoke是通過呼叫JNI方法,在JNI方法中再呼叫對應的java method方法,這種方式相對於使用GeneratedMethodAccessor.invoe方法來說,前者效能較弱,原因有以下幾點:
針對本地方法,jvm無法優化,無法動態inline,其他高階的優化方案都無法優化jni。
執行native涉及到執行棧切換(虛擬機器棧切換到本地方法棧),如果本地方法中再呼叫java方法是有一定的開銷的,肯定比不上Java中呼叫Java方法。
二者記憶體模型不一樣,引數需要轉換,比如字串,陣列,複雜結構。轉換成本非常高。此開銷和呼叫介面引數有關。
在預設情況下,方法的反射呼叫為委派實現,委派給本地實現來進行方法呼叫。在呼叫超過 15 次之後,委派實現便會將委派物件切換至動態實現。這個動態的位元組碼是在Java執行過程中通過ASM自動生成的,它將直接使用 invoke 指令來呼叫目標方法。Java實現的版本在初始化時需要較多時間,但長久來說效能較好;native版本正好相反,啟動時相對較快,但執行時間長了之後速度就比不過Java版了。這是HotSpot的優化方式帶來的效能特性,同時也是許多虛擬機器的共同點:跨越native邊界會對優化有阻礙作用,它就像個黑箱一樣讓虛擬機器難以分析也將其內聯,於是執行時間長了之後反而是託管版本的程式碼更快些。
GeneratedMethodAccessor機制
預設method.invoke呼叫超過15次,會呼叫MethodAccessorGenerator.generate生成對應GeneratedMethodAccessorN類,程式碼如下:
// MethodAccessorGenerator
private MagicAccessorImpl generate(final Class<?> var1, String var2, Class<?>[] var3, Class<?> var4, Class<?>[] var5, int var6, boolean var7, boolean var8, Class<?> var9) {
ByteVector var10 = ByteVectorFactory.create();
this.asm = new ClassFileAssembler(var10);
this.declaringClass = var1;
this.parameterTypes = var3;
this.returnType = var4;
this.modifiers = var6;
this.isConstructor = var7;
this.forSerialization = var8;
this.asm.emitMagicAndVersion();
short var11 = 42;
boolean var12 = this.usesPrimitiveTypes();
if (var12) {
var11 = (short)(var11 + 72);
}
if (var8) {
var11 = (short)(var11 + 2);
}
var11 += (short)(2 * this.numNonPrimitiveParameterTypes());
this.asm.emitShort(add(var11, (short)1));
final String var13 = generateName(var7, var8);
this.asm.emitConstantPoolUTF8(var13);
this.asm.emitConstantPoolClass(this.asm.cpi());
this.thisClass = this.asm.cpi();
if (var7) {
if (var8) {
this.asm.emitConstantPoolUTF8("sun/reflect/SerializationConstructorAccessorImpl");
} else {
this.asm.emitConstantPoolUTF8("sun/reflect/ConstructorAccessorImpl");
}
} else {
this.asm.emitConstantPoolUTF8("sun/reflect/MethodAccessorImpl");
}
this.asm.emitConstantPoolClass(this.asm.cpi());
this.superClass = this.asm.cpi();
this.asm.emitConstantPoolUTF8(getClassName(var1, false));
this.asm.emitConstantPoolClass(this.asm.cpi());
this.targetClass = this.asm.cpi();
short var14 = 0;
if (var8) {
this.asm.emitConstantPoolUTF8(getClassName(var9, false));
this.asm.emitConstantPoolClass(this.asm.cpi());
var14 = this.asm.cpi();
}
this.asm.emitConstantPoolUTF8(var2);
this.asm.emitConstantPoolUTF8(this.buildInternalSignature());
this.asm.emitConstantPoolNameAndType(sub(this.asm.cpi(), (short)1), this.asm.cpi());
if (this.isInterface()) {
this.asm.emitConstantPoolInterfaceMethodref(this.targetClass, this.asm.cpi());
} else if (var8) {
this.asm.emitConstantPoolMethodref(var14, this.asm.cpi());
} else {
this.asm.emitConstantPoolMethodref(this.targetClass, this.asm.cpi());
}
this.targetMethodRef = this.asm.cpi();
if (var7) {
this.asm.emitConstantPoolUTF8("newInstance");
} else {
this.asm.emitConstantPoolUTF8("invoke");
}
this.invokeIdx = this.asm.cpi();
if (var7) {
this.asm.emitConstantPoolUTF8("([Ljava/lang/Object;)Ljava/lang/Object;");
} else {
this.asm.emitConstantPoolUTF8("(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;");
}
this.invokeDescriptorIdx = this.asm.cpi();
this.nonPrimitiveParametersBaseIdx = add(this.asm.cpi(), (short)2);
for(int var15 = 0; var15 < var3.length; ++var15) {
Class var16 = var3[var15];
if (!isPrimitive(var16)) {
this.asm.emitConstantPoolUTF8(getClassName(var16, false));
this.asm.emitConstantPoolClass(this.asm.cpi());
}
}
this.emitCommonConstantPoolEntries();
if (var12) {
this.emitBoxingContantPoolEntries();
}
if (this.asm.cpi() != var11) {
throw new InternalError("Adjust this code (cpi = " + this.asm.cpi() + ", numCPEntries = " + var11 + ")");
} else {
this.asm.emitShort((short)1);
this.asm.emitShort(this.thisClass);
this.asm.emitShort(this.superClass);
this.asm.emitShort((short)0);
this.asm.emitShort((short)0);
this.asm.emitShort((short)2);
this.emitConstructor();
this.emitInvoke();
this.asm.emitShort((short)0);
var10.trim();
final byte[] var17 = var10.getData(); // class資料
return (MagicAccessorImpl)AccessController.doPrivileged(new PrivilegedAction<MagicAccessorImpl>() {
public MagicAccessorImpl run() {
try {
// 生成反射類GeneratedMethodAccessorN,對應的classLoader為DelegatingClassLoader
return (MagicAccessorImpl)ClassDefiner.defineClass(var13, var17, 0, var17.length, var1.getClassLoader()).newInstance();
} catch (IllegalAccessException | InstantiationException var2) {
throw new InternalError(var2);
}
}
});
}
}
View Code
注意:生成對應GeneratedMethodAccessorN類,其對應的classLoader是DelegatingClassLoader,生成的GeneratedMethodAccessorN類是放在永久代的,那麼就會產生一個問題,如果數量過多,則會佔用永久代太多空間(java8中已沒有永久代空間,類資料放在直接記憶體中)。
生成的GeneratedMethodAccessorN類是什麼樣的呢?如下所示:在此我向大家推薦一個架構學習交流群。交流學習群號:821169538 裡面會分享一些資深架構師錄製的視訊錄影:有Spring,MyBatis,Netty原始碼分析,高併發、高效能、分散式、微服務架構的原理,JVM效能優化、分散式架構等這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多。
package sun.reflect;
import com.luo.test.InvokeBean;
import java.lang.reflect.InvocationTargetException;
public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
public GeneratedMethodAccessor1() {
}
public Object invoke(Object var1, Object[] var2) throws InvocationTargetException {
if (var1 == null) {
throw new NullPointerException();
} else {
InvokeBean var10000;
Integer var10001;
try {
var10000 = (InvokeBean)var1;
if (var2.length != 1) {
throw new IllegalArgumentException();
}
var10001 = (Integer)var2[0];
} catch (NullPointerException | ClassCastException var4) {
throw new IllegalArgumentException(var4.toString());
}
try {
return var10000.test(var10001);
} catch (Throwable var3) {
throw new InvocationTargetException(var3);
}
}
}
}