面試必問(wèn)系列之JDK動(dòng)態(tài)代理

掃描文末二維碼或者微信搜索公眾號(hào)小李不禿,即可關(guān)注微信公眾號(hào)黑忱,獲取到更多 Java 相關(guān)內(nèi)容宴抚。

1. 帶著問(wèn)題去學(xué)習(xí)

面試中經(jīng)常會(huì)問(wèn)到關(guān)于 Spring 的代理方式有哪兩種?大家異口同聲的回答:JDK 動(dòng)態(tài)代理和 CGLIB 動(dòng)態(tài)代理甫煞。

這兩種代理有什么區(qū)別呢菇曲?JDK 動(dòng)態(tài)代理的類通過(guò)接口實(shí)現(xiàn),CGLIB 動(dòng)態(tài)代理是通過(guò)子類來(lái)實(shí)現(xiàn)的抚吠。

image

那 JDK 動(dòng)態(tài)代理你了到底了解多少呢常潮?有去看過(guò)代理對(duì)象的 class 文件么?下面兩個(gè)關(guān)于 JDK 動(dòng)態(tài)代理的問(wèn)題你能回答上來(lái)么楷力?

  • 問(wèn)題1:為什么 JDK 動(dòng)態(tài)代理要基于接口實(shí)現(xiàn)喊式?而不是基于繼承來(lái)實(shí)現(xiàn)?
  • 問(wèn)題2:JDK 動(dòng)態(tài)代理中萧朝,目標(biāo)對(duì)象調(diào)用自己的另一個(gè)方法岔留,會(huì)經(jīng)過(guò)代理對(duì)象么
image

小李帶著大家更深入的了解一下 JDK 的動(dòng)態(tài)代理剪勿。

2. JDK 動(dòng)態(tài)代理的寫法

  • JDK 動(dòng)態(tài)代理需要這幾部分內(nèi)容:接口贸诚、實(shí)現(xiàn)類、代理對(duì)象厕吉。
  • 代理對(duì)象需要繼承 InvocationHandler酱固,代理類調(diào)用方法時(shí)會(huì)調(diào)用 InvocationHandlerinvoke 方法。
  • Proxy 是所有代理類的父類头朱,它提供了一個(gè)靜態(tài)方法 newProxyInstance 動(dòng)態(tài)創(chuàng)建代理對(duì)象运悲。
image
public interface IBuyService {
     void buyItem(int userId);
     void refund(int nums);
}
@Service
public class BuyServiceImpl implements IBuyService {
    @Override
    public void buyItem(int userId) {
        System.out.println("小李不禿要買東西!小李不禿的id是: " + userId);
    }
    @Override
    public void refund(int nums) {
        System.out.println("商品過(guò)保質(zhì)期了项钮,需要退款班眯,退款數(shù)量 :" + nums);
    }
}
public class JdkProxy implements InvocationHandler {

    private Object target;
    public JdkProxy(Object target) {
        this.target = target;
    }
    // 方法增強(qiáng)
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before(args);
        Object result = method.invoke(target,args);
        after(args);
        return result;
    }
    private void after(Object result) { System.out.println("調(diào)用方法后執(zhí)行OM!!J鸢宠能!" ); }
    private void before(Object[] args) { System.out.println("調(diào)用方法前執(zhí)行!4挪汀Nコ纭!" ); }

    // 獲取代理對(duì)象
    public <T> T getProxy(){
        return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),this);
    }
}
public class JdkProxyMain {
    public static void main(String[] args) {
        // 標(biāo)明目標(biāo) target 是 BuyServiceImpl
        JdkProxy proxy = new JdkProxy(new BuyServiceImpl());
        // 獲取代理對(duì)象實(shí)例
        IBuyService buyItem = proxy.getProxy();
        // 調(diào)用方法
        buyItem.buyItem(12345);
    }
}

查看運(yùn)行結(jié)果

