Java中的類反射機制

kyokosaika發表於2020-04-04

Java中的類反射機制<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

 

一、反射的概念

反射的概念是由Smith1982年首次提出的,主要是指程式可以訪問、檢測和修改它本身狀態或行為的一種能力。這一概念的提出很快引發了電腦科學領域關於應用反射性的研究。它首先被程式語言的設計領域所採用,並在Lisp和麵向物件方面取得了成績。其中LEAD/LEAD++ OpenC++ MetaXaOpenJava等就是基於反射機制的語言。最近,反射機制也被應用到了視窗系統、作業系統和檔案系統中。

反射本身並不是一個新概念,它可能會使我們聯想到光學中的反射概念,儘管電腦科學賦予了反射概念新的含義,但是,從現象上來說,它們確實有某些相通之處,這些有助於我們的理解。在電腦科學領域,反射是指一類應用,它們能夠自描述和自控制。也就是說,這類應用通過採用某種機制來實現對自己行為的描述(self-representation)和監測(examination),並能根據自身行為的狀態和結果,調整或修改應用所描述行為的狀態和相關的語義。可以看出,同一般的反射概念相比,電腦科學領域的反射不單單指反射本身,還包括對反射結果所採取的措施。所有采用反射機制的系統(即反射系統)都希望使系統的實現更開放。可以說,實現了反射機制的系統都具有開放性,但具有開放性的系統並不一定採用了反射機制,開放性是反射系統的必要條件。一般來說,反射系統除了滿足開放性條件外還必須滿足原因連線(Causally-connected)。所謂原因連線是指對反射系統自描述的改變能夠立即反映到系統底層的實際狀態和行為上的情況,反之亦然。開放性和原因連線是反射系統的兩大基本要素。13700863760

Java中,反射是一種強大的工具。它使您能夠建立靈活的程式碼,這些程式碼可以在執行時裝配,無需在元件之間進行源代表連結。反射允許我們在編寫與執行時,使我們的程式程式碼能夠接入裝載到JVM中的類的內部資訊,而不是原始碼中選定的類協作的程式碼。這使反射成為構建靈活的應用的主要工具。但需注意的是:如果使用不當,反射的成本很高。

二、Java中的類反射:

Reflection Java 程式開發語言的特徵之一,它允許執行中的 Java 程式對自身進行檢查,或者說自審,並能直接操作程式的內部屬性。Java 的這一能力在實際應用中也許用得不是很多,但是在其它的程式設計語言中根本就不存在這一特性。例如,PascalC 或者 C++ 中就沒有辦法在程式中獲得函式定義相關的資訊。

1.檢測類:

1.1 reflection的工作機制

考慮下面這個簡單的例子,讓我們看看 reflection 是如何工作的。

import java.lang.reflect.*;
public class DumpMethods {
    public static void main(String args[]) {
        try {
            Class c = Class.forName(args[0]);
            Method m[] = c.getDeclaredMethods();
            for (int i = 0; i < m.length; i++)
                System.out.println(m[i].toString());
        } catch (Throwable e) {
            System.err.println(e);
        }
    }
}

按如下語句執行:

java DumpMethods java.util.Stack

它的結果輸出為:

public java.lang.Object java.util.Stack.push(java.lang.Object)

public synchronized java.lang.Object java.util.Stack.pop()

public synchronized java.lang.Object java.util.Stack.peek()

public boolean java.util.Stack.empty()

public synchronized int java.util.Stack.search(java.lang.Object)

這樣就列出了java.util.Stack 類的各方法名以及它們的限制符和返回型別。

這個程式使用 Class.forName 載入指定的類,然後呼叫 getDeclaredMethods 來獲取這個類中定義了的方法列表。java.lang.reflect.Methods 是用來描述某個類中單個方法的一個類。

1.2 Java類反射中的主要方法

對於以下三類元件中的任何一類來說 -- 建構函式、欄位和方法 -- java.lang.Class 提供四種獨立的反射呼叫,以不同的方式來獲得資訊。呼叫都遵循一種標準格式。以下是用於查詢建構函式的一組反射呼叫:

l         Constructor getConstructor(Class[] params) -- 獲得使用特殊的引數型別的公共建構函式,

l         Constructor[] getConstructors() -- 獲得類的所有公共建構函式

l         Constructor getDeclaredConstructor(Class[] params) -- 獲得使用特定引數型別的建構函式(與接入級別無關)

l         Constructor[] getDeclaredConstructors() -- 獲得類的所有建構函式(與接入級別無關)

獲得欄位資訊的Class 反射呼叫不同於那些用於接入建構函式的呼叫,在引數型別陣列中使用了欄位名:

l         Field getField(String name) -- 獲得命名的公共欄位

