淺談JDK動態代理<轉載2>

前情提要

假設如今項目經理有一個需求:在項目現有全部類的方法先後打印日誌。java

你如何在不修改已有代碼的前提下,完成這個需求?程序員

靜態代理數組

具體作法以下:app

1.爲現有的每個類都編寫一個對應的代理類,而且讓它實現和目標類相同的接口(假設都有)ide

2.在建立代理對象時,經過構造器塞入一個目標對象,而後在代理對象的方法內部調用目標對象同名方法,並在調用先後打印日誌。也就是說,代理對象 = 加強代碼 + 目標對象(原對象),有了代理對象後,就不用原對象了學習

靜態代理的缺陷測試

程序員要手動爲每個目標類,編寫對應的代理類。若是當前系統已經有成百上千個類,工做量太大了。因此,如今咱們的努力方向是:如何少寫或者不寫代理類,卻能完成代理功能?ui


接口建立對象的可行性分析

複習對象的建立過程this

首先,在不少初學者的印象中,類和對象的關係是這樣的:編碼

雖然知道源代碼通過javac命令編譯後會在磁盤中獲得字節碼文件(.class文件),也知道java命令會啓動JVM將字節碼文件加載進內存,但也僅僅止步於此了。至於從字節碼文件加載進內存到堆中產生對象,期間具體發生了什麼,他們並不清楚。

所謂「萬物皆對象」,字節碼文件也難逃「被對象」的命運。它被加載進內存後,JVM爲其建立了一個對象,之後全部該類的實例,皆以它爲模板。這個對象叫Class對象,它是Class類的實例。

你們想一想,Class類是用來描述全部類的,好比Person類,Student類...那我如何經過Class類建立Person類的Class對象呢?這樣嗎:

Class clazz = new Class();

好像不對吧,我說這是Student類的Class對象也行啊。有點暈了...

其實,程序員是沒法本身new一個Class對象的,它僅由JVM建立。

  • Class類的構造器是private的,杜絕了外界經過new建立Class對象的可能。當程序須要某個類時,JVM本身會調用這個構造器,並傳入ClassLoader(類加載器),讓它去加載字節碼文件到內存,而後JVM爲其建立對應的Class對象
  • 爲了方便區分,Class對象的表示法爲:Class<String>,Class<Person>

因此藉此機會,咱們不妨換種方式看待類和對象:

也就是說,要獲得一個類的實例,關鍵是先獲得該類的Class對象!只不過new這個關鍵字實在太方便,爲咱們隱藏了底層不少細節,我在剛開始學習Java時甚至沒意識到Class對象的存在。

接口Class和類Class的區別

來分析一下接口Class和類Class的區別。以Calculator接口的Class對象和CalculatorImpl實現類的Class對象爲例:

import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;

public class ProxyTest {
    public static void main(String[] args) {
        /*Calculator接口的Class對象
                  獲得Class對象的三種方式:1.Class.forName(xxx) 
                                           2.xxx.class 
                                           3.xxx.getClass()
                  注意,這並非咱們new了一個Class對象,而是讓虛擬機加載並建立Class對象            
                */
        Class<Calculator> calculatorClazz = Calculator.class;
        //Calculator接口的構造器信息
        Constructor[] calculatorClazzConstructors = calculatorClazz.getConstructors();
        //Calculator接口的方法信息
        Method[] calculatorClazzMethods = calculatorClazz.getMethods();
        //打印
        System.out.println("------接口Class的構造器信息------");
        printClassInfo(calculatorClazzConstructors);
        System.out.println("------接口Class的方法信息------");
        printClassInfo(calculatorClazzMethods);

        //Calculator實現類的Class對象
        Class<CalculatorImpl> calculatorImplClazz = CalculatorImpl.class;
        //Calculator實現類的構造器信息
        Constructor<?>[] calculatorImplClazzConstructors = calculatorImplClazz.getConstructors();
        //Calculator實現類的方法信息
        Method[] calculatorImplClazzMethods = calculatorImplClazz.getMethods();
        //打印
        System.out.println("------實現類Class的構造器信息------");
        printClassInfo(calculatorImplClazzConstructors);
        System.out.println("------實現類Class的方法信息------");
        printClassInfo(calculatorImplClazzMethods);
    }

