1. 程式人生 > >面試必問系列之JDK動態代理

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

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

1. 帶著問題去學習

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

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

那 JDK 動態代理你了到底瞭解多少呢?有去看過代理物件的 class 檔案麼?下面兩個關於 JDK 動態代理的問題你能回答上來麼?

  • 問題1:為什麼 JDK 動態代理要基於介面實現?而不是基於繼承來實現?
  • 問題2:JDK 動態代理中,目標物件呼叫自己的另一個方法,會經過代理物件麼?

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

2. JDK 動態代理的寫法

  • JDK 動態代理需要這幾部分內容:介面、實現類、代理物件。
  • 代理物件需要繼承 InvocationHandler,代理類呼叫方法時會呼叫 InvocationHandler 的 invoke 方法。
  • 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> 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。