l         Field[] getFields() -- 獲得類的所有公共欄位

l         Field getDeclaredField(String name) -- 獲得類宣告的命名的欄位

l         Field[] getDeclaredFields() -- 獲得類宣告的所有欄位

用於獲得方法資訊函式:

l         Method getMethod(String name, Class[] params) -- 使用特定的引數型別,獲得命名的公共方法

l         Method[] getMethods() -- 獲得類的所有公共方法

l         Method getDeclaredMethod(String name, Class[] params) -- 使用特寫的引數型別,獲得類宣告的命名的方法

l         Method[] getDeclaredMethods() -- 獲得類宣告的所有方法

 

1.3開始使用 Reflection

用於 reflection 的類,如 Method,可以在 java.lang.relfect 包中找到。使用這些類的時候必須要遵循三個步驟:第一步是獲得你想操作的類的 java.lang.Class 物件。在執行中的 Java 程式中,用 java.lang.Class 類來描述類和介面等。

下面就是獲得一個 Class 物件的方法之一:

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

這條語句得到一個 String 類的類物件。還有另一種方法,如下面的語句:

Class c = int.class;

或者

Class c = Integer.TYPE;

它們可獲得基本型別的類資訊。其中後一種方法中訪問的是基本型別的封裝類 ( Integer) 中預先定義好的 TYPE 欄位。

第二步是呼叫諸如 getDeclaredMethods 的方法,以取得該類中定義的所有方法的列表。

一旦取得這個資訊,就可以進行第三步了——使用 reflection API 來操作這些資訊,如下面這段程式碼:

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

Method m[] = c.getDeclaredMethods();

System.out.println(m[0].toString());

它將以文字方式列印出 String 中定義的第一個方法的原型。

2.處理物件:

如果要作一個開發工具像debugger之類的,你必須能發現filed values,以下是三個步驟:

a.建立一個Class物件
b.
通過getField 建立一個Field物件
c.
呼叫Field.getXXX(Object)方法(XXXInt,Float等,如果是物件就省略;Object是指例項).

例如:
import java.lang.reflect.*;
import java.awt.*;

class SampleGet {

   public static void main(String[] args) {
      Rectangle r = new Rectangle(100, 325);
      printHeight(r);

   }

   static void printHeight(Rectangle r) {
      Field heightField;
      Integer heightValue;
      Class c = r.getClass();
      try {
        heightField = c.getField("height");
        heightValue = (Integer) heightField.get(r);
        System.out.println("Height: " + heightValue.toString());
      } catch (NoSuchFieldException e) {
          System.out.println(e);
      } catch (SecurityException e) {
          System.out.println(e);
      } catch (IllegalAccessException e) {
          System.out.println(e);
      }
   }
}

 

三、安全性和反射

在處理反射時安全性是一個較複雜的問題。反射經常由框架型程式碼使用,由於這一點,我們可能希望框架能夠全面接入程式碼,無需考慮常規的接入限制。但是,在其它情況下,不受控制的接入會帶來嚴重的安全性風險,例如當程式碼在不值得信任的程式碼共享的環境中執行時。

由於這些互相矛盾的需求,Java程式語言定義一種多級別方法來處理反射的安全性。基本模式是對反射實施與應用於原始碼接入相同的限制:

n         從任意位置到類公共元件的接入

n         類自身外部無任何到私有元件的接入

n         受保護和打包(預設接入)元件的有限接入

不過至少有些時候,圍繞這些限制還有一種簡單的方法。我們可以在我們所寫的類中,擴充套件一個普通的基本類java.lang.reflect.AccessibleObject 類。這個類定義了一種setAccessible方法,使我們能夠啟動或關閉對這些類中其中一個類的例項的接入檢測。唯一的問題在於如果使用了安全性管理器,它將檢測正在關閉接入檢測的程式碼是否許可了這樣做。如果未許可,安全性管理器丟擲一個例外。

下面是一段程式,在TwoString 類的一個例項上使用反射來顯示安全性正在執行:

public class ReflectSecurity {

    public static void main(String[] args) {

        try {

            TwoString ts = new TwoString("a", "b");

            Field field = clas.getDeclaredField("m_s1");

//          field.setAccessible(true);

            System.out.println("Retrieved value is " +

                field.get(inst));

        } catch (Exception ex) {

            ex.printStackTrace(System.out);

        }

    }

}

如果我們編譯這一程式時,不使用任何特定引數直接從命令列執行,它將在field .get(inst)呼叫中丟擲一個IllegalAccessException異常。如果我們不註釋field.setAccessible(true)程式碼行,那麼重新編譯並重新執行該程式碼,它將編譯成功。最後,如果我們在命令列新增了JVM引數-Djava.security.manager以實現安全性管理器,它仍然將不能通過編譯,除非我們定義了ReflectSecurity類的許可許可權。

