你確定不瞭解下 Java 中反射黑魔法嗎?

語言: CN / TW / HK

攜手創作,共同成長!這是我參與「掘金日新計劃 · 8 月更文挑戰」的第3天,點選檢視活動詳情

前言

反射在Java 中算是黑魔法的存在了。 用一句話來形容「反其道而行之」

很多限制在反射面前,就是形同虛設。 例如我們設定了一個類的成員變數是 private, 目的就是為了不讓外部可以隨意修改訪問。但是呢,使用反射就可以,你說牛不牛。

正因為反射技術的靈活性,所以在各大框架中被頻繁的使用,所以在學習的過程中,瞭解反射的意義對後續框架的學習有很大的幫助。

具體是這麼做到的?還是其他更巧妙的用法?想知道的話,就接著看下去吧~

What:反射是什麼?

在java中,執行時,只要給定類的名字,能夠知道這個類的所有資訊,可以構造出指定物件,可以呼叫它的任意一個屬性和方法。

這種動態獲取的資訊以及動態呼叫物件的方法的功能稱為Java語言的反射機制。

基於這個機制,反射的作用是 - 動態的載入類,動態的獲取類的資訊(屬性,方法,構造器) - 動態的構造物件 - 動態呼叫類和物件的任意方法,構造器 - 獲取泛型資訊 - 處理註解 - 動態代理

How:如何使用反射?

Java 反射機制主要提供了以下功能:

  • 在執行時判斷任意一個物件所屬的類。
  • 在執行時構造任意一個類的物件。
  • 在執行時判斷任意一個類所具有的成員變數和方法。
  • 在執行時呼叫任意一個物件的方法。

咱們下面,使用反射獲取Person 這個類,來為大家一一演示下。

```java public class Person {

public String name;
private int age;
private List<String> favorities;

public Person(String name, int age) {
    this.name = name;
    this.age = age;
}

public void addFavorite(String favorite) {
    if (favorities == null) {
        favorities = new ArrayList<>();
    }
    favorities.add(favorite);
}

private void changeAge(int age) {
    this.age = age;
}

@Override
public String toString() {
    return "Person{" +
            "name='" + name + '\'' +
            ", age=" + age +
            ", favorities=" + favorities +
            '}';
}

} ```

獲取反射中的Class物件

獲取 Class 類物件有三種方法 - 使用 Class.forName 靜態方法。(需要給出該類的全路徑名:包名+類名) - 使用 .class 方法。 - 使用類物件的 getClass() 方法。

java //第一種,使用 Class.forName 靜態方法。 Class clazz1 = Class.forName("reflect.Person"); //第二種,使用 .class 方法。 Class clazz2 = Person.class; //第三種,使用類物件的 getClass() 方法。 Person person = new Person("淺淺",18); Class clazz3 = person.getClass();

其中

  • 基本型別只可以通過.class 方法獲得Class物件。

通過反射建立類物件

很多文章說主要有兩種方式: - 通過 Class 物件的 newInstance() 方法、 - 通過 Constructor 物件的 newInstance() 方法。

兩者的區別是通過 Class 物件則只能使用預設的無引數構造方法,通過 Constructor 物件建立類物件可以選擇特定構造方法。

其實, class.newInstance() 內部實現也是通過 Constructor。而且現在已經標記為 @Deprecated(since="9"),建議使用 Constructor 。

```java //獲取class物件 Class clazz = Class.forName("reflect.Person");

        //如果有預設構造方法,就使用這個
        Person p1 = (Person) clazz.getConstructor().newInstance();

        //指定構造方法
        Person p2 = (Person) clazz.getDeclaredConstructor(String.class, int.class).newInstance("dr", 1);

```

通過反射獲取類的成員變數

  • 成員變數

```java //獲取class物件 Class clazz = Class.forName("reflect.Person"); //構造物件 Person p = (Person) clazz.getDeclaredConstructor(String.class, int.class).newInstance("dr", 18);

        //獲取指定變數 name
        Field nameField = clazz.getField("name");
        //修改變數的值
        nameField.set(p,"張三");
        //獲取指定變數 age 
        Field ageField = clazz.getDeclaredField("age");
        //age 是private變數,需要先設定可用
        ageField.setAccessible(true);
        //修改變數的值
        ageField.setInt(p,17);

```

需要注意的是: - getField() : 只能獲取 public 修飾的變數,如果是私有變數,會報錯NoSuchField - getDeclaredField() : 可用於獲取全部變數,在不知道變數的許可權修飾符的時候,建議使用 getDeclaredField(), 比較保險。

