詳解Java動態(tài)代理機制

?????之前介紹的反射和注解都是Java中的動態(tài)特性,還有即將介紹的動態(tài)代理也是Java中的一個動態(tài)特性排监。這些動態(tài)特性使得我們的程序很靈活。動態(tài)代理是面向AOP編程的基礎践樱。通過動態(tài)代理翰灾,我們可以在運行時動態(tài)創(chuàng)建一個類缕粹,實現(xiàn)某些接口中的方法,目前為止該特性已被廣泛應用于各種框架和類庫中纸淮,例如:Spring平斩,Hibernate,MyBatis等萎馅。理解動態(tài)代理是理解框架底層的基礎双戳。
?????主要內(nèi)容如下:

  • 理解代理是何意
  • Java SDK實現(xiàn)動態(tài)代理
  • 第三方庫cglib實現(xiàn)動態(tài)代理

一、代理的概念
?????單從字面上理解糜芳,代理就是指原對象的委托人飒货,它不是原對象但是卻有原對象的權限。Java中的代理意思類似峭竣,就是指通過代理來操作原對象的方法和屬性塘辅,而原對象不直接出現(xiàn)。這樣做有幾點好處:

  • 節(jié)省創(chuàng)建原對象的高開銷皆撩,創(chuàng)建一個代理并不會立馬創(chuàng)建一個實際原對象扣墩,而是保存一個原對象的地址,按需加載
  • 執(zhí)行權限檢查扛吞,保護原對象
這里寫圖片描述

實際上代理堵在了原對象的前面呻惕,在代理的內(nèi)部往往還是調(diào)用了原對象的方法,只是它還做了其他的一些操作滥比。下面看第一種實現(xiàn)動態(tài)代理的方式亚脆。

二、Java SDK實現(xiàn)動態(tài)代理
?????實現(xiàn)動態(tài)代理主要有如下幾個步驟:

  • 實現(xiàn) InvocationHandler接口盲泛,完成自定義調(diào)用處理器
  • 通過Proxy的getProxyClass方法獲取對應的代理類
  • 利用反射技術獲取該代理類的constructor構造器
  • 利用constructor構造代理實例對象

在一步步解析源碼之前,我們先通過一個完整的實例了解下,整個程序的一步步邏輯走向枫振。

//定義了一個調(diào)用處理器
public class MyInvotion implements InvocationHandler {

    private Object realObj;

    public MyInvotion(Object obj){
        this.realObj =  obj;
    }
    
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
        //通過代理執(zhí)行原對象的方法
        return method.invoke(realObj,args);
    }
}
//定義一個接口
public interface MyInterface {
    
    public void sayHello();
}

//該接口的一個實現(xiàn)類,該類就是我們的原對象
public class ClassA implements MyInterface {

    public void sayHello(){
        System.out.println("hello walker");
    }
}
//main 函數(shù)
public static void main(String[] args){
        ClassA a = new ClassA();
        MyInvotion myInvotion = new MyInvotion(a);
        Class myProxy = Proxy.getProxyClass(ClassA.class.getClassLoader(), new Class[]{MyInterface.class});
        Constructor constructor =myProxy.getConstructor(new Class[]{InvocationHandler.class});
        MyInterface m = (MyInterface)constructor.newInstance(myInvotion);
        m.sayHello();
    }
輸出結果:hello walker

簡單說下整體的運行過程屈雄,首先我們創(chuàng)建ClassA 實例并將它傳入自定義的調(diào)用處理器MyInvotion,在MyInvotion中用realObj接受該參數(shù)代表原對象官套。接著調(diào)用Proxy的getProxyClass方法酒奶,將ClassA 的類加載器和ClassA 的實現(xiàn)的接口集合傳入,該方法內(nèi)部會實現(xiàn)所有接口返回該類的代理類虏杰,然后我們利用反射獲取代理類的構造器并創(chuàng)建實例讥蟆。

