1. 程式人生 > >大白話說Java反射:入門、使用、原理

大白話說Java反射:入門、使用、原理

 

java的反射機制就是增加程式的靈活性,避免將程式寫死到程式碼裡,
例如: 例項化一個 person()物件, 不使用反射, new person(); 如果想變成 例項化 其他類, 那麼必須修改原始碼,並重新編譯。
      使用反射: class.forName("person").newInstance(); 而且這個類描述可以寫到配置檔案中,如 **.xml, 這樣如果想例項化其他類,只要修改配置檔案的"類描述"就可以了,不需要重新修改程式碼並編譯。

總體來說

增加程式的靈活性。
如struts中。請求的派發控制。
當請求來到時。struts通過查詢配置檔案。找到該請求對應的action。已經方法。
然後通過反射例項化action。並呼叫響應method。
如果不適用反射,那麼你就只能寫死到程式碼裡了。
所以說,一個靈活,一個不靈活。
很少情況下是非用反射不可的。大多數情況下反射是為了提高程式的靈活性。
因此一般框架中使用較多。因為框架要適用更多的情況。對靈活性要求較高。

 

反射之中包含了一個「反」字,所以想要解釋反射就必須先從「正」開始解釋。

一般情況下,我們使用某個類時必定知道它是什麼類,是用來做什麼的。於是我們直接對這個類進行例項化,之後使用這個類物件進行操作。

Apple apple = new Apple(); //直接初始化,「正射」
apple.setPrice(4);

上面這樣子進行類物件的初始化,我們可以理解為「正」。

而反射則是一開始並不知道我要初始化的類物件是什麼,自然也無法使用new關鍵字來建立物件了。

這時候,我們使用JDK提供的反射API進行反射呼叫:

Class clz = Class.forName("com.chenshuyi.reflect.Apple");
Method method = clz.getMethod("setPrice", int.class);
Constructor constructor = clz.getConstructor();
Object object = constructor.newInstance();
method.invoke(object, 4);

上面兩段程式碼的執行結果,其實是完全一樣的。但是其思路完全不一樣,第一段程式碼在未執行時就已經確定了要執行的類(蘋果),而第二段程式碼則是在執行時通過字串值才得知要執行的類(com.chenshuyi.reflect.Apple)。

所以說什麼是反射?

反射就是在執行時才知道要操作的類是什麼,並且可以在執行時獲取類的完整構造,並呼叫對應的方法。

一個簡單的例子

上面提到的示例程式,其完整的程式程式碼如下:

public class Apple {

    private int price;

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public static void main(String[] args) throws Exception{
        //正常的呼叫
        Apple apple = new Apple();
        apple.setPrice(5);
        System.out.println("Apple Price:" + apple.getPrice());
        //使用反射呼叫
        Class clz = Class.forName("com.chenshuyi.api.Apple");
        Method setPriceMethod = clz.getMethod("setPrice", int.class);
        Constructor appleConstructor = clz.getConstructor();
        Object appleObj = appleConstructor.newInstance();
        setPriceMethod.invoke(appleObj, 14);
        Method getPriceMethod = clz.getMethod("getPrice");
        System.out.println("Apple Price:" + getPriceMethod.invoke(appleObj));
    }
}

從程式碼中可以看到我們使用反射呼叫了setPrice方法,並傳遞了14的值。之後使用反射呼叫了getPrice方法,輸出其價格。上面的程式碼整個的輸出結果是:

Apple Price:5
Apple Price:14

從這個簡單的例子可以看出,一般情況下我們使用反射獲取一個物件的步驟:

  • 獲取類的類物件例項
Class clz = Class.forName("com.zhenai.api.Apple");
  • 根據類物件例項獲取建構函式物件
Constructor appleConstructor = clz.getConstructor();
  • 使用建構函式物件的newInstance方法獲取反射類物件
Object appleObj = appleConstructor.newInstance();

而如果要呼叫某一個方法,則需要經過下面的步驟:

  • 獲取方法的方法物件
Method setPriceMethod = clz.getMethod("setPrice", int.class);
  • 利用呼叫方法呼叫方法
setPriceMethod.invoke(appleObj, 14);

到這裡,我們已經能夠掌握反射的基本使用。但如果要進一步掌握反射,還需要對反射的常用API有更深入的理解。