修改變數的值的方法分為兩類 - 引用型別 只有一個方法 set(Object obj, Object value) - 基本型別 為每個基本型別,都提供了一個方法,例如 setInt(Object obj, int i)setLong(Object obj, long l) 大家使用時,根據不同的型別,選擇不同的方法。

通過反射獲取類的方法

```java //獲取class物件 Class clazz = Class.forName("reflect.Person"); //構造物件 Person p = (Person) clazz.getDeclaredConstructor(String.class, int.class).newInstance("dr", 18); //獲取指定方法 "addFavorite" Method method = clazz.getMethod("addFavorite", String.class); //呼叫物件方法 method.invoke(p, "籃球");

        //獲取指定方法 "changeAge"
        Method privateMethod = clazz.getDeclaredMethod("changeAge", int.class);
        //changeAge 是private方法,需要先設定可用
        privateMethod.setAccessible(true);
        privateMethod.invoke(p, 17);

```

同樣提供了 getMethod()getDeclaredMethod(),和 Field 作用一樣,不贅述了,

呼叫方法的方式只有一種 invoke(Object obj, Object... args)

Why:反射為什麼可以拿到所有的類資訊

要想知道這個問題,需要從類的載入機制說起。

當我們寫完一個類,它首先是一個 java 檔案,首先會被編譯成.class 檔案,然後在執行時,要使用到這個類的時候,jvm 會開始載入這個 .class 檔案到記憶體中。 .class 是個二進位制檔案,裡面包含了類的全部資訊。當載入到記憶體的時候,分為兩個部分: - 方法區:儲存類執行時資料結構。 - 堆:建立相應的 Class 物件

所謂類執行時資料結構,其實就是Java類在 JVM 記憶體中的一個快照,包括常量池、類欄位、類方法等。

Class 物件就是相當於是一個入口,放在堆中,方便去程式設計師訪問方法區的類執行時結構資訊。 在這裡插入圖片描述

也就是說,只要類被載入到 JVM 記憶體,就可以獲取它全部的資訊了。

那類載入的時機是什麼呢?

其實,虛擬機器規範中並沒有強制約束何時進行載入,但是規定了5種情況必須對類進行「初始化」,其中就有一條關於反射的:

使用 java.lang.reflect 包的方法對類進行反射呼叫的時候,如果類沒有進行初始化,則需要先觸發其初始化。

那類初始化一定會先執行載入嗎

類的生命週期如下: 在這裡插入圖片描述

其中還規定了 載入、驗證、準備、初始化,解除安裝這5個階段的先後順序是確定的。

所以初始化一定會觸發載入的。

反射的機制就是基於以上基礎。

小問題:同一個類的 Class 物件有幾個呢?

還記得上文「獲取反射中的Class物件」建立的 clazz1,clazz2,clazz3 嗎?你覺得這是3個不同的物件嗎?

System.out.println(clazz1 == clazz2); 會輸出什麼?

System.out.println(clazz2 == clazz3); 會輸出什麼?

答案是都是同個物件,所以輸出的都是 true。 原因是什麼呢?我們馬上來揭曉~

Java 中類的載入都是由類載入器實現的。在我們開發人員眼中,類載入器可以分為以下幾種:

  • 啟動類載入器:Bootstrap ClassLoader 它用來載入Java核心類庫。使用C/C++語言實現的,巢狀在JVM內部,java程式無法直接操作這個類。
  • 擴充套件類載入器:Extension ClassLoader java.ext.dirs目錄中載入類庫,或者從JDK安裝目錄:jre/lib/ext目錄下載入類庫
  • 應用程式類載入器:Application Classloader 程式中預設的類載入器,咱們程式寫的類,預設都是由它載入完成的。
  • 自定義載入器:User ClassLoader 咱們自己如果有特別需求,實現的載入器

他們的之間的關係是:

在這裡插入圖片描述

jvm 對class檔案採用的是按需載入的方式,當需要使用該類時,jvm才會將它的class檔案載入到記憶體中產生class物件。

將這個過程詳細說說的話,就是: - 第一步:如果一個類載入器接收到了類載入的請求,它自己不會先去載入,會把這個請求委託給父類載入器去執行。

  • 第二步:如果父類還存在父類載入器,則繼續向上委託,一直委託到啟動類載入器:Bootstrap ClassLoader

  • 第三步:如果父類載入器可以完成載入任務,就返回成功結果,如果父類載入失敗,就由子類自己去嘗試載入,如果子類載入失敗就會丟擲ClassNotFoundException異常

