java | 什么是動(dòng)態(tài)代理破婆?

最近在復(fù)習(xí) Java 相關(guān)涮总,回顧了下代理模式。代理模式在 Java 領(lǐng)域很多地方都有應(yīng)用祷舀,它分為靜態(tài)代理和動(dòng)態(tài)代理瀑梗,其中 Spring AOP 就是動(dòng)態(tài)代理的典型例子。動(dòng)態(tài)代理又分為接口代理和 cglib (子類代理)裳扯,結(jié)合我的理解寫了幾個(gè) demo 分享給你們抛丽,這是昨晚修仙到 3 點(diǎn)寫出來(lái)的文章,不點(diǎn)在看饰豺,我覺得說(shuō)不過(guò)去了亿鲜。

代理模式在我們?nèi)粘V泻艹R姡钐幪幱写恚?/p>

  • 看張學(xué)友的演唱會(huì)很難搶票哟忍,可以找黃牛排隊(duì)買
  • 嫌出去吃飯麻煩狡门,可以叫外賣

無(wú)論是黃牛自点、外賣騎手都得幫我們干活。但是他們不能一手包辦(比如黃牛不能幫我吃飯)赵抢,他們只能做我們不能或者不想做的事嫩舟。

  • 找黃牛可以幫我排隊(duì)買上張學(xué)友的演唱會(huì)門票
  • 外賣騎手可以幫我把飯送到樓下

所以叛复,你看仔引。代理模式其實(shí)就是當(dāng)前對(duì)象不愿意做的事情,委托給別的對(duì)象做褐奥。

靜態(tài)代理

我還是以找黃牛幫我排隊(duì)買張學(xué)友的演唱會(huì)門票的例子咖耘,寫個(gè) demo 說(shuō)明。現(xiàn)在有一個(gè) Human 接口撬码,無(wú)論是我還是黃牛都實(shí)現(xiàn)了這個(gè)接口儿倒。

public interface Human {

    void eat();

    void sleep();

    void lookConcert();

}

例如,我這個(gè)類呜笑,我會(huì)吃飯和睡覺夫否,如以下類:

public class Me implements Human{

    @Override
    public void eat() {
        System.out.println("eat emat ....");
    }

    @Override
    public void sleep() {
        System.out.println("Go to bed at one o'clock in the morning");
    }

    @Override
    public void lookConcert() {
        System.out.println("Listen to Jacky Cheung's Concert");
    }

}

有黃牛類,例如:

public class Me implements Human{

    @Override
    public void eat() {
    }

    @Override
    public void sleep() {
    }

    @Override
    public void lookConcert() {
    }

}

現(xiàn)在我和黃牛都已經(jīng)準(zhǔn)備好了叫胁,怎么把這二者關(guān)聯(lián)起來(lái)呢凰慈?我們要明確的是黃牛是要幫我買票的,買票必然就需要幫我排隊(duì)驼鹅,于是有以下黃牛類:注意這里我們不關(guān)心微谓,黃牛的其他行為,我們只關(guān)心他能不能排隊(duì)買票输钩。

public class HuangNiu implements Human{

    private Me me;

    public HuangNiu() {
        me = new Me();
    }

    @Override
    public void eat() {
    }

    @Override
    public void sleep() {
    }

    @Override
    public void lookConcert() {
        // 添加排隊(duì)買票方法
        this.lineUp();
        me.lookConcert();
    }

    public void lineUp() {

        System.out.println("line up");

    }

}

最終的 main 方法調(diào)用如下:

public class Client {

    public static void main(String[] args) {

        Human human = new HuangNiu();
        human.lookConcert();

    }

}

結(jié)果如下:

靜態(tài)代理結(jié)果

由此可見豺型,黃牛就只是做了我們不愿意做的事(排隊(duì)買票),實(shí)際看演唱會(huì)的人還是我张足〈ゴ矗客戶端也并不關(guān)心代理類代理了哪個(gè)類,因?yàn)榇a控制了客戶端對(duì)委托類的訪問(wèn)为牍『甙螅客戶端代碼表現(xiàn)為 Human human = new HuangNiu();

由于代理類實(shí)現(xiàn)了抽象角色的接口,導(dǎo)致代理類無(wú)法通用碉咆。比如抖韩,我的狗病了,想去看醫(yī)生疫铜,但是排隊(duì)掛號(hào)很麻煩茂浮,我也想有個(gè)黃牛幫我的排隊(duì)掛號(hào)看病,但是黃牛它不懂這只狗的特性(黃牛跟狗不是同一類型,黃牛屬于 Human 但狗屬于 Animal 類)但排隊(duì)掛號(hào)和排隊(duì)買票相對(duì)于黃牛來(lái)說(shuō)它兩就是一件事席揽,這個(gè)方法是不變的顽馋,現(xiàn)場(chǎng)排隊(duì)。那我們能不能找一個(gè)代理說(shuō)既可以幫人排隊(duì)買票也可以幫狗排隊(duì)掛號(hào)呢幌羞?

