從設計上理解JDK動態代理

作者簡介:大家好,我是smart哥,前中興通訊、美團架構師,現某互聯網公司CTO

聯系qq:184480602,加我進群,大家一起學習,一起進步,一起對抗互聯網寒冬

照理說,動態代理經過前面3篇介紹,該講的都已經講完了,再深入下去的意義不是特別大。但看到群里有小伙伴說對InvocationHandler#invoke()方法的參數有些困惑,所以又補了一篇。

關于這三個參數,其實一句話就能講完:

  • Object proxy:很遺憾,是代理對象本身,而不是目標對象(不要調用,會無限遞歸,一般不會使用)
  • Method method:方法執行器,用來執行方法(有點不好解釋,Method只是一個執行器,傳入目標對象就執行目標對象的方法)
  • Obeject[] args:方法參數

上一篇也是這么介紹的,但大家的接受度似乎不是很好,甚至對參數怎么傳進來的感到困惑,所以本篇打算站在Proxy類設計的角度分析三個參數的由來。

當然啦,我還遠不敢說能寫出JDK級別的代碼。本文雖然也嘗試編寫MyProxy類,但它是用來解釋參數由來的,意義并不在于完美復刻JDK Proxy。

山寨Proxy類概覽

先看一下我編寫的山寨MyProxy和MyInvocationHandler吧:

是不是和上面原版的JDK Proxy幾乎一模一樣呢?激動嗎?一起來看看我是怎么設計的(不要細想代碼邏輯,這根本是偽代碼,跑不起來的)。

/*** 山寨Proxy類*/
public static class MyProxy implements java.io.Serializable {protected MyInvocationHandler h;private MyProxy() {}protected MyProxy(MyInvocationHandler h) {Objects.requireNonNull(h);this.h = h;}public static Object newProxyInstance(ClassLoader classLoader,Class<?>[] interfaces,MyInvocationHandler h) throws Exception {// 拷貝一份接口Class(接口可能有多個,所以拷貝的Class也有多個)final Class<?>[] interfaceCls = interfaces.clone();// 這里簡化處理,只取第一個Class<?> copyClazzOfInterface = interfaceCls[0];// 獲取Proxy帶InvocationHandler參數的那個有參構造器Constructor<?> constructor = copyClazzOfInterface.getConstructor(MyInvocationHandler.class);// 創建一個Proxy代理對象,并把InvocationHandler塞到代理對象內部,返回代理對象return constructor.newInstance(h);}}/*** 山寨InvocationHandler接口*/
interface MyInvocationHandler {Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

也就是說,上面的設計思路就是之前分析的這張圖:

目前上面的MyProxy有兩個問題沒解決:

  • 返回的代理對象只是Proxy類型的,沒法強轉為目標接口類型
  • 返回的代理對象即使能調用接口的同名方法,如何最終調用到它內部的InvocationHandler#invoke()呢

底層原理

上面遺留的兩個問題,其實換種說法就是:

  • 怎么讓MyProxy的實例對象變成代理類的對象呢(比如Calculator)?
  • InvocationHandler#invoke()怎么調用到目標對象同名方法?

首先我們要明確,MyProxy(JDK Proxy同理)是一個已經寫好的類,一開始就沒有實現Calculator接口,那么它的實例對象肯定是無法強轉為Calculator的。那么,Java是如何解決這個問題的呢?方式很簡單粗暴,因為JVM確確實實在運行時動態構造了代理類,并讓代理類實現了接口,也就是我們經常看到的$Proxy0。

也就是說,我們通常理解的代理對象,并不是JDK Proxy的直接實例對象,而是JDK Proxy的子類$Proxy0的實例對象,而$Proxy0 extends Proxy implements Calculator

由于$Proxy0是運行時的產物,一旦程序停止便會消失,我們需要借助阿里開源的Arthas工具來觀察并驗證。

假設需要代理的接口是:

/*** 需要代理的接口*/
interface Calculator {int add(int a, int b);
}

當我們期望使用Proxy創建代理對象時,JDK會先動態生成一個代理類$Proxy0:

final class $Proxy0 extends Proxy implements InvocationHandlerTest.Calculator {private static Method m1;private static Method m2;private static Method m3;private static Method m0;public $Proxy0(InvocationHandler invocationHandler) {super(invocationHandler);}static {try {m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);m3 = Class.forName("com.bravo.demo.InvocationHandlerTest$Calculator").getMethod("add", Integer.TYPE, Integer.TYPE);m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);return;}catch (NoSuchMethodException noSuchMethodException) {throw new NoSuchMethodError(noSuchMethodException.getMessage());}catch (ClassNotFoundException classNotFoundException) {throw new NoClassDefFoundError(classNotFoundException.getMessage());}}public final int add(int n, int n2) {try {return (Integer)this.h.invoke(this, m3, new Object[]{n, n2});}catch (Error | RuntimeException throwable) {throw throwable;}catch (Throwable throwable) {throw new UndeclaredThrowableException(throwable);}}public final boolean equals(Object object) {try {return (Boolean)this.h.invoke(this, m1, new Object[]{object});}catch (Error | RuntimeException throwable) {throw throwable;}catch (Throwable throwable) {throw new UndeclaredThrowableException(throwable);}}public final String toString() {try {return (String)this.h.invoke(this, m2, null);}catch (Error | RuntimeException throwable) {throw throwable;}catch (Throwable throwable) {throw new UndeclaredThrowableException(throwable);}}public final int hashCode() {try {return (Integer)this.h.invoke(this, m0, null);}catch (Error | RuntimeException throwable) {throw throwable;}catch (Throwable throwable) {throw new UndeclaredThrowableException(throwable);}}
}

簡化無關代碼:

// 1.自動實現目標接口,所以代理對象可以轉成Calculator
final class $Proxy0 extends Proxy implements InvocationHandlerTest.Calculator {private static Method m3;public $Proxy0(InvocationHandler invocationHandler) {super(invocationHandler);}static {// 2.獲取目標方法Methodm3 = Class.forName("com.bravo.demo.InvocationHandlerTest$Calculator").getMethod("add", Integer.TYPE, Integer.TYPE);}public final int add(int n, int n2) {// 3.通過InvocationHandler執行方法,現在你能理解invoke()三個參數的含義了嗎?//   this:就是$Proxy0的實例,所以是代理對象,不是目標對象return (Integer)this.h.invoke(this, m3, new Object[]{n, n2});}}

最后一個問題是,代理對象$proxy調用add()時,是如何最終調用到目標對象的add()方法的呢?觀察上面的代碼可以發現,代理對象的方法調用都是通過this.h.invoke()橋接過去的,而這個h就是InvocationHandler,在$Proxy的父類Proxy中已經存在,而且會被賦值。

我編寫的MyProxy基本上就是簡化版的JDK Proxy,沒有本質的區別。只不過JVM只認識JDK Proxy,只會給它生成動態代理類,所以我的MyProxy即使模仿到99.99%,也注定少了最關鍵的那一步,最終淪為一段玩具代碼。

希望這篇文章能幫大家更了解JDK動態代理。至于Java是如何自動生成$Proxy代理類的,交給大家另外研究。很多讀者讓我講講CGLib,其實沒啥好講的,就是API使用而已,底層也是自動生成代理類/代理對象,和JDK動態代理很相似,只不過CGLib底層用的是ASM,感興趣可以去百度一下。

作者簡介:大家好,我是smart哥,前中興通訊、美團架構師,現某互聯網公司CTO

進群,大家一起學習,一起進步,一起對抗互聯網寒冬

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/166788.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/166788.shtml
英文地址,請注明出處:http://en.pswp.cn/news/166788.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

上門預約小程序開發優勢

想要放松身心&#xff0c;享受按摩的舒適感&#xff1f;那就需要一個專業的按摩師來上門服務。我們開發的預約按摩小程序app系統&#xff0c;匯聚各類上門按摩服務&#xff0c;包括推拿SPA、小兒推拿、中醫等&#xff0c;為您提供高價值、高標準的養生健康體驗。24小時隨時提供…

GEE土地分類——使用隨機森林方法和多源遙感數據進行面向對象的土地分類NAIP數據為例

簡介: 數據: 國家農業圖像計劃 (NAIP) 在美國大陸的農業生長季節獲取航空圖像。 NAIP 項目每年根據可用資金和圖像獲取周期簽訂合同。從 2003 年開始,NAIP 以 5 年為一個周期。2008 年是過渡年,2009 年開始采用 3 年周期。 NAIP 圖像以一米的地面采樣距離 (GSD) 采集,水…

【前端】讓列表像Excel單元格一樣編輯

前言 領導說了一堆的話,最后總結一句就是客戶很懶,客戶的員工更加懶。 本著讓別人節省時間的原則,提倡出了讓列表和Excal的單元格一樣,不僅看數據還可以隨時更改數據。 查資料 根據 Jeecg-Vue3 源碼介紹,從而知道是基于 Vben Admin 開源項目進行改造的。 因此在 Vben…

Sulfo-CY3 NHS熒光染料的制備和表征

Sulfo-CY3 NHS(源自星戈瑞的花菁染料)熒光染料的制備和表征是確保染料質量和性能的關鍵步驟。制備Sulfo-CY3 NHS熒光染料&#xff1a; 原材料準備&#xff1a;準備所需的原材料&#xff0c;包括CY3 NHS ester&#xff08;或等效的前體&#xff09;&#xff0c;用于制備Sulfo-C…

沉頭孔和埋頭孔的區別

埋頭空和沉頭孔的區別在于螺栓孔上部擴孔&#xff1a;沉頭孔是直筒結構&#xff1b;埋頭孔是四十五度結構&#xff0c;比沉頭孔較為平順。 螺栓孔上部擴孔能容納螺栓頭部&#xff0c;使螺頭部不高于周圍表面。埋頭空和沉頭孔只是兩種不同的叫法。 沉頭孔是 PCB 上的圓柱形凹槽…

RK3568 支持4x4矩陣鍵盤

在對應的設備樹添加: keypad {compatible = "gpio-matrix-keypad";pinctrl-names = "default";pinctrl-0 = <&GPIO3_A1_pin&GPIO1_D3_pin&GPIO1_D4_pin&GPIO1_C7_pin&GPIO1_D2_pin&GPIO1_D1_pin&GPIO1_D0_pin&GPIO3_A…

將form表單中的省市區的3個el-select下拉框的樣式調成統一的間隔距離和長度,vue3項目iot->供應商管理

省市區是用3個el-select組成的 在表單中用el-col&#xff0c;會導致3個下拉的距離不統一&#xff0c;市和區的前面也是不需要文字label的 如何解決:用vue3的:deep()進行樣式穿透&#xff0c;由于el-form-item標簽都是一樣的&#xff0c;為了能準確的找到市的el-form-item&…

解決:前端js下載文件流出現“未知文件格式”錯誤

第一中情況&#xff1a; 出現的問題&#xff0c;前端已經設置了responseType: blob,下載下來還是格式不對。 最后經過排查&#xff0c;后端缺少charsetutf-8&#xff0c;所以前端可以設置編碼&#xff1a; 第二中情況&#xff1a; 后端已經設置了charsetutf-8&#xff0c;前…

機器學習/sklearn 筆記:K-means,kmeans++,MiniBatchKMeans,二分Kmeans

1 K-means介紹 1.0 方法介紹 KMeans算法通過嘗試將樣本分成n個方差相等的組來聚類&#xff0c;該算法要求指定群集的數量。它適用于大量樣本&#xff0c;并已在許多不同領域的廣泛應用領域中使用。KMeans算法將一組樣本分成不相交的簇&#xff0c;每個簇由簇中樣本的平均值描…

JS實現數組去重

數組去重&#xff0c;一般都是在面試的時候才會碰到&#xff0c;一般是要求手寫數組去重方法的代碼。如果是被提問到&#xff0c;數組去重的方法有哪些&#xff1f;你能答出其中的 10 種&#xff0c;面試官很有可能對你刮目相看。 在真實的項目中碰到的數組去重&#xff0c;一般…

數據結構-樹

參考&#xff1a;https://www.hello-algo.com/chapter_tree/binary_tree/#711 1. 介紹 樹存儲不同于數組和鏈表的地方在于既可以保證數據檢索的速度&#xff0c;又可以保證數據插入刪除修改的速度&#xff0c;二者兼顧。 二叉樹是一種很重要的數據結構&#xff0c;是非線性的…

【學習篇】Linux中grep、sed、awk

Linux 文本處理三劍客 – awk, sed, grep grep過濾文本 https://zhuanlan.zhihu.com/p/561445240 grep 是 Linux/Unix 系統中的一個命令行工具&#xff0c;用于從文件中搜索文本或字符串。grep 代表全局正則表達式打印。當我們使用指定字符串運行 grep 命令時&#xff0c;如…

Mysql并發時常見的死鎖及解決方法

使用數據庫時&#xff0c;有時會出現死鎖。對于實際應用來說&#xff0c;就是出現系統卡頓。 死鎖是指兩個或兩個以上的事務在執行過程中&#xff0c;因爭奪資源而造成的一種互相等待的現象。就是所謂的鎖資源請求產生了回路現象&#xff0c;即死循環&#xff0c;此時稱系統處于…

星河創新,開拓新紀!2023“星河產業應用創新獎”報名全面開啟!

科技的浪潮洶涌而至&#xff0c;人工智能正悄無聲息地滲透進我們生活的每一個角落&#xff0c;成為推動社會奔騰向前的強大引擎。 隨著大模型時代到來&#xff0c;更多的創新者涌現出來&#xff0c;他們正積極探索AI與實體的深度融合&#xff0c;解決行業難題&#xff0c;開拓…

算法的奧秘:種類、特性及應用詳解(算法導論筆記1)

算法&#xff0c;是計算機科學領域的靈魂&#xff0c;是解決問題的重要工具。在算法的世界里&#xff0c;有著各種各樣的種類和特性。今天&#xff0c;我將帶各位踏上一段探索算法種類的旅程&#xff0c;分享一些常見的算法種類&#xff0c;并給出相應的實踐和案例分析。希望通…

c# 微信小程序支付,訂單錄入發貨

微信改動&#xff0c;大家一起改&#xff0c;來吧 private string GetAccessToken(string openid){string AppID "";string AppSecret "";string url "https://api.weixin.qq.com/cgi-bin/token?grant_typeclient_credential&appid"AppI…

華納云:Linux每天自動備份mysql數據庫怎么實現

在 Linux 系統中&#xff0c;你可以使用 cron 任務來定期執行 MySQL 數據庫備份。以下是一個簡單的步驟&#xff0c;演示如何設置每天自動備份 MySQL 數據庫&#xff1a; 創建備份腳本&#xff1a; 創建一個 Shell 腳本&#xff0c;其中包含備份 MySQL 數據庫的命令。假設腳本名…

【目標檢測】保姆級別教程從零開始實現基于Yolov8的一次性筷子計數

前言 一&#xff0c;環境配置 一&#xff0c;虛擬環境創建 二&#xff0c;安裝資源包 前言 最近事情比較少&#xff0c;無意間刷到群聊里分享的基于百度飛漿平臺的一次性筷子檢測&#xff0c;感覺很有意思&#xff0c;恰巧自己最近在學習Yolov8&#xff0c;于是看看能不能復…

前端JS數據時間排序

一、sort()方法 var data [ { name:‘1’, time:‘2019-04-26 10:53:19’ }, { name:‘2’, time:‘2019-04-26 10:51:19’ },{ name:‘3’, time:‘2019-04-26 11:04:32’ },{ name:‘4’, time:‘2019-04-26 11:05:32’ } ] data.sort(function(a,b){ return a.time < b…

js進階筆記之作用域

目錄 全局作用域 局部作用域 函數作用域 塊作用域 作用域鏈 閉包 垃圾回收機制 作用域&#xff08;scope&#xff09;規定了變量能夠被訪問的“范圍”&#xff0c;離開了這個“范圍”變量便不能被訪問&#xff0c;作用域分為全局作用域和局部作用域。 全局作用域 <…