這就是我們常說的「雙親委派模型」類載入機制。這樣做的好處是,載入器之間具備帶有優先順序的層次關係,每個載入器負責的 classpath 是不同的,而且每載入一個類,全部的載入器範圍判定都是從上到下的,所以可以保證一個Class檔案只能被同一類載入器載入一次,所以在堆中的Class都是同個物件。

Class 是如何儲存反射資料的?

在Class裡有個關鍵的屬性叫做reflectionData,這裡主要存的是每次從jvm裡獲取到的一些類屬性,比如方法,欄位等。避免每次都要從 JVM 裡去獲取資料。

這個屬性主要是SoftReference的,記憶體不足的的情況下有可能會被回收。

ReflectionData 中的屬性是按需懶載入的。當呼叫了某個反射方法獲取屬性時,才會將當前屬性資料填充到ReflectionData 的成員變數中。

```java public final class Class{ //反射資料資料結構 private static class ReflectionData { volatile Field[] declaredFields; volatile Field[] publicFields; volatile Method[] declaredMethods; volatile Method[] publicMethods; volatile Constructor[] declaredConstructors; volatile Constructor[] publicConstructors; // Intermediate results for getFields and getMethods volatile Field[] declaredPublicFields; volatile Method[] declaredPublicMethods; volatile Class<?>[] interfaces;

    // Cached names
    String simpleName;
    String canonicalName;
    static final String NULL_SENTINEL = new String();

    // Value of classRedefinedCount when we created this ReflectionData instance
    final int redefinedCount;

    ReflectionData(int redefinedCount) {
        this.redefinedCount = redefinedCount;
    }
}

//反射資料軟應用物件 private transient volatile SoftReference> reflectionData;

}

```

而且需要說明的是,我們每次獲取反射的Constructor/Method/Field時,都是重新生成的物件。無論是獲取全部,還是某個指定的物件,都會執行下copy() 動作。

以Method為例:

public Method getMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException { Objects.requireNonNull(name); SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkMemberAccess(sm, Member.PUBLIC, Reflection.getCallerClass(), true); } Method method = getMethod0(name, parameterTypes); if (method == null) { throw new NoSuchMethodException(methodToString(name, parameterTypes)); } //執行Copy動作 return getReflectionFactory().copyMethod(method); } copyMethod() 最終會呼叫 Method 類的 copy() 方法

```java Method copy() { if (this.root != null) throw new IllegalArgumentException("Can not copy a non-root Method");

    Method res = new Method(clazz, name, parameterTypes, returnType,
                            exceptionTypes, modifiers, slot, signature,
                            annotations, parameterAnnotations, annotationDefault);
    res.root = this;
    res.methodAccessor = methodAccessor;
    return res;
}

```

由此可見,我們每次通過呼叫getDeclaredMethod方法返回的Method物件其實都是一個新的物件,所以不宜多調哦,如果呼叫頻繁最好快取起來。 同樣的道理,也適用於 Constructor和 Method.

Why: setAccessible() 是如何修改訪問許可權的?

實際上並沒有真正修改Method/Filed/Constructor的訪問許可權,而是通過一個 boolean 值資料 override 跳過許可權檢查的。

許可權的檢查,都封裝在一個 AccessibleObject的類中,Method/Filed/Constructor 都繼承了它。其中提供了 setAccessible()方法來進行許可權檢查開關設定。

``` public class AccessibleObject implements AnnotatedElement {

//預設為false  
boolean override;

/**
 * setAccessible()最終會跳到這個方法
 * 修改override的值
 */
boolean setAccessible0(boolean flag) {
    this.override = flag;
    return flag;
}

} ```

以Method.invoke 為例,其中就判斷了 override屬性。

