面試必問系列之JDK動態代理

掃描文末二維碼或者微信搜索公眾號小李不禿,即可關注微信公眾號,獲取到更多 Java 相關內容。

1. 帶着問題去學習

面試中經常會問到關於 Spring 的代理方式有哪兩種?大家異口同聲的回答:JDK 動態代理和 CGLIB 動態代理。

這兩種代理有什麼區別呢?JDK 動態代理的類通過接口實現,CGLIB 動態代理是通過子類來實現的。

那 JDK 動態代理你了到底了解多少呢?有去看過代理對象的 class 文件么?下面兩個關於 JDK 動態代理的問題你能回答上來么?

  • 問題1:為什麼 JDK 動態代理要基於接口實現?而不是基於繼承來實現?
  • 問題2:JDK 動態代理中,目標對象調用自己的另一個方法,會經過代理對象么

小李帶着大家更深入的了解一下 JDK 的動態代理。

2. JDK 動態代理的寫法

  • JDK 動態代理需要這幾部分內容:接口、實現類、代理對象。
  • 代理對象需要繼承 InvocationHandler,代理類調用方法時會調用 InvocationHandlerinvoke 方法。
  • Proxy 是所有代理類的父類,它提供了一個靜態方法 newProxyInstance 動態創建代理對象。
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("商品過保質期了,需要退款,退款數量 :" + nums);
    }
}

 

public class JdkProxy implements InvocationHandler {

    private Object target;
    public JdkProxy(Object target) {
        this.target = target;
    }
    // 方法增強
    @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("調用方法后執行!!!!" ); }
    private void before(Object[] args) { System.out.println("調用方法前執行!!!!" ); }

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

 

public class JdkProxyMain {
    public static void main(String[] args) {
        // 標明目標 target 是 BuyServiceImpl
        JdkProxy proxy = new JdkProxy(new BuyServiceImpl());
        // 獲取代理對象實例
        IBuyService buyItem = proxy.getProxy();
        // 調用方法
        buyItem.buyItem(12345);
    }
}

查看運行結果

調用方法前執行!!!!
小李不禿要買東西!小李不禿的id是: 12345
調用方法后執行!!!!

我們完成了對目標方法的增強,開始對代理對象進行一個更全面的分析。

3. 剖析代理對象並解答問題

剖析代理對象的前提得是有代理對象,動態代理的對象是在運行時期創建的,我們就沒辦法通過打斷點的方式進行分析了。但是我們可以通過反編譯 .class 文件進行分析。如何獲取到 .class 文件呢?

通過在代碼中添加:System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true") ,就能夠實現將動態代理對象的 class 文件寫入到磁盤中。代碼如下:

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

在項目的根目錄下多了一個 $Proxy0.class 文件

看一下這個文件的內容

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());
        }
    }
}

動態代理對象 $Proxy0 繼承了 Proxy 類並且實現了 IBuyService 接口。那問題 1 的答案就出來了:動態代理對象默認繼承了 Proxy 對象,而且 Java 不支持多繼承,所以 JDK 動態代理要基於接口來實現。

$Proxy0 重寫了 IBuyService 接口的方法,還有 Object 的方法。在重寫的方法中,統一調用 super.h.invoke 方法。super 指的是 Proxyh 代表 InvocationHandler,這裏就是 JdkProxy。所以這裏調用的是 JdkProxyinvoke 方法。

所以每次調用 buyItem 方法的時候,會先打印出 調用方法前執行!!!!

@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("調用方法后執行!!!!" ); }
private void before(Object[] args) { System.out.println("調用方法前執行!!!!" ); }

問題 2 還沒解決呢,接着往下看

@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("商品過保質期了,需要退款,退款數量 :" + nums);
    }
}

上面這段代碼中,在 buyItem 調用內部的 refund 方法,那這個內部調用方法是否走代理對象呢?看一下執行結果:

調用方法前執行!!!!
小李不禿要買東西!小李不禿的id是: 12345
商品過保質期了,需要退款,退款數量 :100
調用方法后執行!!!!

確實是沒有走代理對象,其實我們期待的結果是下面這樣的

調用方法前執行!!!!
小李不禿要買東西!小李不禿的id是: 12345
調用方法前執行!!!!
商品過保質期了,需要退款,退款數量 :100
調用方法后執行!!!!
調用方法后執行!!!!

那為什麼會造成這種差異呢?

因為內部調用 refund 方法的調用,相當於 this.refund(100),而這個 this 指的是 BuyServiceImpl 對象,而不是代理對象,所以refund 方法沒有得到增強

4. 總結和延伸

  • 本篇文章了解了 JDK 動態代理的使用,通過分析 JDK 動態代理生成對象的 class 文件,解決了兩個問題:

    • 問題1:為什麼 JDK 動態代理要基於接口實現?而不是基於繼承來實現?
    • 解答:因為 JDK 動態代理生成的對象默認是繼承 Proxy ,Java 不支持多繼承,所以 JDK 動態代理要基於接口來實現。
    • 問題2:JDK 動態代理中,目標對象調用自己的另一個方法,會經過代理對象么
    • 解答:內部調用方法使用的對象是目標對象本身,被調用的方法不會經過代理對象。
  • 我們知道了 JDK 動態代理內部調用是不走代理對象的。那對於 @Transactional 和 @Async 等註解不起作用是不是就搞清楚為啥了?

  • 因為 @Transactional@Async 等註解是通過 Spring AOP 來進行實現的,如果動態代理使用的是 JDK 動態代理,那麼在方法的內部調用該方法中其它帶有該註解的方法,由於此時調用的不是動態代理對象,所以註解失效

  • 上面這些問題就是 JDK 動態代理的缺點,那 Spring 如何避免這個問題呢?就是另個一個動態代理:CGLIB 動態代理,我會在下篇文章進行分析。

5. 參考

  • https://juejin.im/post/5d8a0799f265da5b7a752e7c#heading-6
  • https://blog.csdn.net/varyall/article/details/102952365

6. 猜你喜歡

  • JSON的學習和使用

  • 學習反射看這一篇就夠了

  • 併發編程學習(一)Java 內存模型

掃描下方二維碼即可關注微信公眾號小李不禿,一起高效學習 Java。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

您可能也會喜歡…