    public static void printClassInfo(Executable[] targets){
        for (Executable target : targets) {
            // 構造器/方法名稱
            String name = target.getName();
            StringBuilder sBuilder = new StringBuilder(name);
            // 拼接左括號
            sBuilder.append('(');
            Class[] clazzParams = target.getParameterTypes();
            // 拼接參數
            for(Class clazzParam : clazzParams){
                sBuilder.append(clazzParam.getName()).append(',');
            }
            //刪除最後一個參數的逗號
            if(clazzParams!=null && clazzParams.length != 0) {
                sBuilder.deleteCharAt(sBuilder.length()-1);
            }
            //拼接右括號
            sBuilder.append(')');
            //打印 構造器/方法
            System.out.println(sBuilder.toString());
        }
    }
}

運行結果:

  • 接口Class對象沒有構造方法,因此Calculator接口不能直接new對象
  • 實現類Class對象有構造方法,因此CalculatorImpl實現類能夠new對象
  • 接口Class對象有兩個方法add()、subtract()
  • 實現類Class對象除了add()、subtract(),還有從Object繼承的方法

也就是說,接口和實現類的Class信息除了構造器,基本類似。

既然咱們但願經過接口建立實例,就沒法避開下面兩個問題:

1.接口方法體缺失問題

首先,接口的Class對象已經獲得,它描述了方法信息。

但它沒方法體。

不要緊,反正代理對象的方法是個空殼,只要調用目標對象的方法便可。

JVM能夠在建立代理對象時,隨便糊弄一個空的方法體,反正後期咱們會想辦法把目標對象塞進去調用。

因此這個問題,勉強算是解決。

2.接口Class沒有構造器,沒法new

這個問題好像無解...畢竟這麼多年了,的確沒聽哪位仁兄直接new接口的。

可是,仔細想一想,接口之因此不能new,是由於它缺乏構造器,它自己是具有完善的類結構信息的。就像一個武藝高強的大內太監(接口),他空有一身絕世神功(類結構信息),卻後繼無人。若是江湖上有一位妙手聖醫,能克隆他的一身武藝,那麼克隆人不就武藝高強的同時,還能生兒育女了嗎?
因此咱們就想,JDK有沒有提供這麼一個方法,好比getXxxClass(),咱們傳進一個接口Class對象,它幫咱們克隆一個具備相同類結構信息,又具有構造器的新的Class對象呢?

至此,分析完畢,咱們沒法根據接口直接建立對象(廢話)。

那動態代理是怎麼建立實例的呢?它到底有沒有相似getXxxClass()這樣的方法呢?


動態代理

不錯,動態代理確實存在getXxxClass()這樣的方法。

咱們須要java.lang.reflect.InvocationHandler接口和 java.lang.reflect.Proxy類的支持。Proxy後面會用到InvocationHandler,所以我打算以Proxy爲切入點。首先,再次明確咱們的思路:

經過查看API,咱們發現Proxy類有一個靜態方法能夠幫助咱們。

Proxy.getProxyClass():返回代理類的Class對象。終於找到妙手聖醫。

也就說,只要傳入目標類實現的接口的Class對象,getProxyClass()方法便可返回代理Class對象,而不用實際編寫代理類。這至關於什麼概念?

廢話很少說,開搞。