以上便是整個程序運行的大致流程,接下來我們從源代碼的角度看看具體是如何實現(xiàn)的纺阔。首先我們看InvocationHandler接口瘸彤,這是我們的調(diào)用處理器,在代理類中訪問的所有的方法都會被轉發(fā)到這執(zhí)行笛钝,具體的等我們看了代理類源碼及理解了质况。該接口中唯一的方法是:

public Object invoke(Object proxy, Method method, Object[] args)
  • 參數(shù)Proxy表示動態(tài)生成的代理類的對象,基本沒啥用
  • 參數(shù)method表示當前正在被調(diào)用的方法
  • 數(shù)組args指定了該方法的參數(shù)集合

我們上例中對該接口的實現(xiàn)情況玻靡,定義了一個realObj用于保存原對象的引用结榄。重寫的invoke方法中調(diào)用了原對象realObj的method方法,具體誰來調(diào)用該方法以及傳入的參數(shù)是什么囤捻,在看完代理類源碼即可知曉臼朗。

接下來我們看看最核心的內(nèi)容,如何動態(tài)創(chuàng)建代理類蝎土。這是getProxyClass方法的源碼:

    public static Class<?> getProxyClass(ClassLoader loader,
                                         Class<?>... interfaces)
        throws IllegalArgumentException
    {
        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        return getProxyClass0(loader, intfs);
    }

首先獲取了該類實現(xiàn)的所有的接口的集合视哑,然后判斷創(chuàng)建該代理是否具有安全性問題,檢查接口類對象是否對類裝載器可見等誊涯。然后調(diào)用另外一個getProxyClass0方法挡毅,我們跟進去:

    private static Class<?> getProxyClass0(ClassLoader loader,
                                           Class<?>... interfaces) {
        if (interfaces.length > 65535) {
            throw new IllegalArgumentException("interface limit exceeded");
        }

        // If the proxy class defined by the given loader implementing
        // the given interfaces exists, this will simply return the cached copy;
        // otherwise, it will create the proxy class via the ProxyClassFactory
        return proxyClassCache.get(loader, interfaces);
    }

判斷如果該類的接口超過65535(想必還沒有那么牛的類),拋出異常暴构。在我們的Proxy類中有個屬性proxyClassCache跪呈,這是一個WeakCache類型的靜態(tài)變量。它指示了我們的類加載器和代理類之間的映射取逾。所以proxyClassCache的get方法用于根據(jù)類加載器來獲取Proxy類耗绿,如果已經(jīng)存在則直接從cache中返回,如果沒有則創(chuàng)建一個映射并更新cache表砾隅。具體創(chuàng)建一個Proxy類并存入cache表中的代碼限于能力缭乘,未能參透。

至此我們就獲取到了該ClassA類對應的代理類型,接著我們通過該類的getConstructor方法獲取該代理類的構造器堕绩,并傳入InvocationHandler.class作為參數(shù),至于為何要傳入該類型作為參數(shù)邑时,等會看代理類源碼變一目了然了奴紧。

最后newInstance創(chuàng)建該代理類的實例,實現(xiàn)對ClassA對象的代理晶丘。

可能看完上述的介紹黍氮,你還會有點暈。下面我們通過查看動態(tài)生成的代理類的源碼來加深理解浅浮。上述getProxyClass方法會動態(tài)創(chuàng)建一個代理類并返回他的Class類型沫浆,這個代理類一般被命名為$ProxyN,這個N是遞增的用于標記不同的代理類滚秩。我們可以利用反編譯工具反編譯該class:

final class $Proxy0 extends Proxy implements MyInterface {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

    public $Proxy0(InvocationHandler paramInvocationHandler) {
        super(paramInvocationHandler);
    }

    public final boolean equals(Object paramObject) {
        return ((Boolean) this.h.invoke(this, m1, 
                new Object[] { paramObject })).booleanValue();
    }

    public final void sayHello() {
        this.h.invoke(this, m3, null);
    }

    public final String toString() {
        return (String) this.h.invoke(this, m2, null);
    }