在 JDK 中,反射相關的 API 可以分為下面幾個方面:獲取反射的 Class 物件、通過反射建立類物件、通過反射獲取類屬性方法及構造器。

反射常用API

獲取反射中的Class物件

在反射中,要獲取一個類或呼叫一個類的方法,我們首先需要獲取到該類的 Class 物件。

在 Java API 中,獲取 Class 類物件有三種方法:

第一種,使用 Class.forName 靜態方法。當你知道該類的全路徑名時,你可以使用該方法獲取 Class 類物件。

Class clz = Class.forName("java.lang.String");

第二種,使用 .class 方法。

這種方法只適合在編譯前就知道操作的 Class。

Class clz = String.class;

第三種,使用類物件的 getClass() 方法。

String str = new String("Hello");
Class clz = str.getClass();

通過反射建立類物件

通過反射建立類物件主要有兩種方式:通過 Class 物件的 newInstance() 方法、通過 Constructor 物件的 newInstance() 方法。

第一種:通過 Class 物件的 newInstance() 方法。

Class clz = Apple.class;
Apple apple = (Apple)clz.newInstance();

第二種:通過 Constructor 物件的 newInstance() 方法

Class clz = Apple.class;
Constructor constructor = clz.getConstructor();
Apple apple = (Apple)constructor.newInstance();

通過 Constructor 物件建立類物件可以選擇特定構造方法,而通過 Class 物件則只能使用預設的無引數構造方法。下面的程式碼就呼叫了一個有引數的構造方法進行了類物件的初始化。

Class clz = Apple.class;
Constructor constructor = clz.getConstructor(String.class, int.class);
Apple apple = (Apple)constructor.newInstance("紅富士", 15);

通過反射獲取類屬性、方法、構造器

我們通過 Class 物件的 getFields() 方法可以獲取 Class 類的屬性,但無法獲取私有屬性。

Class clz = Apple.class;
Field[] fields = clz.getFields();
for (Field field : fields) {
    System.out.println(field.getName());
}

輸出結果是:

price

而如果使用 Class 物件的 getDeclaredFields() 方法則可以獲取包括私有屬性在內的所有屬性:

Class clz = Apple.class;
Field[] fields = clz.getDeclaredFields();
for (Field field : fields) {
    System.out.println(field.getName());
}

輸出結果是:

name
price

與獲取類屬性一樣,當我們去獲取類方法、類構造器時,如果要獲取私有方法或私有構造器,則必須使用有 declared 關鍵字的方法。

反射原始碼解析

當我們懂得了如何使用反射後,今天我們就來看看 JDK 原始碼中是如何實現反射的。或許大家平時沒有使用過反射,但是在開發 Web 專案的時候會遇到過下面的異常:

java.lang.NullPointerException 
...
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  at java.lang.reflect.Method.invoke(Method.java:497)

可以看到異常堆疊指出了異常在 Method 的第 497 的 invoke 方法中,其實這裡指的 invoke 方法就是我們反射呼叫方法中的 invoke。

Method method = clz.getMethod("setPrice", int.class); 
method.invoke(object, 4);   //就是這裡的invoke方法

例如我們經常使用的 Spring 配置中,經常會有相關 Bean 的配置:

<bean class="com.chenshuyi.Apple">
</bean>

當我們在 XML 檔案中配置了上面這段配置之後,Spring 便會在啟動的時候利用反射去載入對應的 Apple 類。而當 Apple 類不存在或發生啟發異常時,異常堆疊便會將異常指向呼叫的 invoke 方法。

從這裡可以看出,我們平常很多框架都使用了反射,而反射中最最終的就是 Method 類的 invoke 方法了。

下面我們來看看 JDK 的 invoke 方法到底做了些什麼。

進入 Method 的 invoke 方法我們可以看到,一開始是進行了一些許可權的檢查,最後是呼叫了 MethodAccessor 類的 invoke 方法進行進一步處理,如下圖紅色方框所示。

那麼 MethodAccessor 又是什麼呢?

其實 MethodAccessor 是一個介面,定義了方法呼叫的具體操作,而它有三個具體的實現類:

  • sun.reflect.DelegatingMethodAccessorImpl
  • sun.reflect.MethodAccessorImpl
  • sun.reflect.NativeMethodAccessorImpl