import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyTest {
    public static void main(String[] args) {
        /*
         * 參數1:Calculator的類加載器(當初把Calculator加載進內存的類加載器)
         * 參數2:代理對象須要和目標對象實現相同接口Calculator
         * */
        Class calculatorProxyClazz = Proxy.getProxyClass(Calculator.class.getClassLoader(), Calculator.class);
        //以Calculator實現類的Class對象做對比,看看代理Class是什麼類型
                System.out.println(CalculatorImpl.class.getName());
        System.out.println(calculatorProxyClazz.getName());
        //打印代理Class對象的構造器
        Constructor[] constructors = calculatorProxyClazz.getConstructors();
        System.out.println("----構造器----");
        printClassInfo(constructors);
        //打印代理Class對象的方法
        Method[] methods = calculatorProxyClazz.getMethods();
        System.out.println("----方法----");
        printClassInfo(methods);
    }

    public static void printClassInfo(Executable[] targets) {
        for (Executable target : targets) {
            // 構造器/方法名稱
            String name = target.getName();
            StringBuilder sBuilder = new StringBuilder(name);
            // 拼接左括號
            sBuilder.append('(');
            Class[] clazzParams = target.getParameterTypes();
            // 拼接參數
            for (Class clazzParam : clazzParams) {
                sBuilder.append(clazzParam.getName()).append(',');
            }
            //刪除最後一個參數的逗號
            if (clazzParams != null && clazzParams.length != 0) {
                sBuilder.deleteCharAt(sBuilder.length() - 1);
            }
            //拼接右括號
            sBuilder.append(')');
            //打印 構造器/方法
            System.out.println(sBuilder.toString());
        }
    }
}

運行結果:

你們還記得接口Class的打印信息嗎?

也就是說,經過給Proxy.getProxyClass()傳入類加載器和接口Class對象,咱們獲得了一個增強版的Class:即包含接口的方法信息add()、subtract(),又包含了構造器$Proxy0(InvocationHandler),還有一些本身特有的方法以及從Object繼承的方法。

梳理一下:

1.原先咱們本打算直接根據接口Class獲得代理對象,無奈接口Class只有方法信息,沒有構造器

2.因而,咱們想,有沒有辦法建立一個Class對象,既有接口Class的方法信息,同時又包含構造器方便建立代理實例呢?

3.利用Proxy類的靜態方法getProxyClass()方法,給它傳一個接口Class對象,它能返回一個增強版Class對象。也就是說getProxyClass()的本質是:用Class,造Class。

要謝謝Proxy類和JVM,讓咱們不寫代理類卻直接獲得代理Class對象,進而獲得代理對象。

靜態代理

動態代理:用Class造Class

既然Class<$Proxy0>有方法信息,又有構造器,咱們試着用它獲得代理實例吧:

咱們發現,newInstance()建立對象失敗。由於Class的newInstance()方法底層會走無參構造器。而以前打印$Proxy0的Class信息時,咱們發現它沒有無參構造,只有有參構造$Proxy0(InvocationHandler)。那就靠它了:

constructor.newInstance()須要傳入一個InvocationHandler對象,這裏採用匿名對象的方式,invoke()方法不作具體實現,直接返回null

舒服~


Proxy.getProxyClass()的祕密

一個小問題

好不容易經過Proxy.getProxyClass()獲得代理Class,又經過反射最終獲得代理對象,固然要玩一玩:

尷尬,居然發生了空指針異常。縱觀整個代碼,新寫的add()和subtract()返回值是int,不會是空指針。而再往上的代碼以前編譯都是經過的,應該沒問題啊。再三思量,咱們發現匿名對象InvocationHandler的invoke()返回null。難道是它?作個實驗:讓invoke()返回1,而後觀察結果。

結果代理對象的add和subtract都返回1

巧合嗎?應該不是。我猜:每次調用代理對象的方法都會調用invoke(),且invoke()的返回值就是代理方法的返回值。若是真是如此,空指針異常就能夠解釋了:add()和suntract()期待的返回值類型是int,可是以前invoke()返回null,類型不匹配,因而空指針異常。

以防萬一,再驗證一下invoke()和代理對象方法的關係:

好了,什麼都不用說了。就目前的實驗來看,調用過程應該是這樣:

動態代理底層調用邏輯

一樣的,知道告終果後,咱們再反推原理。

靜態代理:往代理對象的構造器傳入目標對象,而後代理對象調用目標對象的同名方法。

動態代理:constructor反射建立代理對象時,須要傳入InvocationHandler,我猜,代理對象內部有一個成員變量InvocationHandler:

果真不出所料。那麼動態代理的大體設計思路就是:

爲何這麼設計?

爲了解耦,也爲了通用性。

若是JVM生成代理對象的同時生成了特定邏輯的方法體,那這個代理對象後期就沒有擴展的餘地,只能有一種玩法。而引入InvocationHandler的好處是:

  • JVM建立代理對象時沒必要考慮方法實現,只要造一個空殼的代理對象,舒服
  • 後期代理對象想要什麼樣的方法實現,我寫在invocationHandler對象的invoke()方法裏送進來即是

因此,invocationHandler的做用,倒像是把「方法」和「方法體」分離。JVM只造一個空的代理對象給你,後面想怎麼玩,由你本身組裝。反正代理對象中有個成員變量invocationHandler,每個方法裏只有一句話:handler.invoke()。因此調任何一個代理方法,最終都會跑去調用invoke()方法。

invoke()方法是代理對象和目標對象的橋樑。

可是咱們真正想要的結果是:調用代理對象的方法時,去調用目標對象的方法。

因此,接下來努力的方向就是:設法在invoke()方法獲得目標對象,並調用目標對象的同名方法。

代理對象調用目標對象方法

那麼,如何在invoke()方法內部獲得目標對象呢?咱們來看看能不能從invoke()方法的形參上獲取點線索:

  • Object proxy:很遺憾,是代理對象自己,而不是目標對象(不要調用,會無限遞歸)
  • Method method:本次被調用的代理對象的方法
  • Obeject[] args:本次被調用的代理對象的方法參數

很惋惜,proxy不是代理對象。其實想一想也知道,建立代理對象的過程當中自始至終沒有目標對象參與,因此也就沒法產生關聯。並且一個接口能夠同時被多個類實現,因此JVM也沒法判斷當前代理對象想要代理哪一個目標對象。但好在咱們已經知道本次調的方法名(Method)和參數(args)。咱們接下來要作的就是獲得目標對象並調用同名方法,而後把參數給它。

如何獲得目標對象呢?沒辦法,爲今之計只能new了...哈哈哈哈。我靠,饒了一大圈,又是動態代理,又是invoke()的,結果仍是要手動new?別急,先玩玩。後面會改進的:

可是這樣的寫法顯然是倒退30年,一晚上回到解放前。咱們須要改進一下,封裝Proxy.getProxyClass(),使得目標對象能夠做爲參數傳入:

public class ProxyTest {
    public static void main(String[] args) throws Throwable {
        CalculatorImpl target = new CalculatorImpl();
                //傳入目標對象
                //目的:1.根據它實現的接口生成代理對象 2.代理對象調用目標對象方法
        Calculator calculatorProxy = (Calculator) getProxy(target);
        calculatorProxy.add(1, 2);
        calculatorProxy.subtract(2, 1);
    }

    private static Object getProxy(final Object target) throws Exception {
        //參數1:隨便找個類加載器給它, 參數2:目標對象實現的接口,讓代理對象實現相同接口
        Class proxyClazz = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
        Constructor constructor = proxyClazz.getConstructor(InvocationHandler.class);
        Object proxy = constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println(method.getName() + "方法開始執行...");
                Object result = method.invoke(target, args);
                System.out.println(result);
                System.out.println(method.getName() + "方法執行結束...");
                return result;
            }
        });
        return proxy;
    }
}

厲害厲害...惋惜,仍是太麻煩了。有沒有更簡單的方式獲取代理對象?有!

直接返回代理對象,而不是代理對象Class