四、反射效能

反射是一種強大的工具,但也存在一些不足。一個主要的缺點是對效能有影響。使用反射基本上是一種解釋操作,我們可以告訴JVM,我們希望做什麼並且它滿足我們的要求。這類操作總是慢於只直接執行相同的操作。

下面的程式是欄位接入效能測試的一個例子,包括基本的測試方法。每種方法測試欄位接入的一種形式 -- accessSame 與同一物件的成員欄位協作,accessOther 使用可直接接入的另一物件的欄位,accessReflection 使用可通過反射接入的另一物件的欄位。在每種情況下,方法執行相同的計算 -- 迴圈中簡單的加/乘順序。

程式如下:

public int accessSame(int loops) {

    m_value = 0;

    for (int index = 0; index < loops; index++) {

        m_value = (m_value + ADDITIVE_VALUE) *

            MULTIPLIER_VALUE;

    }

    return m_value;

}

 

public int accessReference(int loops) {

    TimingClass timing = new TimingClass();

    for (int index = 0; index < loops; index++) {

        timing.m_value = (timing.m_value + ADDITIVE_VALUE) *

            MULTIPLIER_VALUE;

    }

    return timing.m_value;

}

 

public int accessReflection(int loops) throws Exception {

    TimingClass timing = new TimingClass();

    try {

        Field field = TimingClass.class.

            getDeclaredField("m_value");

        for (int index = 0; index < loops; index++) {

            int value = (field.getInt(timing) +

                ADDITIVE_VALUE) * MULTIPLIER_VALUE;

            field.setInt(timing, value);

        }

        return timing.m_value;

    } catch (Exception ex) {

        System.out.println("Error using reflection");

        throw ex;

    }

}

在上面的例子中,測試程式重複呼叫每種方法,使用一個大迴圈數,從而平均多次呼叫的時間衡量結果。平均值中不包括每種方法第一次呼叫的時間,因此初始化時間不是結果中的一個因素。下面的圖清楚的向我們展示了每種方法欄位接入的時間:

1:欄位接入時間
<?xml:namespace prefix = v ns = "urn:schemas-microsoft-com:vml" />

我們可以看出:在前兩副圖中(Sun JVM),使用反射的執行時間超過使用直接接入的1000倍以上。通過比較,IBM JVM可能稍好一些,但反射方法仍舊需要比其它方法長700倍以上的時間。任何JVM上其它兩種方法之間時間方面無任何顯著差異,但IBM JVM幾乎比Sun JVM快一倍。最有可能的是這種差異反映了Sun Hot Spot JVM的專業優化,它在簡單基準方面表現得很糟糕。反射效能是Sun開發1.4 JVM時關注的一個方面,它在反射方法呼叫結果中顯示。在這類操作的效能方面,Sun <?xml:namespace prefix = st1 ns = "urn:schemas-microsoft-com:office:smarttags" />1.4.1 JVM顯示了比1.3.1版本很大的改進。

如果為為建立使用反射的物件編寫了類似的計時測試程式,我們會發現這種情況下的差異不象欄位和方法呼叫情況下那麼顯著。使用newInstance()呼叫建立一個簡單的java.lang.Object例項耗用的時間大約是在Sun 1.3.1 JVM上使用new Object()12倍,是在IBM 1.4.0 JVM的四倍,只是Sun 1.4.1 JVM上的兩部。使用Array.newInstance(type, size)建立一個陣列耗用的時間是任何測試的JVM上使用new type[size]的兩倍,隨著陣列大小的增加,差異逐步縮小。

結束語

Java語言反射提供一種動態連結程式元件的多功能方法。它允許程式建立和控制任何類的物件(根據安全性限制),無需提前硬編碼目標類。這些特性使得反射特別適用於建立以非常普通的方式與物件協作的庫。例如,反射經常在持續儲存物件為資料庫、XML或其它外部格式的框架中使用。Java reflection 非常有用,它使類和資料結構能按名稱動態檢索相關資訊,並允許在執行著的程式中操作這些資訊。Java 的這一特性非常強大,並且是其它一些常用語言,如 CC++Fortran 或者 Pascal 等都不具備的。

但反射有兩個缺點。第一個是效能問題。用於欄位和方法接入時反射要遠慢於直接程式碼。效能問題的程度取決於程式中是如何使用反射的。如果它作為程式執行中相對很少涉及的部分,緩慢的效能將不會是一個問題。即使測試中最壞情況下的計時圖顯示的反射操作只耗用幾微秒。僅反射在效能關鍵的應用的核心邏輯中使用時效能問題才變得至關重要。