    public final int hashCode() {
        return ((Integer) this.h.invoke(this, m0, null)).intValue();
    }

    static {
        m1 = Class.forName("java.lang.Object").getMethod("equals",
                new Class[] { Class.forName("java.lang.Object") });
        m3 = Class.forName("laoma.demo.proxy.SimpleJDKDynamicProxyDemo$IService")
                .getMethod("sayHello",new Class[0]);
        m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
        m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
    }
}

這就是上述的ClassA的動態(tài)代理類专执,我們看到該類的構造方法傳入?yún)?shù)InvocationHandler類型,并調(diào)用了父類Proxy的構造方法保存了這個InvocationHandler實例郁油,這也解釋了我們?yōu)槭裁丛讷@取構造器的時候需要指定參數(shù)類型為InvocationHandler本股,就是因為動態(tài)代理類只有一個構造器并且參數(shù)類型為InvocationHandler。

接著我們看其中的方法桐腌,貌似只有一個sayHello是我們知道的拄显,別的方法哪來的?我們說過在動態(tài)創(chuàng)建代理類的時候案站,會實現(xiàn)原對象的所有接口躬审。所以sayHello方法是實現(xiàn)的MyInterface。而其余的四個方法是代理類由于比較常用蟆盐,被默認添加到其中承边。而這些方法的內(nèi)部都是調(diào)用的this.h.invoke這個方法,this.h就是保存在父類Proxy中的InvocationHandler實例(我們用構造器向其中保存的)舱禽,調(diào)用了這個類的invoke方法炒刁,在我們自定義的InvocationHandler實例中重寫了invoke方法,我們寫的比較簡單誊稚,直接執(zhí)行傳入的method翔始。

也就是我們調(diào)用代理類的任何一個方法都會轉發(fā)到該InvocationHandler實例中的involve中,因為該實例中保存有我們的原對象里伯,所以我們可以選擇直接調(diào)取原對象中的方法作為回調(diào)城瞎。

以上便是有關Java SDK中動態(tài)代理的相關內(nèi)容,稍微總結下疾瓮,首先我們通過實現(xiàn)InvocationHandler自定義一個調(diào)用處理類脖镀,該類中會保存我們的原對象,并提供一個invoke方法供代理類使用狼电。然后我們通過getProxyClass方法動態(tài)創(chuàng)建代理類蜒灰,最后用反射獲取代理類的實例對象弦蹂。

需要注意的是:以上我們使用的四步創(chuàng)建代理實例時最根本的,其實Proxy中提供一個方法可以封裝2到4步的操作强窖。上述代碼也可以這么寫:

ClassA a = new ClassA();
MyInterface aProxy = (MyInterface)Proxy.newProxyInstance(ClassA.class.getClassLoader(),new Class<?>[]{MyInterface.class},new MyInvotion(a));
aProxy.sayHello();

我們打開該方法的內(nèi)部源碼凸椿,其實走的還是我們上述的過程,它就是做了封裝翅溺。

public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
{

    Objects.requireNonNull(h);
        
        //獲取所有接口
        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }
        
        //創(chuàng)建動態(tài)代理類
        Class<?> cl = getProxyClass0(loader, intfs);

        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }

            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            return cons.newInstance(new Object[]{h});
    .............
    ..............
}

二脑漫、第三方庫cglib實現(xiàn)動態(tài)代理
?????使用動態(tài)代理,我們編寫通用的代碼邏輯咙崎,即僅實現(xiàn)一個InvocationHandler實例完成對多個類型的代理优幸。但是我們從動態(tài)生成的代理類的源碼可以看到,所有的代理類都繼承自Proxy這個類褪猛,這就導致我們這種方式不能代理類网杆,只能代理接口。因為java中是單繼承的握爷。也就是說跛璧,給我們一個類型,我們只能動態(tài)實現(xiàn)該類所有的接口類型新啼,但是該類繼承的別的類我們在代理類中是不能使用的追城,因為它沒有被代理類繼承。下面看個例子:

public class ClassB {
    public void welcome(){
        System.out.println("welcom walker");
    }
}
public interface MyInterface {

    public void sayHello();
}

//需要被代理的原類型燥撞,繼承了ClassB和接口MyInterface 
public class ClassA extends ClassB implements MyInterface {

    public void sayHello(){
        System.out.println("hello walker");
    }
}
//InvocationHandler 實例
public class MyInvotion implements InvocationHandler {

private Object realObj;

public MyInvotion(Object obj){
     this.realObj =  obj;
}

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
        return method.invoke(realObj,args);
    }
}

我們反編譯該代理類座柱,和上述的源碼是一樣的,此處不再重復貼出物舒。我們能從中看出來的是色洞,我們的代理只會實現(xiàn)原類型中所有的接口,至于原類型所繼承的類冠胯,在生成Proxy代理類的時候會丟棄火诸,因為所有的代理類必須繼承Proxy類,這就導致原類型的父類中的方法 在代理類中丟失荠察。這是該種方式的一大弊端置蜀。下面我們看看另一種方式實現(xiàn)動態(tài)代理,該種方式完美解決了這種不足悉盆。

限于篇幅盯荤,我們下篇介紹cglib實現(xiàn)動態(tài)代理機制的內(nèi)容,本篇暫時結束焕盟,總結的不好秋秤,望海涵。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市灼卢,隨后出現(xiàn)的幾起案子绍哎,更是在濱河造成了極大的恐慌,老刑警劉巖芥玉,帶你破解...
    沈念sama閱讀 222,464評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蛇摸,死亡現(xiàn)場離奇詭異,居然都是意外死亡灿巧,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評論 3 399
  • 文/潘曉璐 我一進店門揽涮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抠藕,“玉大人,你說我怎么就攤上這事蒋困《芩疲” “怎么了?”我有些...
    開封第一講書人閱讀 169,078評論 0 362
  • 文/不壞的土叔 我叫張陵雪标,是天一觀的道長零院。 經(jīng)常有香客問我,道長村刨,這世上最難降的妖魔是什么告抄? 我笑而不...
    開封第一講書人閱讀 59,979評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮嵌牺,結果婚禮上打洼,老公的妹妹穿的比我還像新娘。我一直安慰自己逆粹,他們只是感情好募疮,可當我...
    茶點故事閱讀 69,001評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著僻弹,像睡著了一般阿浓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蹋绽,一...
    開封第一講書人閱讀 52,584評論 1 312
  • 那天芭毙,我揣著相機與錄音,去河邊找鬼蟋字。 笑死稿蹲,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的鹊奖。 我是一名探鬼主播苛聘,決...
    沈念sama閱讀 41,085評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了设哗?” 一聲冷哼從身側響起唱捣,我...
    開封第一講書人閱讀 40,023評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎网梢,沒想到半個月后震缭,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,555評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡战虏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,626評論 3 342
  • 正文 我和宋清朗相戀三年拣宰,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片烦感。...
    茶點故事閱讀 40,769評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡巡社,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出手趣,到底是詐尸還是另有隱情晌该,我是刑警寧澤,帶...
    沈念sama閱讀 36,439評論 5 351
  • 正文 年R本政府宣布绿渣,位于F島的核電站朝群,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏中符。R本人自食惡果不足惜姜胖,卻給世界環(huán)境...
    茶點故事閱讀 42,115評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望舟茶。 院中可真熱鬧谭期,春花似錦、人聲如沸吧凉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽阀捅。三九已至胀瞪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間饲鄙,已是汗流浹背凄诞。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留忍级,地道東北人帆谍。 一個月前我還...
    沈念sama閱讀 49,191評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像轴咱,于是被迫代替她去往敵國和親汛蝙。 傳聞我的和親對象是個殘疾皇子烈涮,可洞房花燭夜當晚...
    茶點故事閱讀 45,781評論 2 361

推薦閱讀更多精彩內(nèi)容