從一開始就存在,哈哈。可是我以爲getProxyClass()切入更好理解。

public class ProxyTest {
    public static void main(String[] args) throws Throwable {
        CalculatorImpl target = new CalculatorImpl();
        Calculator calculatorProxy = (Calculator) getProxy(target);
        calculatorProxy.add(1, 2);
        calculatorProxy.subtract(2, 1);
    }

    private static Object getProxy(final Object target) throws Exception {
        Object proxy = Proxy.newProxyInstance(
                target.getClass().getClassLoader(),/*類加載器*/
                target.getClass().getInterfaces(),/*讓代理對象和目標對象實現相同接口*/
                new InvocationHandler(){/*代理對象的方法最終都會被JVM導向它的invoke方法*/
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println(method.getName() + "方法開始執行...");
                        Object result = method.invoke(target, args);
                        System.out.println(result);
                        System.out.println(method.getName() + "方法執行結束...");
                        return result;
                    }
                }
        );
        return proxy;
    }
}

編寫可生成代理和可插入通知的通用方法

上面的代碼,已經比上一篇開頭直接修改目標類好多了。再來看一下當時的四大缺點:

  1. 直接修改源程序,不符合開閉原則。應該對擴展開放,對修改關閉√
  2. 若是Calculator有幾十個、上百個方法,修改量太大√
  3. 存在重複代碼(都是在覈心代碼先後打印日誌)×
  4. 日誌打印硬編碼在代理類中,不利於後期維護:好比你花了一上午終於寫完了,組長告訴你這個功能取消,因而你又要打開Calculator花十分鐘刪除日誌打印的代碼!×

使用動態代理,讓咱們避免手寫代理類,只要給getProxy()方法傳入target就能夠生成對應的代理對象。可是日誌打印還是硬編碼在invoke()方法中。雖然修改時只要改一處,可是別忘了「開閉原則」。因此最好是能把日誌打印單獨拆出來,像目標對象同樣做爲參數傳入。

日誌打印其實就是AOP裏的通知概念。我打算定義一個Advice接口,而且寫一個MyLogger實現該接口。

通知接口

public interface Advice {
    void beforeMethod(Method method);
    void afterMethod(Method method);
}

日誌打印

public class MyLogger implements Advice {

    public void beforeMethod(Method method) {
        System.out.println(method.getName() + "方法執行開始...");
    }

    public void afterMethod(Method method) {
        System.out.println(method.getName() + "方法執行結束...");
    }
}

測試類

public class ProxyTest {
    public static void main(String[] args) throws Throwable {
        CalculatorImpl target = new CalculatorImpl();
        Calculator calculatorProxy = (Calculator) getProxy(target, new MyLogger());
        calculatorProxy.add(1, 2);
        calculatorProxy.subtract(2, 1);
    }

    private static Object getProxy(final Object target, Advice logger) throws Exception {
        /*代理對象的方法最終都會被JVM導向它的invoke方法*/
        Object proxy = Proxy.newProxyInstance(
                target.getClass().getClassLoader(),/*類加載器*/
                target.getClass().getInterfaces(),/*讓代理對象和目標對象實現相同接口*/
                (proxy1, method, args) -> {
                    logger.beforeMethod(method);
                    Object result = method.invoke(target, args);
                    System.out.println(result);
                    logger.afterMethod(method);
                    return result;
                }
        );
        return proxy;
    }
}

差一點完美~下篇講講更完美的作法。


類加載器補充

初學者可能對諸如「字節碼文件」、Class對象比較陌生。因此這裏花一點點篇幅介紹一下類加載器的部分原理。若是咱們要定義類加載器,須要繼承ClassLoader類,並覆蓋findClass()方法:

@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
    try {
        /*本身另外寫一個getClassData()
                  經過IO流從指定位置讀取xxx.class文件獲得字節數組*/
        byte[] datas = getClassData(name);
        if(datas == null) {
            throw new ClassNotFoundException("類沒有找到:" + name);
        }
        //調用類加載器自己的defineClass()方法,由字節碼獲得Class對象
        return this.defineClass(name, datas, 0, datas.length);
    } catch (IOException e) {
        e.printStackTrace();
        throw new ClassNotFoundException("類找不到:" + name);
    }
}

因此,這就是類加載之因此能把xxx.class文件加載進內存,並建立對應Class對象的深層緣由。具體文章能夠參考基友寫的另外一篇:請叫我程序猿大人:好怕怕的類加載器


小結

靜態代理

代理類CalculatorProxy是咱們事先寫好的,編譯後獲得Proxy.class字節碼文件。隨後和目標類一塊兒被ClassLoader(類加載器)加載進內存,生成Class對象,最後生成實例對象。代理對象中有目標對象的引用,調用同名方法並先後加上日誌打印。

優勢:不用修改目標類源碼

缺點是:高度綁定,不通用。硬編碼,不易於維護。

動態代理

咱們本想經過接口Class直接建立代理實例,無奈的是,接口Class雖然有方法信息描述,卻沒有構造器,沒法建立對象。因此咱們但願JDK能提供一套API,咱們傳入接口Class,它自動複製裏面的方法信息,造出一個有構造器、能建立實例的代理Class對象。

優勢:

  • 不用寫代理類,根據目標對象直接生成代理對象
  • 通知能夠傳入,不是硬編碼
    • *

彩蛋

上面的討論都在刻意迴避代理對象的類型,放最後來聊一聊。

最後討論一下代理對象是什麼類型。

首先,請區分兩個概念:代理Class對象和代理對象。

單從名字看,代理Class和Calculator的接口確實相去甚遠,可是咱們卻能講代理對象賦值給接口類型:

但誰說可否複製給接口是看名字的?難道不是隻要實現接口就好了嗎?

代理對象的本質就是:和目標對象實現相同接口的實例。代理Class能夠叫任何名字,whatever,只要它實現某個接口,就能成爲該接口類型。

我寫了一個MyProxy類,那麼它的Class名字必然叫MyProxy。但這和可否賦值給接口沒有任何關係。因爲它實現了Serializable和Collection,因此myProxy(代理實例)同時是這兩個接口的類型。

我想了個很騷的比喻,但願能解釋清楚:

接口Class對象是大內太監,裏面的方法和字段比作他的一身武藝,可是他沒有小DD(構造器),因此不能new實例。一身武藝後繼無人。

那怎麼辦呢?

正常途徑(implements):

寫一個類,實現該接口。這個就至關於大街上拉了一我的,認他作乾爹。一身武藝傳給他,只是比他乾爹多了小DD,能夠new實例。

非正常途徑(動態代理):

經過妙手聖醫Proxy的克隆大法(Proxy.getProxyClass()),克隆一個Class,可是有小DD。因此這個克隆人Class能夠建立實例,也就是代理對象。

代理Class其實就是附有構造器的接口Class,同樣的類結構信息,卻能建立實例。

JDK動態代理生成的實例

CGLib動態代理生成的實例

若是說繼承的父類是親爹(只有一個),那麼實現的接口是乾爹(能夠有多個)。

實現接口是一個類認乾爹的過程。接口沒法建立對象,但實現該接口的類能夠。

好比

class Student extends Person implements A, B

這個類new一個實例出來,你問它:你爸爸是誰啊?它會告訴你:我只有一個爸爸Person。

可是student instanceof A interface,或者student instanceof B interface,它會告訴你兩個都是它乾爹(true),均可以用來接收它。

然而,凡有利必有弊。

也就是說,動態代理生成的代理對象,最終均可以用接口接收,和目標對象一塊兒造成了多態,能夠隨意切換展現不一樣的功能。可是切換的同時,只能使用該接口定義的方法。

相關文章
相關標籤/搜索