public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { //override == true,就會不執行許可權檢查 if (!override) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, Modifier.isStatic(modifiers) ? null : obj.getClass(), modifiers); } MethodAccessor ma = methodAccessor; // read volatile if (ma == null) { ma = acquireMethodAccessor(); } return ma.invoke(obj, args); }

Why: 為什麼說反射的效能很差?

我們平常很多框架都使用了反射,而反射中最多使用的就是 Method ,所以我們就從這裡分析。

進入 Method 的 invoke 方法我們可以看到,一開始是進行了一些許可權的檢查,最後是呼叫了 MethodAccessor 類的 invoke 方法進行進一步處理:

java public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { //檢查許可權 if (!override) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, Modifier.isStatic(modifiers) ? null : obj.getClass(), modifiers); } MethodAccessor ma = methodAccessor; if (ma == null) { //獲取MethodAccessor ma = acquireMethodAccessor(); } //實際上呼叫了 MethodAccessor return ma.invoke(obj, args); }

MethodAccessor 實際上是一個介面,的到底是哪個類物件,所以我們需要進入 acquireMethodAccessor() 方法中看看。

```java private MethodAccessor acquireMethodAccessor() { //是否存在對應的 MethodAccessor 物件, MethodAccessor tmp = null; if (root != null) tmp = root.getMethodAccessor(); if (tmp != null) { //存在就複用之前的物件 methodAccessor = tmp; } else { // 否則通過ReflectionFactory 建立一個新的。 tmp = reflectionFactory.newMethodAccessor(this); setMethodAccessor(tmp); }

    return tmp;
}

```

關鍵的實現是 reflectionFactory.newMethodAccessor()方法。 ```java public MethodAccessor newMethodAccessor(Method method) { //檢查是否初始化 checkInitted();

    if (Reflection.isCallerSensitive(method)) {
        Method altMethod = findMethodForReflection(method);
        if (altMethod != null) {
            method = altMethod;
        }
    }

    // use the root Method that will not cache caller class
    Method root = langReflectAccess().getRoot(method);
    if (root != null) {
        method = root;
    }

    //如果是不使用Inflation機制
    if (noInflation && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
        //生成新的 MethodAccessor
        return new MethodAccessorGenerator().
            generateMethod(method.getDeclaringClass(),
                           method.getName(),
                           method.getParameterTypes(),
                           method.getReturnType(),
                           method.getExceptionTypes(),
                           method.getModifiers());
    } else {
    //否則使用NativeMethodAccessorImpl
        NativeMethodAccessorImpl acc =
            new NativeMethodAccessorImpl(method);
        DelegatingMethodAccessorImpl res =
            new DelegatingMethodAccessorImpl(acc);
        acc.setParent(res);
        return res;
    }
}

```

條件語句上出現了 noInflation,這是個啥呢,從初始化方法 checkInitted() 中可以看到都是從JVM的引數設定中讀取的,預設 noInflation = false,也就是說 Inflation 機制開關設定,預設是開啟的。

```java private static void checkInitted() { //省略......

    Properties props = GetPropertyAction.privilegedGetProperties();
    // Inflation 機制開關設定,預設開啟,noInflation 預設是false
    String val = props.getProperty("sun.reflect.noInflation");
    if (val != null && val.equals("true")) {
        noInflation = true;
    }
    // Inflation 機制的閾值引數設定,預設是15
    val = props.getProperty("sun.reflect.inflationThreshold");
    if (val != null) {
        try {
            inflationThreshold = Integer.parseInt(val);
        } catch (NumberFormatException e) {
            throw new RuntimeException("Unable to parse property sun.reflect.inflationThreshold", e);
        }
    }
     //省略......
}

```

還多了一個inflationThreshold 引數,這是一個Int 型別的資料,預設是15,具體使用在 NativeMethodAccessorImpl 的 invoke方法中。

java public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException { // 如果native版本執行超過 -Dsun.reflect.inflationThreshold的的值,預設值是15,則設定DelegatingMethodAccessorImpl的delegate屬性為java版本的MethodAccessor實現,即切換為java版本 if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) { MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers()); // 切換java版本 this.parent.setDelegate(var3); } // native方法 return invoke0(this.method, var1, var2); }

注意了,這個地方就是 Inflation 機制的精髓所在。

反射方法呼叫的版本實現有兩個版本:Java 和 Native 。 - Native: 具體實現是NativeMethodAccessorImpl,實現最終呼叫native方法 invoke0() 。啟動時相對較快,但是執行效率較慢。 - Java 具體實現是 MethodAccessorGeneratorX,通過ASM技術動態生成一個MethodAccessorImpl 的子類並實現 invoke方法。初始化慢,但是執行效率較快

所以綜合兩個實現,基本效能考慮,引申出 inflation 機制,即:初次載入位元組碼實現反射,使用花費時間更短的 native 實現。如果頻繁使用,再動態生成一個類作為 java 實現,後續都切換為 java 實現。

另外這其中,還是使用了設計模式之代理模式:

  • MethodAccessor的實現都委託給 DelegatingMethodAccessorImpl
  • DelegatingMethodAccessorImpl 內部通過delegate屬性來包裝真正實現 invoke 方法的 MethodAccessorImpl
  • Inflation 機制切換實現方法,就是通過修改delegate屬性來實現的。

有沒有感受到它的妙處~

Java 版本的實現詳解

未開啟 Inflation 機制和超過native 方法呼叫次數閾值都是通過MethodAccessorGenerator.generateMethod() 來生成 Java 版的 MethodAccessor 的實現類。

方法很長,簡單看一下:

``` private MagicAccessorImpl generate(final Class<?> declaringClass, String name, Class<?>[] parameterTypes, Class<?> returnType, Class<?>[] checkedExceptions, int modifiers, boolean isConstructor, boolean forSerialization, Class<?> serializationTargetClass) { ByteVector vec = ByteVectorFactory.create(); //位元組碼工具實現 asm = new ClassFileAssembler(vec); this.declaringClass = declaringClass; this.parameterTypes = parameterTypes; this.returnType = returnType; this.modifiers = modifiers; this.isConstructor = isConstructor; this.forSerialization = forSerialization;

    asm.emitMagicAndVersion();
    asm.emitShort(add(numCPEntries, S1));

    final String generatedName = generateName(isConstructor, forSerialization);
    asm.emitConstantPoolUTF8(generatedName);
    asm.emitConstantPoolClass(asm.cpi());
    thisClass = asm.cpi();
    if (isConstructor) {
        if (forSerialization) {
            asm.emitConstantPoolUTF8
                ("jdk/internal/reflect/SerializationConstructorAccessorImpl");
        } else {
            asm.emitConstantPoolUTF8("jdk/internal/reflect/ConstructorAccessorImpl");
        }
    } else {
        asm.emitConstantPoolUTF8("jdk/internal/reflect/MethodAccessorImpl");
    }
    asm.emitConstantPoolClass(asm.cpi());
    superClass = asm.cpi();
    //省略......

```

核心邏輯主要是通過 ClassFileAssembler 類來生成位元組碼,會生成一個 class 檔案,名稱是 GeneratedMethodAccessorX,這個X 是呼叫次數,會遞增的。初始化慢的原因就在這裡。

生成的 class 檔案大概是這個樣子的,想當於直接呼叫。

例如是實現 Person#addFavorite() 方法反射的時候,就會生成如下的 GeneratedMethodAccessorX

```java package sun.reflect;

public class GeneratedMethodAccessor1 extends MethodAccessorImpl { public GeneratedMethodAccessor1() { super(); }

//invoke方法實現 public Object invoke(Object obj, Object[] args) throws IllegalArgumentException, InvocationTargetException { // prepare the target and parameters if (obj == null) throw new NullPointerException(); try { //物件例項型別強轉成 Person Person target = (Person) obj; if (args.length != 1) throw new IllegalArgumentException(); String arg0 = (String) args[0]; } catch (ClassCastException e) { throw new IllegalArgumentException(e.toString()); } catch (NullPointerException e) { throw new IllegalArgumentException(e.toString()); } // make the invocation try { //直接呼叫物件的方法 "addFavorite" target.addFavorite(arg0); } catch (Throwable t) { throw new InvocationTargetException(t); } } } ```

總結起來,Java版的實現就是用空間換時間。

需要特別說明的是,載入Java版本 GeneratedMethodAccessorX的類載入器是專門提供的自定義載入器 DelegatingClassLoader,而且每個方法都會生成一個類載入器。

```java static Class<?> defineClass(String name, byte[] bytes, int off, int len, final ClassLoader parentClassLoader) { ClassLoader newLoader = AccessController.doPrivileged( new PrivilegedAction() { public ClassLoader run() { return new DelegatingClassLoader(parentClassLoader); } }); return JLA.defineClass(newLoader, name, bytes, null, "ClassDefiner"); }

//自定義載入器,內部沒有額外實現,只是名字的區別 class DelegatingClassLoader extends ClassLoader { DelegatingClassLoader(ClassLoader parent) { super(parent); } } ```

方法註釋中,解釋的也很清楚:

  • 安全風險:避免了在同一個載入器下,載入不同的位元組碼帶來的未知風險。
  • 效能考慮:在某些情況下可以提前解除安裝這些生成的類,從而減少執行時間。(因為類的解除安裝是隻有在類載入器可以被回收的情況下才會被回收的)

最後

嘔心瀝血萬字長文,讓我對反射認識的更清晰了,同時也希望能夠帶給大家幫助~


最簡單的易懂的技術乾貨,好玩的事情,都會在「淺淺同學的開發筆記」分享,快來呀~期待與你共同成長!