答案肯定是可以的寸谜,可以用動(dòng)態(tài)代理。

基于接口的動(dòng)態(tài)代理

如靜態(tài)代理的內(nèi)容所描述的属桦,靜態(tài)代理受限于接口的實(shí)現(xiàn)熊痴。動(dòng)態(tài)代理就是通過(guò)使用反射,動(dòng)態(tài)地獲取抽象接口的類型聂宾,從而獲取相關(guān)特性進(jìn)行代理果善。因動(dòng)態(tài)代理能夠?yàn)樗械奈蟹竭M(jìn)行代理,因此給代理類起個(gè)通用點(diǎn)的名字 HuangNiuHandle系谐。先看黃牛類可以變成什么樣巾陕?

public class HuangNiuHandle implements InvocationHandler {

    private Object proxyTarget;

    public Object getProxyInstance(Object target) {
        this.proxyTarget = target;
        return Proxy.newProxyInstance(proxyTarget.getClass().getClassLoader(), proxyTarget.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        Object methodObject = null;

        System.out.println("line up");
        methodObject = method.invoke(proxyTarget, args);
        System.out.println("go home and sleep");

        return methodObject;
    }

}

這個(gè)時(shí)候的客戶端代碼就變成這樣了

public class Client {

    public static void main(String[] args) {

        HuangNiuHandle huangNiuHandle = new HuangNiuHandle();
        Human human = (Human) huangNiuHandle.getProxyInstance(new Me());

        human.eat();
        human.run();
        human.lookConcert();

        System.out.println("------------------");

        Animal animal = (Animal) huangNiuHandle.getProxyInstance(new Dog());
        animal.eat();
        animal.run();
        animal.seeADoctor();
    }

}

使用動(dòng)態(tài)代理有三個(gè)要點(diǎn),

  1. 必須實(shí)現(xiàn) InvocationHandler 接口纪他,表明該類是一個(gè)動(dòng)態(tài)代理執(zhí)行類惜论。

  2. InvocationHandler 接口內(nèi)有一實(shí)現(xiàn)方法如下: public Object invoke(Object proxy, Method method, Object[] args) 。使用時(shí)需要重寫這個(gè)方法

  3. 獲取代理類止喷,需要使用 Proxy.newProxyInstance(Clas loader, Class<?>[] interfaces, InvocationHandler h) 這個(gè)方法去獲取Proxy對(duì)象(Proxy 類類型的實(shí)例)。

注意到 Proxy.newProxyInstance 這個(gè)方法混聊,它需要傳入 3 個(gè)參數(shù)弹谁。解析如下:

// 第一個(gè)參數(shù),是類的加載器
// 第二個(gè)參數(shù)是委托類的接口類型句喜,證代理類返回的是同一個(gè)實(shí)現(xiàn)接口下的類型预愤,保持代理類與抽象角色行為的一致
// 第三個(gè)參數(shù)就是代理類本身,即告訴代理類咳胃,代理類遇到某個(gè)委托類的方法時(shí)該調(diào)用哪個(gè)類下的invoke方法
Proxy.newProxyInstance(Class loader, Class<?>[] interfaces, InvocationHandler h)

再來(lái)看看 invoke 方法植康,用戶調(diào)用代理對(duì)象的什么方法,實(shí)質(zhì)上都是在調(diào)用處理器的
invoke 方法展懈,通過(guò)該方法調(diào)用目標(biāo)方法销睁,它也有三個(gè)參數(shù):

// 第一個(gè)參數(shù)為 Proxy 類類型實(shí)例,如匿名的 $proxy 實(shí)例
// 第二個(gè)參數(shù)為委托類的方法對(duì)象
// 第三個(gè)參數(shù)為委托類的方法參數(shù)
// 返回類型為委托類某個(gè)方法的執(zhí)行結(jié)果
public Object invoke(Object proxy, Method method, Object[] args)

調(diào)用該代理類之后的輸出結(jié)果:

動(dòng)態(tài)代理

由結(jié)果可知存崖,黃牛不僅幫了(代理)我排隊(duì)買票冻记,還幫了(代理)我的狗排隊(duì)掛號(hào)。所以来惧,你看靜態(tài)代理需要自己寫代理類(代理類需要實(shí)現(xiàn)與目標(biāo)對(duì)象相同的接口)冗栗,還需要一一實(shí)現(xiàn)接口方法,但動(dòng)態(tài)代理不需要。

注意隅居,我們并不是所有的方法都需要黃牛這個(gè)代理去排隊(duì)钠至。我們知道只有我看演唱會(huì)和我的狗去看醫(yī)生時(shí),才需要黃牛胎源,如果要實(shí)現(xiàn)我們想要的方法上面添加特定的代理棉钧,可以通過(guò) invoke 方法里面的方法反射獲取 method 對(duì)象方法名稱即可實(shí)現(xiàn),所以動(dòng)態(tài)代理類可以變成這樣:

public class HuangNiuHandle implements InvocationHandler {