而要看 ma.invoke() 到底呼叫的是哪個類的 invoke 方法,則需要看看 MethodAccessor 物件返回的到底是哪個類物件,所以我們需要進入 acquireMethodAccessor() 方法中看看。

從 acquireMethodAccessor() 方法我們可以看到,程式碼先判斷是否存在對應的 MethodAccessor 物件,如果存在那麼就複用之前的 MethodAccessor 物件,否則呼叫 ReflectionFactory 物件的 newMethodAccessor 方法生成一個 MethodAccessor 物件。

在 ReflectionFactory 類的 newMethodAccessor 方法裡,我們可以看到首先是生成了一個 NativeMethodAccessorImpl 物件,再這個物件作為引數呼叫 DelegatingMethodAccessorImpl 類的構造方法。

這裡的實現是使用了代理模式,將 NativeMethodAccessorImpl 物件交給 DelegatingMethodAccessorImpl 物件代理。我們檢視 DelegatingMethodAccessorImpl 類的構造方法可以知道,其實是將 NativeMethodAccessorImpl 物件賦值給 DelegatingMethodAccessorImpl 類的 delegate 屬性。

所以說ReflectionFactory 類的 newMethodAccessor 方法最終返回 DelegatingMethodAccessorImpl 類物件。所以我們在前面的 ma.invoke() 裡,其將會進入 DelegatingMethodAccessorImpl 類的 invoke 方法中。

進入 DelegatingMethodAccessorImpl 類的 invoke 方法後,這裡呼叫了 delegate 屬性的 invoke 方法,它又有兩個實現類,分別是:DelegatingMethodAccessorImpl 和 NativeMethodAccessorImpl。按照我們前面說到的,這裡的 delegate 其實是一個 NativeMethodAccessorImpl 物件,所以這裡會進入 NativeMethodAccessorImpl 的 invoke 方法。

而在 NativeMethodAccessorImpl 的 invoke 方法裡,其會判斷呼叫次數是否超過閥值(numInvocations)。如果超過該閥值,那麼就會生成另一個MethodAccessor 物件,並將原來 DelegatingMethodAccessorImpl 物件中的 delegate 屬性指向最新的 MethodAccessor 物件。

到這裡,其實我們可以知道 MethodAccessor 物件其實就是具體去生成反射類的入口。通過檢視原始碼上的註釋,我們可以瞭解到 MethodAccessor 物件的一些設計資訊。

"Inflation" mechanism. Loading bytecodes to implement Method.invoke() and Constructor.newInstance() currently costs 3-4x more than an invocation via native code for the first invocation (though subsequent invocations have been benchmarked to be over 20x faster).Unfortunately this cost increases startup time for certain applications that use reflection intensively (but only once per class) to bootstrap themselves.

Inflation 機制。初次載入位元組碼實現反射,使用 Method.invoke() 和 Constructor.newInstance() 載入花費的時間是使用原生程式碼載入花費時間的 3 - 4 倍。這使得那些頻繁使用反射的應用需要花費更長的啟動時間。

為了避免這種損失,我們在方法和建構函式的前幾次呼叫中重用現有的JVM入口點,然後切換到基於位元組碼的實現。可以通過NativeMethodAccessorImpl和NativeConstructorAccessorImpl訪問包私有。

為了避免這種痛苦的載入時間,我們在第一次載入的時候重用了JVM的入口,之後切換到位元組碼實現的實現。

就像註釋裡說的,實際的MethodAccessor實現有兩個版本,一個是Native Version本,一個是Java版本。

原生版本一開始啟動快,但是隨著執行時間邊長,速度變慢的.java版本一開始載入慢,但是隨著執行時間邊長,速度變快。正是因為兩種存在這些問題,所以第一次載入的時候我們會發現使用的是NativeMethodAccessorImpl的實現,而當反射呼叫次數超過15次之後,則使用MethodAccessorGenerator生成的MethodAccessorImpl物件去實現反射。

方法類的呼叫方法整個流程可以表示成如下的時序圖:

講到這裡,我們瞭解了方法類的呼叫方法的具體實現方式。知道了原來的invoke方法內部有兩種實現方式,一種是本地原生的實現方式,一種是Java的實現方式,這兩種各有千秋。而為了最大化效能優勢,JDK原始碼使用了代理的設計模式去實現最大化效能。