許多應用中更嚴重的一個缺點是使用反射會模糊程式內部實際要發生的事情。程式人員希望在原始碼中看到程式的邏輯,反射等繞過了原始碼的技術會帶來維護問題。反射程式碼比相應的直接程式碼更復雜,正如效能比較的程式碼例項中看到的一樣。解決這些問題的最佳方案是保守地使用反射——僅在它可以真正增加靈活性的地方——記錄其在目標類中的使用。

 

 

利用反射實現的動態載入


Bromon原創 請尊重版權

最近在成都寫一個移動增值專案,俺負責後臺server端。功能很簡單,手機使用者通過GPRS開啟Socket與伺服器連線,我則根據使用者傳過來的資料做出響應。做過似專案的兄弟一定都知道,首先需要定義一個似於MSNP的通訊協議,不過今天的話題是如何把這個系統設計得具有高度的擴充套件性。由於這個專案本身沒有進行過較為完善的客戶溝通和需求分析,所以以後肯定會有很多功能上的擴充套件,通訊協議肯定會越來越龐大,而我作為一個不那麼勤快的人,當然不想以後再去修改寫好的程式,所以這個專案是實踐物件導向設計的好機會。

首先定義一個介面來隔離

package org.bromon.reflect;

public interface Operator

{

public java.util.List act(java.util.List params)

}

根據設計模式的原理,我們可以為不同的功能編寫不同的,每個都繼承Operator介面,客戶端只需要針對Operator介面程式設計就可以避免很多麻煩。比如這個

package org.bromon.reflect.*;

public class Success implements Operator

{

public java.util.List act(java.util.List params)

{

List result=new ArrayList();

result.add(new String(“操作成功”));

return result;

}

}

我們還可以寫其他很多,但是有個問題,介面是無法例項化的,我們必須手動控制具體例項化哪個,這很不爽,如果能夠嚮應用程式傳遞一個引數,讓自己去選擇例項化一個,執行它的act方法,那我們的工作就輕鬆多了。

很幸運,我使用的是Java,只有Java才提供這樣的反射機制,或者說內省機制,可以實現我們的無理要求。編寫一個配置檔案emp.properties:

#成功響應

1000=Success

#向客戶傳送普通文字訊息

2000=Load

#客戶向伺服器傳送普通文字訊息

3000=Store

檔案中的鍵名是客戶將發給我的訊息頭,客戶傳送1000給我,那麼我就執行Successact方法,似的如果傳送2000給我,那就執行Loadact方法,這樣一來系統就完全符合開閉原則了,如果要新增新的功能,完全不需要修改已有程式碼,只需要在配置檔案中新增對應規則,然後編寫新的,實現act方法就ok,即使我棄這個專案而去,它將來也可以很好的擴充套件。這樣的系統具備了非常良好的擴充套件性和可插入性。

下面這個例子體現了動態載入的功能,程式在執行過程中才知道應該例項化哪個

package org.bromon.reflect.*;

import java.lang.reflect.*;

public class TestReflect

{

//載入配置檔案,查詢訊息頭對應的

private String loadProtocal(String header)

{

String result=null;

try

{

Properties prop=new Properties();

FileInputStream fis=new FileInputStream("emp.properties");

prop.load(fis);

result=prop.getProperty(header);

fis.close();

}catch(Exception e)

{

System.out.println(e);

}

return result;

}

//針對訊息作出響應,利用反射匯入對應的

public String response(String header,String content)

{

String result=null;

String s=null;

try

{

/*

* 匯入屬性檔案emp.properties,查詢header所對應的的名字

* 通過反射機制動態載入匹配的,所有的都被Operator介面隔離

* 可以通過修改屬性檔案、新增新的(繼承MsgOperator介面)來擴充套件協議

*/

s="org.bromon.reflect."+this.loadProtocal(header);

//載入

Class c=Class.forName(s);

//建立的事例

Operator mo=(Operator)c.newInstance();

//構造引數列表

Class params[]=new Class[1];

params[0]=Class.forName("java.util.List");

//查詢act方法

Method m=c.getMethod("act",params);

Object args[]=new Object[1];

args[0]=content;

//呼叫方法並且獲得返回

Object returnObject=m.invoke(mo,args);

}catch(Exception e)

{

System.out.println("Handler-response:"+e);

}

return result;

}

public static void main(String args[])

{

TestReflect tr=new TestReflect();

tr.response(args[0],”訊息內容”);

}

}

測試一下:java TestReflect 1000

這個程式是針對Operator程式設計的,所以無需做任何修改,直接提供LoadStore,就可以支援20003000做引數的呼叫。

有了這樣的內省機制,可以把介面的作用發揮到極至,設計模式也更能體現出威力,而不僅僅供我們飯後閒聊。

 

 

相關文章