    private Object proxyTarget;

    public Object getProxyInstance(Object target) {
        this.proxyTarget = target;
        return Proxy.newProxyInstance(proxyTarget.getClass().getClassLoader(), proxyTarget.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        Object methodObject = null;

        if ("lookConcert".equals(method.getName()) ||
        "seeADoctor".equals(method.getName())) {

            System.out.println("line up");
            // 調(diào)用目標(biāo)方法
            methodObject = method.invoke(proxyTarget, args);
        } else {
            // 不使用第一個(gè)proxy參數(shù)作為參數(shù)乒融,否則會(huì)造成死循環(huán)
            methodObject = method.invoke(proxyTarget, args);
        }

        return methodObject;
    }

}

結(jié)果如下:可以看到我們只在特定方法求助了黃牛

動(dòng)態(tài)代理

由此可見掰盘,動(dòng)態(tài)代理一般應(yīng)用在記錄日志等橫向業(yè)務(wù)。

值得注意的是:

  1. 基于接口類的動(dòng)態(tài)代理模式赞季,必須具備抽象角色愧捕、委托類、代理三個(gè)基本角色申钩。委托類和代理類必須由抽象角色衍生出來(lái)次绘,否則無(wú)法使用該模式。

  2. 動(dòng)態(tài)代理模式最后返回的是具有抽象角色(頂層接口)的對(duì)象撒遣。在委托類內(nèi)被 private 或者 protected 關(guān)鍵修飾的方法將不會(huì)予以調(diào)用邮偎,即使允許調(diào)用。也無(wú)法在客戶端使用代理類轉(zhuǎn)換成子類接口义黎,對(duì)方法進(jìn)行調(diào)用禾进。也就是說(shuō)上述的動(dòng)態(tài)代理返回的是委托類(Me)或 (Dog)的就接口對(duì)象 (Human)或 (Animal)。

  3. 在 invoke 方法內(nèi)為什么不使用第一個(gè)參數(shù)進(jìn)行執(zhí)行回調(diào)廉涕。在客戶端使用getProxyInstance(new Child( ))時(shí)泻云,JDK 會(huì)返回一個(gè) proxy 的實(shí)例,實(shí)例內(nèi)有InvokecationHandler 對(duì)象及動(dòng)態(tài)繼承下來(lái)的目標(biāo) 狐蜕〕璐浚客戶端調(diào)用了目標(biāo)方法,有如下操作:首先 JDK 先查找 proxy 實(shí)例內(nèi)的 handler 對(duì)象 然后執(zhí)行 handler 內(nèi)的 invoke 方法层释。

根據(jù) public Object invoke 這個(gè)方法第一個(gè)參數(shù) proxy 就是對(duì)應(yīng)著 proxy 實(shí)例婆瓜。如果在 invoke 內(nèi)使用 method.invoke(proxy,args) ,會(huì)出現(xiàn)這樣一條方法鏈,目標(biāo)方法→invoke→目標(biāo)方法→invoke...贡羔,最終導(dǎo)致堆棧溢出廉白。

基于子類的動(dòng)態(tài)代理

為了省事,我這里并沒(méi)有繼承父類治力,但在實(shí)際開發(fā)中是需要繼承父類才比較方便擴(kuò)展的蒙秒。與基于接口實(shí)現(xiàn)類不同的是:

  1. CGLib (基于子類的動(dòng)態(tài)代理)使用的是方法攔截器 MethodInterceptor ,需要導(dǎo)入 cglib.jar 和 asm.jar 包
  2. 基于子類的動(dòng)態(tài)代理宵统,返回的是子類對(duì)象
  3. 方法攔截器對(duì) protected 修飾的方法可以進(jìn)行調(diào)用

代碼如下:

public class Me {

    public void eat() {
        System.out.println("eat meat ....");
    }

    public void run() {
        System.out.println("I run with two legs");
    }

    public void lookConcert() {
        System.out.println("Listen to Jacky Cheung's Concert");
    }

    protected void sleep() {
        System.out.println("Go to bed at one o'clock in the morning");
    }

}

Dog 類

public class Dog {

    public void eat() {
        System.out.println("eat Dog food ....");
    }

