學習Spring必學的Java基礎知識(1)----反射

Java語言允許通過程序化的方式間接對Class進行操作,Class文件由類裝載器裝載後,在JVM中將形成一份描述Class結構的元信息對象,通過該元信息對象可以獲知Class的結構信息:如構造函數、屬性和方法等。Java允許用戶藉由這個Class相關的元信息對象間接調用Class對象的功能,這就爲使用程序化方式操作Class對象開闢了途徑。 

簡單實例 

    我們將從一個簡單例子開始探訪Java反射機制的征程,下面的Car類擁有兩個構造函數、兩個方法以及三個屬性,如代碼清單3-9所示: 

代碼清單3-9  Car 

package com.baobaotao.reflect;
public class Car {
	private String brand;
	private String color;
	private int maxSpeed;
    
     //①默認構造函數
	public Car(){}
     
     //②帶參構造函數
	public Car(String brand,String color,int maxSpeed){ 
		this.brand = brand;
		this.color = color;
		this.maxSpeed = maxSpeed;
	}

     //③未帶參的方法
	public void introduce() { 
       System.out.println("brand:"+brand+";color:"+color+";maxSpeed:" +maxSpeed);
	}
     //省略參數的getter/Setter方法
     …
}


一般情況下,我們會使用如下的代碼創建Car的實例: 
Car car = new Car();
car.setBrand("紅旗CA72");


或者: 

Car car = new Car("紅旗CA72","黑色");

以上兩種方法都採用傳統方式的直接調用目標類的方法,下面我們通過Java反射機制以一種更加通用的方式間接地操作目標類: 

代碼清單3-10  ReflectTest 

package com.baobaotao. reflect;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ReflectTest {
	public static Car  initByDefaultConst() throws Throwable
	{
		//①通過類裝載器獲取Car類對象
		ClassLoader loader = Thread.currentThread().getContextClassLoader(); 
		Class clazz = loader.loadClass("com.baobaotao.reflect.Car"); 
		
          //②獲取類的默認構造器對象並通過它實例化Car
		Constructor cons = clazz.getDeclaredConstructor((Class[])null); 
		Car car = (Car)cons.newInstance();
		
         
          //③通過反射方法設置屬性
		Method setBrand = clazz.getMethod("setBrand",String.class);	       
		setBrand.invoke(car,"紅旗CA72");		
		Method setColor = clazz.getMethod("setColor",String.class);
		setColor.invoke(car,"黑色");		
		Method setMaxSpeed = clazz.getMethod("setMaxSpeed",int.class);
		setMaxSpeed.invoke(car,200);		
		return car;
	}

	public static void main(String[] args) throws Throwable {
		Car car = initByDefaultConst();
		car.introduce();
	}
}


運行以上程序,在控制檯上將打印出以下信息: 
引用
brand:紅旗CA72;color:黑色;maxSpeed:200


這說明我們完全可以通過編程方式調用Class的各項功能,這和直接通過構造函數和方法調用類功能的效果是一致的,只不過前者是間接調用,後者是直接調用罷了。 

在ReflectTest中,使用了幾個重要的反射類,分別是ClassLoader、Class、Constructor和Method,通過這些反射類就可以間接調用目標Class的各項功能了。在①處,我們獲取當前線程的ClassLoader,然後通過指定的全限定類“com.baobaotao.beans.Car”裝載Car類對應的反射實例。在②處,我們通過Car的反射類對象獲取Car的構造函數對象cons,通過構造函數對象的newInstrance()方法實例化Car對象,其效果等同於new Car()。在③處,我們又通過Car的反射類對象的getMethod(String methodName,Class paramClass)獲取屬性的Setter方法對象,第一個參數是目標Class的方法名;第二個參數是方法入參的對象類型。獲取方法反射對象後,即可通過invoke(Object obj,Object param)方法調用目標類的方法,該方法的第一個參數是操作的目標類對象實例;第二個參數是目標方法的入參。 

在代碼清單3 10中,粗體所示部分的信息即是通過反射方法操控目標類的元信息,如果我們將這些信息以一個配置文件的方式提供,就可以使用Java語言的反射功能編寫一段通用的代碼對類似於Car的類進行實例化及功能調用操作了。 

類裝載器ClassLoader 

類裝載器工作機制 

類裝載器就是尋找類的節碼文件並構造出類在JVM內部表示對象的組件。在Java中,類裝載器把一個類裝入JVM中,要經過以下步驟: 

  • [1.]裝載:查找和導入Class文件; 
    [2.]鏈接:執行校驗、準備和解析步驟,其中解析步驟是可以選擇的: 
        [2.1]校驗:檢查載入Class文件數據的正確性; 
        [2.2]準備:給類的靜態變量分配存儲空間; 
        [2.3]解析:將符號引用轉成直接引用; 
    [3.]初始化:對類的靜態變量、靜態代碼塊執行初始化工作。 

