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

来源:https://www.cnblogs.com/zwwhnly/archive/2023/04/17/17324797.html
-Advertisement-
Play Games

1. 示例 首先,定義一個介面: public interface Staff { void work(); } 然後,新增一個類並實現上面的介面: public class Coder implements Staff { @Override public void work() { System ...


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又是單繼承的,如果想要繼續對類進行擴展,只能通過實現介面的方式。

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 好消息:與上題的Emergency是同樣的方法。壞消息:又錯了&&c++真的比c方便太多太多。 A family hierarchy is usually presented by a pedigree tree. Your job is to count those family members ...
  • 安裝Zookeeper和Kafka集群 本文介紹如何安裝Zookeeper和Kafka集群。為了方便,介紹的是在一臺伺服器上的安裝,實際應該安裝在多台伺服器上,但步驟是一樣的。 安裝Zookeeper集群 下載安裝包 從官網上下載安裝包: curl https://dlcdn.apache.org/ ...
  • 原文鏈接:https://www.zhoubotong.site/post/94.html 說下背景吧,大家在開發中可能在不同的目錄(package)下定義了相同的struct(屬性參數完全一樣如名字、個數和類型),在方法調用傳參數的時候,可能是用到了其中某一個struct的引用。 那麼這裡就牽扯到 ...
  • Java的反射機制允許程式員在執行期藉助於Reflection API取得任何類的內部信息,並能操作對象的屬性和方法,在各類框架中應用非常廣泛。這一期是關於反射內容的筆記,包含Class類、Field類、Method類、Constructor類及相關方法。 ...
  • Gin 環境:https://goproxy.cn,driect github.com/gin-gonic/gin 介紹 Gin 是一個用 Go (Golang) 編寫的 Web 框架。 它具有類似 martini 的 API,性能要好得多,多虧了 httprouter,速度提高了 40 倍。 如果 ...
  • 前言 Disruptor的高性能,是多種技術結合以及本身架構的結果。本文主要講源碼,涉及到的相關知識點需要讀者自行去瞭解,以下列出: 鎖和CAS 偽共用和緩存行 volatile和記憶體屏障 原理 此節結合demo來看更容易理解:傳送門 下圖來自官方文檔 官方原圖有點亂,我翻譯一下 在講原理前,先瞭解 ...
  • 作者:馬佩 鏈接:https://juejin.cn/post/7146016771936354312 場景 當我們業務資料庫表中的數據越來越多,如果你也和我遇到了以下類似場景,那讓我們一起來解決這個問題 數據的插入,查詢時長較長 後續業務需求的擴展 在表中新增欄位 影響較大 表中的數據並不是所有的 ...
  • 大家好,我是陶朱公Boy。(一個認真生活總想超越自己的程式員) 一線互聯網Java技術專家,有超過8年+後端開發、架構經驗。公眾號:「陶朱公Boy」歡迎大家關註! 星球簡介 一個幫你學編程、做項目、找工作少走彎路的交流圈,進步從此開始! 加入後你可以: 1.獲取陶朱公原創編程學習路線、原創編程知識庫 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...