JDK 動態代理爲什麼必須要基於接口?

哈嘍大家好啊,我是 Hydra。

如果不出意外的話,這篇文章發出的時間是 2022 年 2 月 22 日的 22 點 22,農曆正月廿二,星期二。畢竟是個有點意思的日期,不發點什麼總感覺有點浪費,畢竟我們大部分人大概率都活不到 2222 年的 2 月 22 日~

哈哈扯遠了,前幾天的時候,交流羣裏的小夥伴拋出了一個問題,爲什麼 JDK 的動態代理一定要基於接口實現呢?

好的安排,其實要想弄懂這個問題還是需要一些關於代理和反射的底層知識的,我們今天就盤一盤這個問題,走你~

一個簡單的例子

在分析原因之前,我們先完整的看一下實現 jdk 動態代理需要幾個步驟,首先需要定義一個接口:

public interface Worker {
    void work();
}

再寫一個基於這個接口的實現類:

public class Programmer implements Worker {
    @Override
    public void work() {
        System.out.println("coding...");
    }
}

自定義一個Handler,實現InvocationHandler接口,通過重寫內部的invoke方法實現邏輯增強。其實這個InvocationHandler可以使用匿名內部類的形式定義,這裏爲了結構清晰拿出來單獨聲明。

public class WorkHandler implements InvocationHandler {
    private Object target;
    WorkHandler(Object target){
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().equals("work")) {
            System.out.println("before work...");
            Object result = method.invoke(target, args);
            System.out.println("after work...");
            return result;
        }
        return method.invoke(target, args);
    }
}

main方法中進行測試,使用Proxy類的靜態方法newProxyInstance生成一個代理對象並調用方法:

public static void main(String[] args) {
    Programmer programmer = new Programmer();
    Worker worker = (Worker) Proxy.newProxyInstance(
            programmer.getClass().getClassLoader(),
            programmer.getClass().getInterfaces(),
            new WorkHandler(programmer));
    worker.work();
}

執行上面的代碼,輸出:

before work...
coding...
after work...

可以看到,執行了方法邏輯的增強,到這,一個簡單的動態代理過程就實現了,下面我們分析一下源碼。

Proxy 源碼解析

既然是一個代理的過程,那麼肯定存在原生對象代理對象之分,下面我們查看源碼中是如何動態的創建代理對象的過程。上面例子中,創建代理對象調用的是Proxy類的靜態方法newProxyInstance,查看一下源碼:

@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) throws IllegalArgumentException{
    Objects.requireNonNull(h);

    final Class<?>[] intfs = interfaces.clone();
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }

    /*
     * Look up or generate the designated proxy class.
     */
    Class<?> cl = getProxyClass0(loader, intfs);

    /*
     * Invoke its constructor with the designated invocation handler.
     */
    try {
        if (sm != null) {
            checkNewProxyPermission(Reflection.getCallerClass(), cl);
        }

        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        if (!Modifier.isPublic(cl.getModifiers())) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    cons.setAccessible(true);
                    return null;
                }
            });
        }
        return cons.newInstance(new Object[]{h});
    }//省略catch
}

概括一下上面代碼中重點部分:

上面這個過程中,獲取構造方法和生成對象都是直接利用的反射,而需要重點看看的是生成代理類的方法getProxyClass0

private static Class<?> getProxyClass0(ClassLoader loader,
                                       Class<?>... interfaces) {
    if (interfaces.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }

    // If the proxy class defined by the given loader implementing
    // the given interfaces exists, this will simply return the cached copy;
    // otherwise, it will create the proxy class via the ProxyClassFactory
    return proxyClassCache.get(loader, interfaces);
}

註釋寫的非常清晰,如果緩存中已經存在了就直接從緩存中取,這裏的proxyClassCache是一個WeakCache類型,如果緩存中目標classLoader和接口數組對應的類已經存在,那麼返回緩存的副本。如果沒有就使用ProxyClassFactory去生成 Class 對象。中間的調用流程可以省略,最終實際調用了ProxyClassFactoryapply方法生成 Class。在apply方法中,主要做了下面 3 件事。

if (proxyPkg == null) {
    // if no non-public proxy interfaces, use com.sun.proxy package
    proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}
/*
 * Choose a name for the proxy class to generate.
 */
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;

如果接口被定義爲public公有,那麼默認會使用com.sun.proxy作爲包名,類名是$Proxy加上一個自增的整數值,初始時是 0,因此生成的文件名是$Proxy0

如果是非公有接口,那麼會使用和被代理類一樣的包名,可以寫一個private接口的例子進行一下測試。

package com.hydra.test.face;
public class InnerTest {
    private interface InnerInterface {
        void run();
    }

    class InnerClazz implements InnerInterface {
        @Override
        public void run() {
            System.out.println("go");
        }
    }
}

這時生成的代理類的包名爲com.hydra.test.face,與被代理類相同:

byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
      proxyName, interfaces, accessFlags);

generateProxyClass方法中,有一個重要的參數會發揮作用:

private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));