類裝載工作由ClassLoader及其子類負責,ClassLoader是一個重要的Java運行時系統組件,它負責在運行時查找和裝入Class字節碼文件。JVM在運行時會產生三個ClassLoader:根裝載器、ExtClassLoader(擴展類裝載器)和AppClassLoader(系統類裝載器)。其中,根裝載器不是ClassLoader的子類,它使用C++編寫,因此我們在Java中看不到它,根裝載器負責裝載JRE的核心類庫,如JRE目標下的rt.jar、charsets.jar等。ExtClassLoader和AppClassLoader都是ClassLoader的子類。其中ExtClassLoader負責裝載JRE擴展目錄ext中的JAR類包;AppClassLoader負責裝載Classpath路徑下的類包。 

這三個類裝載器之間存在父子層級關係,即根裝載器是ExtClassLoader的父裝載器,ExtClassLoader是AppClassLoader的父裝載器。默認情況下,使用AppClassLoader裝載應用程序的類,我們可以做一個實驗: 

代碼清單3-11  ClassLoaderTest 

public class ClassLoaderTest {
	public static void main(String[] args) {
		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		System.out.println("current loader:"+loader);
		System.out.println("parent loader:"+loader.getParent());
		System.out.println("grandparent loader:"+loader.getParent(). getParent());
	}
}


運行以上代碼,在控制檯上將打出以下信息: 
引用
current loader:sun.misc.Launcher$AppClassLoader@131f71a 
parent loader:sun.misc.Launcher$ExtClassLoader@15601ea 
     //①根裝載器在Java中訪問不到,所以返回null 
grandparent loader:null


通過以上的輸出信息,我們知道當前的ClassLoader是AppClassLoader,父ClassLoader是ExtClassLoader,祖父ClassLoader是根類裝載器,因爲在Java中無法獲得它的句柄,所以僅返回null。 

JVM裝載類時使用“全盤負責委託機制”,“全盤負責”是指當一個ClassLoader裝載一個類的時,除非顯式地使用另一個ClassLoader,該類所依賴及引用的類也由這個ClassLoader載入;“委託機制”是指先委託父裝載器尋找目標類,只有在找不到的情況下才從自己的類路徑中查找並裝載目標類。這一點是從安全角度考慮的,試想如果有人編寫了一個惡意的基礎類(如java.lang.String)並裝載到JVM中將會引起多麼可怕的後果。但是由於有了“全盤負責委託機制”,java.lang.String永遠是由根裝載器來裝載的,這樣就避免了上述事件的發生。 

ClassLoader重要方法 

在Java中,ClassLoader是一個抽象類,位於java.lang包中。下面對該類的一些重要接口方法進行介紹: 
  •   Class loadClass(String name)    name參數指定類裝載器需要裝載類的名字,必須使用全限定類名,如com.baobaotao. beans.Car。該方法有一個重載方法loadClass(String name ,boolean resolve),resolve參數告訴類裝載器是否需要解析該類。在初始化類之前,應考慮進行類解析的工作,但並不是所有的類都需要解析,如果JVM只需要知道該類是否存在或找出該類的超類,那麼就不需要進行解析。 
  • Class defineClass(String name, byte[] b, int off, int len)   將類文件的字節數組轉換成JVM內部的java.lang.Class對象。字節數組可以從本地文件系統、遠程網絡獲取。name爲字節數組對應的全限定類名。 
  •   Class findSystemClass(String name)   從本地文件系統載入Class文件,如果本地文件系統不存在該Class文件,將拋出ClassNotFoundException異常。該方法是JVM默認使用的裝載機制。 
  •   Class findLoadedClass(String name)  調用該方法來查看ClassLoader是否已裝入某個類。如果已裝入,那麼返回java.lang.Class對象,否則返回null。如果強行裝載已存在的類,將會拋出鏈接錯誤。 
  •   ClassLoader getParent()   獲取類裝載器的父裝載器,除根裝載器外,所有的類裝載器都有且僅有一個父裝載器,ExtClassLoader的父裝載器是根裝載器,因爲根裝載器非Java編寫,所以無法獲得,將返回null。 

除JVM默認的三個ClassLoader以外,可以編寫自己的第三方類裝載器,以實現一些特殊的需求。類文件被裝載並解析後,在JVM內將擁有一個對應的java.lang.Class類描述對象,該類的實例都擁有指向這個類描述對象的引用,而類描述對象又擁有指向關聯ClassLoader的引用,如圖3-4所示。 

 