調(diào)用方法前執(zhí)行U锱P哐印!脾还!
小李不禿要買東西伴箩!小李不禿的id是: 12345
調(diào)用方法后執(zhí)行!1陕`脱琛!

我們完成了對(duì)目標(biāo)方法的增強(qiáng)怔蚌,開始對(duì)代理對(duì)象進(jìn)行一個(gè)更全面的分析呵恢。

image

3. 剖析代理對(duì)象并解答問(wèn)題

剖析代理對(duì)象的前提得是有代理對(duì)象,動(dòng)態(tài)代理的對(duì)象是在運(yùn)行時(shí)期創(chuàng)建的媚创,我們就沒(méi)辦法通過(guò)打斷點(diǎn)的方式進(jìn)行分析了渗钉。但是我們可以通過(guò)反編譯 .class 文件進(jìn)行分析。如何獲取到 .class 文件呢钞钙?

通過(guò)在代碼中添加:System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true") 鳄橘,就能夠?qū)崿F(xiàn)將動(dòng)態(tài)代理對(duì)象的 class 文件寫入到磁盤中。代碼如下:

public class JdkProxyMain {
    public static void main(String[] args) {
        // 代理對(duì)象的 class 文件寫入到磁盤中
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        // 標(biāo)明目標(biāo) target 是 BuyServiceImpl
        JdkProxy proxy = new JdkProxy(new BuyServiceImpl());
        // 獲取代理對(duì)象實(shí)例
        IBuyService buyItem = proxy.getProxy();
        // 調(diào)用方法
        buyItem.buyItem(12345);
    }
}

在項(xiàng)目的根目錄下多了一個(gè) $Proxy0.class 文件

image

看一下這個(gè)文件的內(nèi)容

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

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

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

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

    public final void refund(int var1) throws  {
        try {
            super.h.invoke(this, m4, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.example.springtest.service.IBuyService").getMethod("buyItem", Integer.TYPE);
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m4 = Class.forName("com.example.springtest.service.IBuyService").getMethod("refund", Integer.TYPE);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

動(dòng)態(tài)代理對(duì)象 $Proxy0 繼承了 Proxy 類并且實(shí)現(xiàn)了 IBuyService 接口芒炼。那問(wèn)題 1 的答案就出來(lái)了:動(dòng)態(tài)代理對(duì)象默認(rèn)繼承了 Proxy 對(duì)象瘫怜,而且 Java 不支持多繼承,所以 JDK 動(dòng)態(tài)代理要基于接口來(lái)實(shí)現(xiàn)本刽。

image

$Proxy0 重寫了 IBuyService 接口的方法鲸湃,還有 Object 的方法。在重寫的方法中子寓,統(tǒng)一調(diào)用 super.h.invoke 方法暗挑。super 指的是 Proxyh 代表 InvocationHandler斜友,這里就是 JdkProxy炸裆。所以這里調(diào)用的是 JdkProxyinvoke 方法。

所以每次調(diào)用 buyItem 方法的時(shí)候鲜屏,會(huì)先打印出 調(diào)用方法前執(zhí)行E肟础9础!惯殊!酱吝。

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before(args);
// 通過(guò)反射調(diào)用方法
Object result = method.invoke(target,args);
after(args);
return result;
}
private void after(Object result) { System.out.println("調(diào)用方法后執(zhí)行!M了肌5敉!" ); }
private void before(Object[] args) { System.out.println("調(diào)用方法前執(zhí)行@四!v帧址愿!" ); }

問(wèn)題 2 還沒(méi)解決呢,接著往下看

image
@Service
public class BuyServiceImpl implements IBuyService {
    @Override
    public void buyItem(int userId) {
        System.out.println("小李不禿要買東西冻璃!小李不禿的id是: " + userId);
        refund(100);
    }
    @Override
    public void refund(int nums) {
        System.out.println("商品過(guò)保質(zhì)期了响谓,需要退款,退款數(shù)量 :" + nums);
    }
}

上面這段代碼中省艳,在 buyItem 調(diào)用內(nèi)部的 refund 方法娘纷,那這個(gè)內(nèi)部調(diào)用方法是否走代理對(duì)象呢?看一下執(zhí)行結(jié)果:

調(diào)用方法前執(zhí)行0峡弧@稻А!辐烂!
小李不禿要買東西遏插!小李不禿的id是: 12345
商品過(guò)保質(zhì)期了,需要退款纠修,退款數(shù)量 :100
調(diào)用方法后執(zhí)行8斐啊!?鄄荨了牛!

確實(shí)是沒(méi)有走代理對(duì)象,其實(shí)我們期待的結(jié)果是下面這樣的

調(diào)用方法前執(zhí)行3矫睢Sセ觥!密浑!
小李不禿要買東西福荸!小李不禿的id是: 12345
調(diào)用方法前執(zhí)行!k戎馈>慈瘛背传!
商品過(guò)保質(zhì)期了,需要退款台夺,退款數(shù)量 :100
調(diào)用方法后執(zhí)行>毒痢!2椤梳星!
調(diào)用方法后執(zhí)行!9龆洹T┰帧!

那為什么會(huì)造成這種差異呢辕近?

因?yàn)閮?nèi)部調(diào)用 refund 方法的調(diào)用韵吨,相當(dāng)于 this.refund(100),而這個(gè) this 指的是 BuyServiceImpl 對(duì)象移宅,而不是代理對(duì)象归粉,所以refund 方法沒(méi)有得到增強(qiáng)

image

4. 總結(jié)和延伸

  • 本篇文章了解了 JDK 動(dòng)態(tài)代理的使用漏峰,通過(guò)分析 JDK 動(dòng)態(tài)代理生成對(duì)象的 class 文件糠悼,解決了兩個(gè)問(wèn)題:

    • 問(wèn)題1:為什么 JDK 動(dòng)態(tài)代理要基于接口實(shí)現(xiàn)?而不是基于繼承來(lái)實(shí)現(xiàn)浅乔?
    • 解答:因?yàn)?JDK 動(dòng)態(tài)代理生成的對(duì)象默認(rèn)是繼承 Proxy 倔喂,Java 不支持多繼承,所以 JDK 動(dòng)態(tài)代理要基于接口來(lái)實(shí)現(xiàn)靖苇。
    • 問(wèn)題2:JDK 動(dòng)態(tài)代理中滴劲,目標(biāo)對(duì)象調(diào)用自己的另一個(gè)方法,會(huì)經(jīng)過(guò)代理對(duì)象么顾复?
    • 解答:內(nèi)部調(diào)用方法使用的對(duì)象是目標(biāo)對(duì)象本身班挖,被調(diào)用的方法不會(huì)經(jīng)過(guò)代理對(duì)象。
  • 我們知道了 JDK 動(dòng)態(tài)代理內(nèi)部調(diào)用是不走代理對(duì)象的芯砸。那對(duì)于 @Transactional 和 @Async 等注解不起作用是不是就搞清楚為啥了萧芙?

    • 因?yàn)? @Transactional@Async 等注解是通過(guò) Spring AOP 來(lái)進(jìn)行實(shí)現(xiàn)的,如果動(dòng)態(tài)代理使用的是 JDK 動(dòng)態(tài)代理假丧,那么在方法的內(nèi)部調(diào)用該方法中其它帶有該注解的方法双揪,由于此時(shí)調(diào)用的不是動(dòng)態(tài)代理對(duì)象,所以注解失效包帚。
  • 上面這些問(wèn)題就是 JDK 動(dòng)態(tài)代理的缺點(diǎn)渔期,那 Spring 如何避免這個(gè)問(wèn)題呢?就是另個(gè)一個(gè)動(dòng)態(tài)代理:CGLIB 動(dòng)態(tài)代理,我會(huì)在下篇文章進(jìn)行分析疯趟。

    image

5. 參考

6. 猜你喜歡

掃描下方二維碼即可關(guān)注微信公眾號(hào)小李不禿拘哨,一起高效學(xué)習(xí) Java。

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末信峻,一起剝皮案震驚了整個(gè)濱河市倦青,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌盹舞,老刑警劉巖产镐,帶你破解...
    沈念sama閱讀 211,639評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異踢步,居然都是意外死亡癣亚,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門获印,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)述雾,“玉大人,你說(shuō)我怎么就攤上這事蓬豁。” “怎么了菇肃?”我有些...
    開封第一講書人閱讀 157,221評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵地粪,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我琐谤,道長(zhǎng)蟆技,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,474評(píng)論 1 283
  • 正文 為了忘掉前任斗忌,我火速辦了婚禮质礼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘织阳。我一直安慰自己眶蕉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評(píng)論 6 386
  • 文/花漫 我一把揭開白布唧躲。 她就那樣靜靜地躺著造挽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪弄痹。 梳的紋絲不亂的頭發(fā)上饭入,一...
    開封第一講書人閱讀 49,816評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音肛真,去河邊找鬼谐丢。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的乾忱。 我是一名探鬼主播讥珍,決...
    沈念sama閱讀 38,957評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼饭耳!你這毒婦竟也來(lái)了串述?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,718評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤寞肖,失蹤者是張志新(化名)和其女友劉穎纲酗,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體新蟆,經(jīng)...
    沈念sama閱讀 44,176評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡觅赊,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了琼稻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吮螺。...
    茶點(diǎn)故事閱讀 38,646評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖帕翻,靈堂內(nèi)的尸體忽然破棺而出鸠补,到底是詐尸還是另有隱情,我是刑警寧澤嘀掸,帶...
    沈念sama閱讀 34,322評(píng)論 4 330
  • 正文 年R本政府宣布紫岩,位于F島的核電站,受9級(jí)特大地震影響睬塌,放射性物質(zhì)發(fā)生泄漏泉蝌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評(píng)論 3 313
  • 文/蒙蒙 一揩晴、第九天 我趴在偏房一處隱蔽的房頂上張望勋陪。 院中可真熱鬧,春花似錦硫兰、人聲如沸诅愚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)呻粹。三九已至,卻和暖如春苏研,著一層夾襖步出監(jiān)牢的瞬間等浊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工摹蘑, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留筹燕,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,358評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像撒踪,于是被迫代替她去往敵國(guó)和親过咬。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評(píng)論 2 348