java反射機制實例學習與解析

 

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,這就是一個程序最基本的加載流程。 

java.png

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

java2.png

然後再看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) 第二個參數的賦值,來選擇是否初始化類。

以上純屬學習總結,如果有錯誤,請及時指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章