每一個類在JVM中都擁有一個對應的java.lang.Class對象,它提供了類結構信息的描述。數組、枚舉、註解以及基本Java類型(如int、double等),甚至void都擁有對應的Class對象。Class沒有public的構造方法。Class對象是在裝載類時由JVM通過調用類裝載器中的defineClass()方法自動構造的。 

Java反射機制 

Class反射對象描述類語義結構,可以從Class對象中獲取構造函數、成員變量、方法類等類元素的反射對象,並以編程的方式通過這些反射對象對目標類對象進行操作。這些反射對象類在java.reflect包中定義,下面是最主要的三個反射類: 
  •   Constructor:類的構造函數反射類,通過Class#getConstructors()方法可以獲得類的所有構造函數反射對象數組。在JDK5.0中,還可以通過getConstructor(Class... parameterTypes)獲取擁有特定入參的構造函數反射對象。Constructor的一個主要方法是newInstance(Object[] initargs),通過該方法可以創建一個對象類的實例,相當於new關鍵字。在JDK5.0中該方法演化爲更爲靈活的形式:newInstance (Object... initargs)。
  •   Method:類方法的反射類,通過Class#getDeclaredMethods()方法可以獲取類的所有方法反射類對象數組Method[]。在JDK5.0中可以通過getDeclaredMethod(String name, Class... parameterTypes)獲取特定簽名的方法,name爲方法名;Class...爲方法入參類型列表。Method最主要的方法是invoke(Object obj, Object[] args),obj表示操作的目標對象;args爲方法入參,代碼清單3 10③處演示了這個反射類的使用方法。在JDK 5.0中,該方法的形式調整爲invoke(Object obj, Object... args)。此外,Method還有很多用於獲取類方法更多信息的方法:      1)Class getReturnType():獲取方法的返回值類型; 
          2)Class[] getParameterTypes():獲取方法的入參類型數組; 
          3)Class[] getExceptionTypes():獲取方法的異常類型數組; 
          4)Annotation[][] getParameterAnnotations():獲取方法的註解信息,JDK 5.0中的新方法;
  •   Field:類的成員變量的反射類,通過Class#getDeclaredFields()方法可以獲取類的成員變量反射對象數組,通過Class#getDeclaredField(String name)則可獲取某個特定名稱的成員變量反射對象。Field類最主要的方法是set(Object obj, Object value),obj表示操作的目標對象,通過value爲目標對象的成員變量設置值。如果成員變量爲基礎類型,用戶可以使用Field類中提供的帶類型名的值設置方法,如setBoolean(Object obj, boolean value)、setInt(Object obj, int value)等。

此外,Java還爲包提供了Package反射類,在JDK 5.0中還爲註解提供了AnnotatedElement反射類。總之,Java的反射體系保證了可以通過程序化的方式訪問目標類中所有的元素,對於private或protected的成員變量和方法,只要JVM的安全機制允許,也可以通過反射進行調用,請看下面的例子: 

代碼清單3-12  PrivateCarReflect 

package com.baobaotao.reflect;
public class PrivateCar {
       //①private成員變量:使用傳統的類實例調用方式,只能在本類中訪問
   private String color; 
        //②protected方法:使用傳統的類實例調用方式,只能在子類和本包中訪問
   protected void drive(){
	   
System.out.println("drive private car! the color is:"+color);
   }
}


color變量和drive()方法都是私有的,通過類實例變量無法在外部訪問私有變量、調用私有方法的,但通過反射機制卻可以繞過這個限制: 

代碼清單3-13  PrivateCarReflect 

…
public class PrivateCarReflect {
   public static void main(String[] args) throws Throwable{
	   ClassLoader loader = Thread.currentThread().getContextClassLoader();
	   Class clazz = loader.loadClass("com.baobaotao.reflect.PrivateCar");
	   PrivateCar pcar = (PrivateCar)clazz.newInstance();
	   
	   Field colorFld = clazz.getDeclaredField("color");
        //①取消Java語言訪問檢查以訪問private變量
	   colorFld.setAccessible(true); 
	   colorFld.set(pcar,"紅色");
	   
	   Method driveMtd = clazz.getDeclaredMethod("drive",(Class[])null);
        //Method driveMtd = clazz.getDeclaredMethod("drive"); JDK5.0下使用
       
        //②取消Java語言訪問檢查以訪問protected方法
	   driveMtd.setAccessible(true); 
        driveMtd.invoke(pcar,(Object[])null);
  }
}


運行該類,打印出以下信息: 
引用
drive private car! the color is:紅色


在訪問private、protected成員變量和方法時必須通過setAccessible(boolean access)方法取消Java語言檢查,否則將拋出IllegalAccessException。如果JVM的安全管理器設置了相應的安全機制,調用該方法將拋出SecurityException。 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章