    public void run() {
        System.out.println("Dog running with four legs");
    }

    public void seeADoctor() {
        System.out.println("The dog go to the hospital");
    }

}

黃牛代理類晕讲,注意 invoke() 這里多了一個(gè)參數(shù) methodProxy 覆获,它的作用是用于執(zhí)行目標(biāo)(委托類)的方法,至于為什么用 methodProxy 瓢省,官方的解釋是速度快且在intercep t內(nèi)調(diào)用委托類方法時(shí)不用保存委托對(duì)象引用弄息。

public class HuangNiuHandle implements MethodInterceptor {

    private Object proxyTarget;

    public Object getProxyInstance(Object target) {
        this.proxyTarget = target;
        return Enhancer.create(target.getClass(), target.getClass().getInterfaces(), this);
    }

    @Override
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

        Object methodObject = null;

        if ("lookConcert".equals(method.getName()) ||
                "seeADoctor".equals(method.getName())) {
            System.out.println("line up");
            // 調(diào)用目標(biāo)方法
            methodObject = methodProxy.invokeSuper(proxy, args);
        } else {
            methodObject = method.invoke(proxyTarget, args);
        }

        return methodObject;
    }
}

client 類

public class Client {

    public static void main(String[] args) {
        HuangNiuHandle huangNiuHandle = new HuangNiuHandle();
        Me me = (Me) huangNiuHandle.getProxyInstance(new Me());

        me.eat();
        me.run();
        me.sleep();
        me.lookConcert();

        System.out.println("------------------");

        Dog dog = (Dog) huangNiuHandle.getProxyInstance(new Dog());
        dog.eat();
        dog.run();
        dog.seeADoctor();
    }
}

結(jié)果:

基于子類的動(dòng)態(tài)代理

注意到 Me 類中被 protected 修飾的方法 sleep 仍然可以被客戶端調(diào)用。這在基于接口的動(dòng)態(tài)代理中是不被允許的勤婚。

靜態(tài)代理與動(dòng)態(tài)代理的區(qū)別

靜態(tài)代理需要自己寫代理類并一一實(shí)現(xiàn)目標(biāo)方法摹量,且代理類必須實(shí)現(xiàn)與目標(biāo)對(duì)象相同的接口。

動(dòng)態(tài)代理不需要自己實(shí)現(xiàn)代理類馒胆,它是利用 JDKAPI缨称,動(dòng)態(tài)地在內(nèi)存中構(gòu)建代理對(duì)象(需要我們傳入被代理類),并且默認(rèn)實(shí)現(xiàn)所有目標(biāo)方法祝迂。

源碼下載:https://github.com/turoDog/review_java.git

后語(yǔ)

如果本文對(duì)你哪怕有一丁點(diǎn)幫助睦尽,請(qǐng)幫忙點(diǎn)好看,你的好看是我堅(jiān)持寫作的動(dòng)力型雳。關(guān)注公眾號(hào)一個(gè)優(yōu)秀的廢人回復(fù) 1024 獲取資料:Python当凡、C++、Java纠俭、Linux沿量、Go、前端冤荆、算法資料分享

一個(gè)優(yōu)秀的廢人朴则,給你講幾斤技術(shù)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末钓简,一起剝皮案震驚了整個(gè)濱河市佛掖,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌涌庭,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件欧宜,死亡現(xiàn)場(chǎng)離奇詭異坐榆,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)冗茸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門席镀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人夏漱,你說(shuō)我怎么就攤上這事豪诲。” “怎么了挂绰?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵屎篱,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng)交播,這世上最難降的妖魔是什么重虑? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮秦士,結(jié)果婚禮上缺厉,老公的妹妹穿的比我還像新娘。我一直安慰自己隧土,他們只是感情好提针,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著曹傀,像睡著了一般辐脖。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上卖毁,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天揖曾,我揣著相機(jī)與錄音,去河邊找鬼亥啦。 笑死炭剪,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的翔脱。 我是一名探鬼主播奴拦,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼届吁!你這毒婦竟也來(lái)了错妖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤疚沐,失蹤者是張志新(化名)和其女友劉穎暂氯,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體亮蛔,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡痴施,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了究流。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片辣吃。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖芬探,靈堂內(nèi)的尸體忽然破棺而出神得,到底是詐尸還是另有隱情,我是刑警寧澤偷仿,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布哩簿,位于F島的核電站宵蕉,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏卡骂。R本人自食惡果不足惜国裳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望全跨。 院中可真熱鬧缝左,春花似錦、人聲如沸浓若。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)挪钓。三九已至是越,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間碌上,已是汗流浹背倚评。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留馏予,地道東北人天梧。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像霞丧,于是被迫代替她去往敵國(guó)和親呢岗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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