如果這個屬性被配置爲true,那麼會把字節碼存儲到硬盤上的 class 文件中,否則不會保存臨時的字節碼文件。

return defineClass0(loader, proxyName,
      proxyClassFile, 0, proxyClassFile.length);

返回代理類的 Class 後的流程我們在前面就已經介紹過了,先獲得構造方法,再使用構造方法反射的方式創建代理對象。

神祕的代理對象

創建代理對象流程的源碼分析完了,我們可以先通過 debug 來看看上面生成的這個代理對象究竟是個什麼:

和源碼中看到的規則一樣,是一個 Class 爲$Proxy0的神祕對象,再看一下代理對象的 Class 的詳細信息:

類的全限定名是com.sun.proxy.$Proxy0,在上面我們提到過,這個類是在運行過程中動態生成的,並且程序執行完成後,會自動刪除掉 class 文件。如果想要保留這個臨時文件不被刪除,就要修改我們上面提到的參數,具體操作起來有兩種方式,第一種是在啓動VM參數中加入:

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

第二種是在代碼中加入下面這一句,注意要加在生成動態代理對象之前:

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles""true");

使用了上面兩種方式中的任意一種後,就可以保存下來臨時的字節碼文件了,需要注意這個文件生成的位置,並不是在target目錄下,而是生成在項目目錄下的com\sun\proxy中,正好和默認生成的包名對應。

拿到字節碼文件後,就可以使用反編譯工具來反編譯它了,這裏使用jad在 cmd 下一條命令直接搞定:

jad -s java $Proxy0.class

看一下反編譯後$Proxy0.java文件的內容,下面的代碼中,我只保留了核心部分,省略了無關緊要的equalstoStringhashCode方法的定義。

public final class $Proxy0 extends Proxy implements Worker{
    public $Proxy0(InvocationHandler invocationhandler){
        super(invocationhandler);
    }

    public final void work(){
        try{
            super.h.invoke(this, m3, null);
            return;
        }catch(Error _ex) { }
        catch(Throwable throwable){
            throw new UndeclaredThrowableException(throwable);
        }
    }

    private static Method m3;
    static {
        try{           
            m3 = Class.forName("com.hydra.test.Worker").getMethod("work", new Class[0]);   
            //省略其他Method
        }//省略catch
    }
}

這個臨時生成的代理類$Proxy0中主要做了下面的幾件事:

到這裏,整體的流程就分析完了,我們可以用一張圖來簡要總結上面的過程:

爲什麼要有接口?

通過上面的分析,我們已經知道了代理對象是如何生成的了,那麼回到開頭的問題,爲什麼 jdk 的動態代理一定要基於接口呢?

其實如果不看上面的分析,我們也應該知道,要擴展一個類有常見的兩種方式,繼承父類或實現接口。這兩種方式都允許我們對方法的邏輯進行增強,但現在不是由我們自己來重寫方法,而是要想辦法讓 jvm 去調用InvocationHandler中的invoke方法,也就是說代理類需要和兩個東西關聯在一起:

而 jdk 處理這個問題的方式是選擇繼承父類Proxy,並把InvocationHandler存在父類的對象中:

public class Proxy implements java.io.Serializable {
    protected InvocationHandler h;
    protected Proxy(InvocationHandler h) {
        Objects.requireNonNull(h);
        this.h = h;
    }
    //...
}

通過父類Proxy的構造方法,保存了創建代理對象過程中傳進來的InvocationHandler的實例,使用protected修飾保證了它可以在子類中被訪問和使用。但是同時,因爲 java 是單繼承的,因此在繼承了Proxy後,只能通過實現目標接口的方式來實現方法的擴展,達到我們增強目標方法邏輯的目的。

扯點別的

其實看完源碼、弄明白代理對象生成的流程後,我們還可以用另一種方法實現動態代理:

public static void main(String[] args) throws Exception {
    Class<?> proxyClass = Proxy.getProxyClass(Test3.class.getClassLoader(), Worker.class);
    Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class);
    InvocationHandler workHandler = new WorkHandler(new Programmer());
    Worker worker = (Worker) constructor.newInstance(workHandler);
    worker.work();
}

運行結果與之前相同,這種寫法其實就是抽出了我們前面介紹的幾個核心方法,中間省略了一些參數的校驗過程,這種方式可以幫助大家熟悉 jdk 動態代理原理,但是在使用過程中還是建議大家使用標準方式,相對更加安全規範。

總結

本文從源碼以及實驗的角度,分析了 jdk 動態代理生成代理對象的流程,通過代理類的實現原理分析了爲什麼 jdk 動態代理一定要基於接口實現。總的來說,jdk 動態代理的應用還是非常廣泛的,例如在 Spring、Mybatis 以及 Feign 等很多框架中動態代理都被大量的使用,可以說學好 jdk 動態代理,對於我們閱讀這些框架的底層源碼還是很有幫助的。

那麼,這次的分享就到這裏,我是 Hydra,下期見。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/bgCciZ7c8h480kZljX7dYg