深入分析JAVA動態(tài)代理

1漫仆、為什么要動態(tài)代理

動態(tài)代理的作用其實就是在不修改原代碼的前提下,對已有的方法進行增強躺孝。
關(guān)鍵點:

不修改原來已有的代碼(滿足設(shè)計模式的要求)

對已有方法進行增強

2瓮钥、舉個栗子

我們用一個很簡單的例子來說明:Hello類,有一個introduction方法湖苞。

現(xiàn)在我們的需求就是不修改Hello類的introduction方法拯欧,在introduction之前先sayHello,在introduction之后再sayGoodBye

3袒啼、實現(xiàn)方式

JAVA中哈扮,實現(xiàn)動態(tài)代理有兩種方式,一種是JDK提供的蚓再,一種是第三方庫CgLib提供的滑肉。特點如下:

JDK動態(tài)代理:被代理的目標類需要實現(xiàn)接口

CgLib方式:可以對任意類實現(xiàn)動態(tài)代理

3.1、JDK動態(tài)代理
JDK動態(tài)代理需要實現(xiàn)接口摘仅,然后通過對接口方法的增強來實現(xiàn)動態(tài)代理

所以要使用JDK動態(tài)代理的話靶庙,我們首先要創(chuàng)建一個接口,并且被代理的方法要在這個接口里面

3.1.1娃属、創(chuàng)建一個接口
我們創(chuàng)建一個接口如下:

Personal.java

public interface Personal {

    /**
     * 被代理的方法
     */
    void introduction();

}

3.1.2六荒、實現(xiàn)接口
創(chuàng)建接口實現(xiàn)類护姆,并且完成introduction方法

PersonalImpl.java

public class PersonalImpl implements Personal {
    @Override
    public void introduction() {
        System.out.println("我是程序員!");
    }
}

3.1.3掏击、創(chuàng)建代理類
JDK代理的關(guān)鍵就是這個代理類了卵皂,需要實現(xiàn)InvocationHandler

在代理類中,所有方法的調(diào)用都好分發(fā)到invoke方法中砚亭。我們在invoke方法完成對方法的增強即可

JDKProxyFactory.java

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class JDKProxyFactory<T> implements InvocationHandler {

    /**
     * 目標對象
     */
    private T target;

    /**
     * 構(gòu)造函數(shù)傳入目標對象
     *
     * @param target 目標對象
     */
    public JDKProxyFactory(T target) {
        this.target = target;
    }

    /**
     * 獲取代理對象
     *
     * @return 獲取代理
     */
    public T getProxy() {
        return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 對方法增強
        System.out.println("大家好灯变!");
        // 調(diào)用原方法
        Object result = method.invoke(target, args);
        // 方法增強
        System.out.println("再見!");
        return result;
    }
}

就這樣捅膘,JDK動態(tài)代理的代碼就完成了添祸,接下來寫一份測試代碼

3.1.4、編寫測試代碼
為了方便測試寻仗,我們編寫一個test方法

同時為了查看class文件刃泌,還添加了一個generatorClass方法,這個方法可以將動態(tài)代理生成的.class輸出到文件

ProxyTest.java

import org.junit.Test;
import sun.misc.ProxyGenerator;

import java.io.FileOutputStream;
import java.io.IOException;

public class ProxyTest {

    @Test
    public void testJdkProxy() {
        // 生成目標對象
        Personal personal = new PersonalImpl();
        // 獲取代理對象
        JDKProxyFactory<Personal> proxyFactory = new JDKProxyFactory<>(personal);
        Personal proxy = proxyFactory.getProxy();

        // 將proxy的class字節(jié)碼輸出到文件
        generatorClass(proxy);

        // 調(diào)用代理對象
        proxy.introduction();
    }

    /**
     * 將對象的class字節(jié)碼輸出到文件
     *
     * @param proxy 代理類
     */
    private void generatorClass(Object proxy) {
        FileOutputStream out = null;
        try {
            byte[] generateProxyClass = ProxyGenerator.generateProxyClass(proxy.getClass().getSimpleName(), new Class[]{proxy.getClass()});
            out = new FileOutputStream(proxy.getClass().getSimpleName() + ".class");
            out.write(generateProxyClass);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                }
            }
        }

    }

}

