【深度思考】聊聊JDK動態代理原理

申城異鄉人發表於2023-04-17

1. 示例

首先,定義一個介面:

public interface Staff {
    void work();
}

然後,新增一個類並實現上面的介面:

public class Coder implements Staff {
    @Override
    public void work() {
        System.out.println("認真寫bug……");
    }
}

假設現在有這麼一個需求:在不改動以上類程式碼的前提下,對該方法增加一些前置操作或者後置操作。

接下來就來講解下,如何使用JDK動態代理來實現這個需求。

首先,自定義一個呼叫處理器,實現java.lang.reflect.InvocationHandler介面並重寫invoke方法:

public class AttendanceInvocationHandler implements InvocationHandler {
    private final Object target;

    public AttendanceInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("上班打卡……");

        Object invoke = method.invoke(target, args);

        System.out.println("下班打卡……");

        return invoke;
    }
}

重點看下Object invoke = method.invoke(target, args);,該行程式碼會執行真正的目標方法,在這前後,我們可以新增一些增強邏輯。

然後,新建個測試類,看下JDK動態代理如何使用:

public class JdkProxyTest {
    public static void main(String[] args) {
        Coder coder = new Coder();
        AttendanceInvocationHandler h = new AttendanceInvocationHandler(coder);
        // 建立代理物件
        Object proxyInstance = Proxy.newProxyInstance(coder.getClass().getClassLoader(),
                coder.getClass().getInterfaces(),
                h);
        Staff staff = (Staff) proxyInstance;
        staff.work();
    }
}

執行以上程式碼,效果如下圖所示:

從執行結果可以看出,在目標方法的前後,執行了自定義的操作。

2. 原理

這裡理解2個概念,目標物件和代理物件,

目標物件是真正要呼叫的物件,上面示例中的Coder類就是目標物件,

代理物件是JDK自動生成的物件,在代理物件內部會去呼叫目標物件的目標方法。

JDK動態代理的核心就是上面示例中的Proxy.newProxyInstance方法,方法簽名如下圖所示:

第1個引數傳入的是目標物件的ClassLoader,第2個引數傳入的是目標物件的介面資訊,第3個引數傳入的是自定義的InvocationHandler。

然後看下該方法的實現邏輯,先看第1處重點:

註釋翻譯過來是:查詢或者生成指定的代理類。

該方法會生成代理類的位元組碼檔案(也可能是從快取中讀取),核心邏輯在ProxyClassFactory類的apply方法中,

該方法中定義了生成的代理類的包名以及檔名:

因此預設情況下,自動生成的代理類名稱是com.sun.proxy.$Proxy0

該方法最後會生成代理類的位元組碼,預設情況下不會儲存到檔案系統,但可以透過引數指定儲存到檔案系統:

可以看出,儲存不儲存到檔案系統,受saveGeneratedFiles的影響,其定義如下所示:

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

所以可以透過指定sun.misc.ProxyGenerator.saveGeneratedFiles引數來讓生成的代理類位元組碼檔案儲存到檔案系統中。

然後看第2處重點:

先是獲取建構函式,然後是生成代理類物件的例項。

3. 為什麼必須要基於介面?

思考一個問題,為什麼JDK動態代理必須要基於介面,帶著這個問題,我們看下動態生成的代理類com.sun.proxy.$Proxy0長什麼樣子?

JVM引數裡新增引數-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true,然後啟動上面示例中的測試程式碼:

生成的代理類位元組碼檔案儲存在專案根目錄下的com/sun/proxy目錄下:

在IDEA中開啟後,如下圖所示:

在靜態程式碼塊中,對靜態變數m0、m1、m2、m3進行了賦值,其中m3是要執行的目標方法。

在構造方法中,執行的是super(var1);,也就是父類Proxy的構造方法:

該方法是將我們自定義的InvocationHandler賦值給了父類的變數h。

而以下測試程式碼實際執行的是代理類$Proxy0裡的work方法:

Staff staff = (Staff) proxyInstance;
staff.work();

代理類$Proxy0裡的work方法實際執行的是自定義InvocationHandler裡的invoke方法:

因此在執行目標方法前後,執行了自定義的前置操作和後置操作。

瞭解了這個呼叫過程,就理解了為什麼JDK動態代理必須要基於介面,因為動態生成的代理類已經繼承了類java.lang.reflect.Proxy

而Java又是單繼承的,如果想要繼續對類進行擴充套件,只能透過實現介面的方式。

文章持續更新,歡迎關注微信公眾號「申城異鄉人」第一時間閱讀!

相關文章