JAVA反射機制是在運行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個對象,都能夠調用它的任意一個方法;簡單說就是:只要給定類的名字,那麼就可以通過反射機制來獲得類的所有信息。
這種動態獲取以及動態調用對象方法的功能稱爲java語言的反射機制。
一、例子解讀JAVA反射機制
有一個用戶自定義的類Car.Class
public class Car {
private String brand;
private String color;
private int maxSpeed;
private String owner;//私有成員,沒有對應的get和set方法;
//①默認構造函數
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);
}
//私有方法
private void whoHasIt(){
System.out.println("The owner of this car is:" + this.owner );
}
//參數的getter/Setter方法
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public int getMaxSpeed() {
return maxSpeed;
}
public void setMaxSpeed(int maxSpeed) {
this.maxSpeed = maxSpeed;
}
}
反射機制怎麼做到:動態獲取以及動態調用對象方法
public class ReflectCall {
public static Car initByDefaultConst() throws Throwable
{
/**
* ①通過類裝載器獲取Car類對象
* 1.獲取當前線程的ClassLoader
* 2.通過指定的全限定類“reflect.Car”裝載Car類對應的反射實例
*/
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class class_car = loader.loadClass("reflect.Car");
/**
* ②獲取類的默認構造器對象並通過它實例化Car
* 1.通過Car的反射類對象獲取Car的構造函數對象cons
* 2.通過構造函數對象的newInstrance()方法實例化Car對象,其效果等同於new Car()
*/
Constructor cons = class_car.getDeclaredConstructor((Class[])null);
Car car = (Car)cons.newInstance();
/**
* ③獲取類的成員變量(包含私有成員),並對成員變量賦值
*/
Field ownerFld = class_car.getDeclaredField("owner");
//取消Java語言訪問檢查以訪問private變量
ownerFld.setAccessible(true);
ownerFld.set(car,"小明");
/**
* ④通過反射方法設置屬性
* 通過Car的反射類對象的getMethod(String methodName,Class paramClass)獲取屬性的Setter方法對象,
* 第一個參數是目標Class的方法名;第二個參數是方法入參的對象類型。
*/
Method setBrand = class_car.getMethod("setBrand",String.class);
/**
* 通過invoke(Object obj,Object param)方法調用目標類的方法,
* 該方法的第一個參數是操作的目標類對象實例;第二個參數是目標方法的入參。
*/
setBrand.invoke(car,"紅旗CA72");
Method setColor = class_car.getMethod("setColor", String.class);
setColor.invoke(car, "黑色");
Method setMaxSpeed = class_car.getMethod("setMaxSpeed",int.class);
setMaxSpeed.invoke(car, 200);
/**
* ⑤獲取類的私有方法,並調用它
*/
Method whoHasItMtd = class_car.getDeclaredMethod("whoHasIt",(Class[])null);
//取消Java語言訪問檢查以訪問private方法
whoHasItMtd.setAccessible(true);
whoHasItMtd.invoke(car,(Object[])null);
return car;
}
public static void main(String[] args) throws Throwable {
Car car = initByDefaultConst();
car.introduce();
}
}
運行結果:
The owner of this car is:小明
brand:紅旗CA72; color:黑色; maxSpeed:200
在 ReflectCall 中,使用了幾個重要的反射類,分別是ClassLoader、Class、Constructor、Method和Field,通過這些反射類就可以間接調用目標Class的各項功能了。
Class反射對象:描述類語義結構,可以從Class對象中獲取構造函數、成員變量、方法類等類元素的反射對象,並以編程的方式通過這些反射對象對目標類對象進行操作。
這些反射對象類在java.reflect包中定義,下面是最主要的三個反射類:
Constructor
Constructor:類的構造函數反射類,通過Class#getConstructors()方法可以獲得類的所有構造函數反射對象數組。在JDK5.0中,還可以通過getConstructor(Class... parameterTypes)獲取擁有特定入參的構造函數反射對象。Constructor的一個主要方法是newInstance(Object[] initargs),通過該方法可以創建一個對象類的實例,相當於new關鍵字。在JDK5.0中該方法演化爲更爲靈活的形式:newInstance (Object... initargs)。
Method
Method:類方法的反射類,通過Class#getDeclaredMethods()方法可以獲取類的所有方法反射類對象數組Method[]。在JDK5.0中可以通過getDeclaredMethod(String name, Class... parameterTypes)獲取特定簽名的方法,name爲方法名;Class...爲方法入參類型列表。Method最主要的方法是invoke(Object obj, Object[] args),obj表示操作的目標對象;在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
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)等。
二、ClassLoader相關介紹
1. classloader
classloader用來加載Class文件到JVM,以供程序使用的。java程序可以動態加載類定義,而這個動態加載的機制就是通過ClassLoader來實現的。
(1)bootstrap classloader
既然ClassLoader是用來加載類到JVM中的,那麼ClassLoader又是如何被加載呢?
這是因爲存在一個ClassLoader不是用java語言所編寫的,而是JVM實現的一部分,這個ClassLoader就是bootstrap classloader(啓動類加載器)。
這個bootstrap classloader在JVM運行的時候加載java核心的API以滿足java程序最基本的需求,其中就包括用戶定義的ClassLoader(這裏所謂的用戶定義是指通過java程序實現的ClassLoader),一個是ExtClassLoader,一個是AppClassLoader。
(2)ExtClassLoader
ExtClassLoader,這個ClassLoader是用來加載java的擴展API的,也就是/lib/ext中的類;
(3)AppClassLoader
AppClassLoader,這個ClassLoader是用來加載用戶機器上CLASSPATH設置目錄中的Class的,通常在沒有指定ClassLoader的情況下,程序員自定義的類就由AppClassLoader進行加載。
2. 一個程序最基本的類加載流程
當運行一個程序的時候,JVM啓動,運行bootstrap classloader,該ClassLoader加載java核心API(ExtClassLoader和AppClassLoader也在此時被加載),然後調用ExtClassLoader加載擴展API,最後AppClassLoader加載CLASSPATH目錄下定義的Class,這就是一個程序最基本的加載流程。
3. ClassLoader使用雙親委託模式進行類加載
每一個自定義ClassLoader都必須繼承ClassLoader這個抽象類,而每個ClassLoader都會有一個parent ClassLoader(注意,這個parent不是指的被繼承的類,而是在實例化該ClassLoader時指定的一個ClassLoader),如果這個parent爲null,那麼就默認該ClassLoader的parent是bootstrap classloader。
下面這個小例子可以看到: 當前線程的上下文類加載器是什麼,還有它的parent ClassLoader,還有它的parent ClassLoader的parent ClassLoader。
public class ClassLoaderTest {
public static void main(String[] args) {
/*
* Thread.currentThread().getContextClassLoader()
* 返回該線程的上下文類加載器。這個上下文類加載器由線程的創建者所提供,爲了代碼運行在該線程時加載類和資源。
* 所以這裏的loader是當前線程的上下文類加載器。
*/
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println("current loader: "+loader);
System.out.println("parent loader: "+loader.getParent());
//祖父ClassLoader是根類裝載器,因爲在Java中無法獲得它的句柄,所以僅返回null
System.out.println("grandparent loader: "+loader.getParent(). getParent());
}
}
輸出結果爲:
current loader: sun.misc.Launcher$AppClassLoader@6da21389
parent loader: sun.misc.Launcher$ExtClassLoader@2bb0bf9a
grandparent loader: null
由上面的例子還可得出:AppClassLoader的parent ClassLoader是ExtClassLoader,而ExtClassLoader的parent ClassLoader是bootstrap classloader。
下面我們再看看爲啥說ClassLoader是使用雙親委託模式進行類加載的?
下面代碼是public abstract class ClassLoader的一個函數:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
①首先判斷這個class是否已經被加載了,如果已經被加載了,則直接返回這個class
②如果當前的class未被加載,則判斷這個ClassLoad的parent ClassLoad是否爲null:
(1)如果parent不爲空,則調用parent的loadClass來加載這個類;
(2)如果parent爲空,則調用BootstrapClassLoader來加載這個類;
③ 如果經過上面的過程,還是沒有找到,則調用自身的findClass進行加載 。
所以,我們要實現一個自定義類的ClassLoad的時候,只需要實現findClass方法即可。
爲什麼要使用這種雙親委託模式呢?
避免重複加載: 當父親已經加載了該類的時候,就沒有必要子ClassLoader再加載一次。
安全考慮:以String類爲例,如果不使用這種委託模式,那我們就可以隨時使用自定義的String來動態替代java核心api中定義類型,這樣會存在非常大的安全隱患,而雙親委託的方式,就可以避免這種情況,因爲String已經在啓動時被加載。
參考資料:http://www.iteye.com/topic/83978
三、思考
學了上面那麼多理論知識,我就在想,在實際使用中,我是否用過這個機制呢?
然後發現在我之前寫過的一篇文章中《數據庫操作(JDBC->mybatis -> mybatis + Spring -> tddl)》,我們要通過jdbc建立數據庫的連接,首先需要加載一個驅動類,下面的例子可以看出,這個驅動類就是通過反射機制加載進來:
public class JDBCUnit {
//表示數據庫的連接對象
private static Connection conn = null;
public static String DBDRIVER = "com.mysql.jdbc.Driver";
//連接地址是由各個數據庫生產商單獨提供的,所以需要單獨記住
public static String DBURL = "jdbc:mysql://127.0.0.1:80/union_cps";
//連接數據庫的用戶名
public static String DBUSER = "username";
//連接數據庫的密碼
public static String DBPASS = "password";
public static Connection getConnection() {
try {
/*
* 1、使用CLASS 類加載驅動程序
* 這裏用了java的反射機制來加載com.mysql.jdbc.Driver這個類
*/
Class.forName(DBDRIVER);
//2、連接數據庫
conn = DriverManager.getConnection(DBURL, DBUSER, DBPASS);
} catch (Exception e) {
e.printStackTrace();
}
return conn;
}
}
Class.forName(DBDRIVER);
在Class.forName加載完驅動類,開始執行靜態初始化代碼時,會自動新建一個Driver的對象,並調用DriverManager.registerDriver把自己註冊到DriverManager中去。
下面看下Driver類的內容:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can\'t register driver!");
}
}
}
conn = DriverManager.getConnection(DBURL, DBUSER, DBPASS);
這個時候debug去看,已經註冊的Driver有2個,其中一個就是我們註冊進去的:com.mysql.jdbc.Driver
然後再看getConnection方法是怎麼去取得連接的:
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
……
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
……
}
Connection con = aDriver.driver.connect(url, info);
com.mysql.jdbc.Driver.connect(url,info)執行之後,發現url= "jdbc:mysql://127.0.0.1:80/union_cps";
這個是它可以處理的,所以就返回對應的connect給con變量。
補充介紹:Class.forName(String className) 與ClassLoader.loadClass(String className)的區別
Class.forName(String className) :加載類,並且執行類初始化
ClassLoader.loadClass(String className):僅僅加載類,不執行類初始化
可以通過Class.forName(String name, boolean initialize,ClassLoader loader) 第二個參數的賦值,來選擇是否初始化類。
以上純屬學習總結,如果有錯誤,請及時指正。