3.1.5署尤、查看運行結(jié)果
可以看到耙替,運行test方法之后,控制臺打印出如下:

大家好沐寺!
我是程序員林艘!
再見!

我們在introduction方法前和后都成功增加了功能混坞,讓這個程序員的自我介紹瞬間變得更加有禮貌了。

3.1.6钢坦、探探動態(tài)代理的秘密
動態(tài)代理的代碼并不多究孕,那么JDK底層是怎么幫我們實現(xiàn)的呢?

在測試的時候我們將動態(tài)生成的代理類的class字節(jié)碼輸出到了文件爹凹,我們可以反編譯看看厨诸。

結(jié)果有點長,就不全部貼出來了禾酱,不過我們可以看到微酬,里面有一個introduction方法如下:

/**
* the invocation handler for this proxy instance.
* @serial
*/
protected InvocationHandler h;

protected Proxy(InvocationHandler h) {
    Objects.requireNonNull(h);
    this.h = h;
}

public final void introduction() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

原來,生成的代理對象里面颤陶,引用了我們的InvocationHandler颗管,然后在將introduction方法里面調(diào)用了InvocationHandler的introduction,而InvocationHandler是由我們編寫的代理類滓走,在這里我們增加了sayHello和sayGoodBye操作垦江,然后還調(diào)用了原對象的introduction方法,就這樣完成了動態(tài)代理搅方。

3.2比吭、CgLib動態(tài)代理
CgLib動態(tài)

3.2.1绽族、創(chuàng)建被代理對象
由于CgLib不需要實現(xiàn)接口,所以我們不需要創(chuàng)建接口文件了(當然衩藤,你要有接口也沒有問題)

直接創(chuàng)建目標類吧慢,實現(xiàn)introduction方法

PersonalImpl.java

public class PersonalImpl {
    public void introduction() {
        System.out.println("我是程序員!");
    }
}

3.2.2赏表、創(chuàng)建代理類
同樣娄蔼,我們也需要創(chuàng)建代理類,并且在這里實現(xiàn)增強的邏輯底哗,這次我們不是實現(xiàn)InvocationHandler接口了岁诉,而是實現(xiàn)CgLib提供的接口MethodInterceptor,都是類似的跋选,MethodInterceptor中涕癣,全部方法調(diào)用都會交給intercept處理,我們在intercept添加處理邏輯即可前标。

CgLibProxyFactory.java

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CgLibProxyFactory<T> implements MethodInterceptor {

    /**
     * 獲取代理對象
     *
     * @param tClass 被代理的目標對象
     * @return 代理對象
     */
    public T getProxyByCgLib(Class<T> tClass) {
        // 創(chuàng)建增強器
        Enhancer enhancer = new Enhancer();

        // 設(shè)置需要增強的類的類對象
        enhancer.setSuperclass(tClass);

        // 設(shè)置回調(diào)函數(shù)
        enhancer.setCallback(this);

        // 獲取增強之后的代理對象
        return (T) enhancer.create();
    }

     /**
     *  代理類方法調(diào)用回調(diào)
     *  
     * @param obj 這是代理對象坠韩,也就是[目標對象]的子類
     * @param method [目標對象]的方法
     * @param args 參數(shù)
     * @param proxy 代理對象的方法
     * @return 返回結(jié)果,返回給調(diào)用者
     * @throws Throwable
     */
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {

        System.out.println("大家好炼列!");

        Object result = proxy.invokeSuper(obj, args);

        System.out.println("再見只搁!");

        return result;
    }
}

3.2.3、編寫測試代碼
在剛才的測試方法中俭尖,我們添加一個cglib的測試方法:

@Test
public void testCgLibProxy() {
    // 生成被代理的目標對象
    PersonalImpl personal = new PersonalImpl();

    // 獲取代理類
    CgLibProxyFactory<PersonalImpl> proxyFactory = new CgLibProxyFactory<>();
    PersonalImpl proxy = proxyFactory.getProxyByCgLib((Class<PersonalImpl>) personal.getClass());

    // 將proxy的class字節(jié)碼輸出到文件
    generatorClass(proxy);

    // 調(diào)用代理對象
    proxy.introduction();
}

3.2.4氢惋、查看運行結(jié)果
運行測試用例,可以看到跟JDK的實現(xiàn)一樣的效果

大家好稽犁!
我是程序員焰望!
再見!

3.2.5已亥、探探動態(tài)代理的秘密
跟JDK的測試一樣熊赖,我們也來看看生成的class文件

public final void introduction() throws  {
    try {
        super.h.invoke(this, m7, (Object[])null);
    } catch (RuntimeException | Error var2) {
        throw var2;
    } catch (Throwable var3) {
        throw new UndeclaredThrowableException(var3);
    }
}

可以發(fā)現(xiàn),與JDK的動態(tài)代理并沒有區(qū)別虑椎。

4震鹉、如何選擇

既然有兩種實現(xiàn)方式,那么到底應該怎么選擇呢捆姜?

就兩個原則:

目標類有接口實現(xiàn)的传趾,JDK和CgLib都可以選擇,你開心就好

目標類沒有實現(xiàn)任何接口娇未,那只能用CgLib了

5墨缘、后記

其實在第一次看到動態(tài)代理的時候,我就想不明白,我們都把目標類new出來了镊讼,為什么還要將目標類丟給代理類呢宽涌?為什么不直接調(diào)用目標類對應的方法呢?

后來才發(fā)現(xiàn)蝶棋,原來我沒搞清楚動態(tài)代理的使用場景卸亮,場景很清晰,就是:

不修改原來已有的代碼(滿足設(shè)計模式的要求)

對已有方法進行增強

關(guān)鍵是增強玩裙,代理類里面我們是可以添加很多處理邏輯的兼贸,從而實現(xiàn)增強效果。就像黃牛搶票比我們厲害些一樣吃溅。
歡迎關(guān)注
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末溶诞,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子决侈,更是在濱河造成了極大的恐慌螺垢,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赖歌,死亡現(xiàn)場離奇詭異枉圃,居然都是意外死亡,警方通過查閱死者的電腦和手機庐冯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進店門孽亲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人展父,你說我怎么就攤上這事返劲。” “怎么了犯祠?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵旭等,是天一觀的道長。 經(jīng)常有香客問我衡载,道長,這世上最難降的妖魔是什么隙袁? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任痰娱,我火速辦了婚禮,結(jié)果婚禮上菩收,老公的妹妹穿的比我還像新娘梨睁。我一直安慰自己,他們只是感情好娜饵,可當我...
    茶點故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布坡贺。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪遍坟。 梳的紋絲不亂的頭發(fā)上拳亿,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天,我揣著相機與錄音愿伴,去河邊找鬼肺魁。 笑死,一個胖子當著我的面吹牛隔节,可吹牛的內(nèi)容都是我干的鹅经。 我是一名探鬼主播,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼怎诫,長吁一口氣:“原來是場噩夢啊……” “哼瘾晃!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起幻妓,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蹦误,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后涌哲,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胖缤,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年阀圾,在試婚紗的時候發(fā)現(xiàn)自己被綠了哪廓。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,932評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡初烘,死狀恐怖涡真,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情肾筐,我是刑警寧澤哆料,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站吗铐,受9級特大地震影響东亦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜唬渗,卻給世界環(huán)境...
    茶點故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一典阵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧镊逝,春花似錦壮啊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽玄渗。三九已至,卻和暖如春狸眼,著一層夾襖步出監(jiān)牢的瞬間藤树,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工份企, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留也榄,地道東北人。 一個月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓司志,卻偏偏與公主長得像甜紫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子骂远,可洞房花燭夜當晚...
    茶點故事閱讀 44,884評論 2 354