面試官:為什麼jdk動態代理只能代理介面實現類?
**本文首發於公眾號【看點程式碼再上班】,歡迎圍觀,第一時間獲取最新文章。**
原文地址:https://mp.weixin.qq.com/s/LWToCjGGP52_0cy9xkiHlQ
大家好,我是tin,這是我的第15篇原創文章
我們日常業務能夠使用jdk動態代理編碼的場景非常少,但是我們使用的框架用到jdk動態代理的卻非常多,今天結合jdk動態代理原始碼講一講“為什麼jdk動態代理只能代理介面?”,先上一個目錄:
-
二、復現問題“jdk動態代理不能代理普通類”
-
三、深入原始碼分析
-
四 、結語
一、把jdk動態代理跑起來
顧名思義,jdk動態代理是jdk實現的一個功能,不需要第三方庫支援,我們配置好依賴jdk到我們的工程即可使用,要把jdk動態代理跑起來,非常簡單:
-
業務目標物件實現介面;
-
實現InvocationHandler介面;
-
使用Proxy.newProxyInstance生成代理物件;
定義業務BookFacade介面:
BookFacadeImpl介面實現:
定義MyInvocationHandler實現InvocationHandler介面:
package com.tin.example.jdk.proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; /** * title: MyInvocationHandler * <p> * description: * * @author tin @公眾號【看點程式碼再上班】 on 2022/1/22 上午9:01 */ public class MyInvocationHandler implements InvocationHandler { //目標物件 private Object target; public Object getInstance(Object target) { this.target = target; Class clazz = this.target.getClass(); // Proxy.newProxyInstance的三個引數分別是: // 1 被代理類的類載入器 // 2 被代理類的介面 // 3 java.lang.reflect.InvocationHandler return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("------前置通知------------"); // 執行目標方法 Object result = method.invoke(target, args); System.out.println("------後置處理------------"); return result; } }
MyInvocationHandler類內的Proxy.newProxyInstance生成代理物件。通過以上類定義,我們來寫一個main測試類:
package com.tin.example.jdk.proxy; /** * title: JdkProxyTest * <p> * description: * * @author tin @公眾號【看點程式碼再上班】 on 2022/1/22 上午9:11 */ public class JdkProxyTest { public static void main(String[] args) throws Exception { //sun.misc.ProxyGenerator.saveGeneratedFiles 用於輸出代理類class檔案到本地 System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); //代理介面 BookFacade proxy = (BookFacade) new MyInvocationHandler().getInstance(new BookFacadeImpl()); proxy.addBook("Java效能權威指南@【看點程式碼再上班】"); } }
main方法執行結果如下:
因為我的測試類已經加了以下程式碼:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
這是用於把生成的proxy代理類的class檔案輸出到本地的,開啟生成的class檔案,內容如下:
同樣的,生成的代理類實現了目標介面定義的方法addBook():
到這裡就可以初步回答我們標題的問題了:
jdk動態代理生成的代理類實現了我們業務定義的介面,並重寫了我們介面的方法,如此才實現代理的功能,所以我們的目標類需要實現介面。
二、復現問題“jdk動態代理不能代理普通類”
看了以上示例估計就有朋友反問了,所舉例子只是一個特例,說明介面實現類可以被jdk代理,非介面實現類也可以吧,不一定是通過"implements"方式實現,這就是我們這小節的問題“jdk動態代理能不能代理普通類?”。
新建一個沒有實現介面的類BookOperation:
public class BookOperation { public void addBook(String bookName) { System.out.println("新增書籍, bookName:" + bookName); } }
Test測試類如下:
public class JdkProxyTest { public static void main(String[] args) throws Exception { //sun.misc.ProxyGenerator.saveGeneratedFiles 用於輸出代理類class檔案到本地 System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); //代理普通類 BookOperation bookOperation = (BookOperation) new MyInvocationHandler().getInstance(new BookOperation()); bookOperation.addBook("Java效能權威指南@【看點程式碼再上班】"); } }
依然通過類強轉的方式獲取生成的類:
BookOperation bookOperation = (BookOperation) new MyInvocationHandler().getInstance(new BookOperation());
有朋友可能會反問,它不一定是生成BookOperation類,這樣取是不是不合適的?是的,這樣的提問是沒問題的,但是如果不強轉,我不知道如何能夠呼叫我們的目標方法。
為了能夠驗證需要型別強轉且確確實實無法代理非介面實現類,我們看下面的執行結果以及生成的代理類的結構。
執行結果如下:
從拋的異常來看,代理類Proxy已經生成,但不是我想要的BookOperation的代理類!型別強轉失敗。
既然不是我指定的目標類的代理類,那麼生成的Proxy類究竟是怎麼樣的呢?
見下圖:
和我們前面的Proxy最大的不同在於沒有實現介面(因為BookOperation沒有實現介面,這是必然的), 整個Proxy內也沒有addBook()方法。
既然沒有目標類的方法,代理類如何執行我們的目標方法?
“ 既然沒有介面,那為什麼不能繼承BookOperation類完成代理功能?! ”會有人這麼想。
這樣想就對了,這個也就是cglib採用的方式,具體原理請閱讀以下文章啦:
到這裡,我們可以再次確定標題所問之答案:jdk動態代理無法代理非介面實現類。
三、深入原始碼分析
以上只是通過表層分析我們的jdk動態代理無法代理非介面實現類。是否真如此呢?我們通過原始碼看一看。
jdk代理的關鍵在代理類的生成。我們從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; } }); } /* * ③ 根據我們定義的invocation handler生成代理物件 */ return cons.newInstance(new Object[]{h}); } catch (IllegalAccessException|InstantiationException e) { throw new InternalError(e.toString(), e); } catch (InvocationTargetException e) { Throwable t = e.getCause(); if (t instanceof RuntimeException) { throw (RuntimeException) t; } else { throw new InternalError(t.toString(), t); } } catch (NoSuchMethodException e) { throw new InternalError(e.toString(), e); } }
Proxy.newProxyInstance()關鍵在兩步:
-
① Class<?> cl = getProxyClass0(loader, intfs) 生成代理類;
-
② return cons.newInstance(new Object[]{h}) 生成代理物件;
第①點,生成代理類,就是生成我們以上com/sun/proxy/$Proxy0.class代理類的位元組碼,繼續跟進去看一下。
getProxyClass0方法程式碼很少,其內部直接呼叫了proxyClassCache.get方法:
proxyClassCache是一個本地靜態變數也是一個快取cache,其構造器的兩個入參KeyFactory和ProxyClassFactory至關重要,分別對應 keyFactory和valueFactory :
繼續進入到proxyClassCache.get()方法內部,如下:
看上圖的第③點,獲取最終的代理類value值重點就在於Factory的get()方法:
前面講proxyClassCache的構造器入參時已經提到valueFactory對應的就是ProxyClassFactory。valueFactory.apply其實對應的是ProxyClassFactory#apply。
我們繼續跟進去看看ProxyClassFactory#apply方法邏輯(ProxyClassFactory也是Proxy的一個內部類):
第①點,
interfaceClass = Class.forName(intf.getName(), false, loader);
驗證入參類載入器按全限定名稱重新載入入參介面得到的class物件是否和介面class物件相同,主要也是為了驗證類載入器是否相同,因為同一個類載入器載入的class才相同,否則不相同:
第①點,
if (!interfaceClass.isInterface())
驗證入參class物件是否是介面。
其實,如果目標類沒有實現介面,interfaces陣列會為空,不會走到這裡,我們啟動非介面實現類debug如下:
interfaces為空,會導致最終生成的代理類缺少相關的方法。繼續跟進到第③點 生成代理類 一看究竟。
sun.misc.ProxyGenerator#generateProxyClass(java.lang.String, java.lang.Class<?>[], int)方法如下:
最終生成class是在generateClassFile(), 該方法生成類的方法和屬性等資訊,它會遍歷interfaces中的每一個介面class,把class的每一個方法包裝為ProxyMethod物件,最後通過ProxyMethod生成代理類的代理方法並輸出到位元組流中,如下圖:
從以上圖示可以很明顯的看出來,非介面實現類傳的引數interfaces是一個空陣列,最後generateClassFile()無法生成對應的代理方法。
以上,從原始碼維度也佐證了jdk動態代理依賴介面,只有實現了介面的類的方法才能被加入到代理類中生成代理方法,最終完成代理功能。
四、結語
我是tin,一個在努力讓自己變得更優秀的普通工程師。自己閱歷有限、學識淺薄,如有發現文章不妥之處,非常歡迎加我提出,我一定細心推敲並加以修改。
堅持創作不容易,你的正反饋是我堅持輸出的最強大動力,謝謝!
最後別忘了關注我公眾號哦【看點程式碼再上班】!附上原文連結:arrow_double_down::arrow_double_down::arrow_double_down:
https://mp.weixin.qq.com/s/LWToCjGGP52_0cy9xkiHlQ
- 面試官:cglib為什麼不能代理private方法?
- 想看Dubbo原始碼?一定要先看一看這一篇!
- 死磕synchronized二:系統剖析延遲偏向篇一
- 架構與思維:高併發下解決主從延時的一些思路
- 道與術
- OopMap看不懂,怎麼調優哇
- Kafka 精妙的高效能設計(下篇)
- 一次tcp視窗被填滿問題的排查實踐
- 搶了個票,還以為發現了12306的系統BUG
- 微服務5:服務註冊與發現(實踐篇)
- 看一遍就理解:零拷貝詳解
- 分散式:分散式系統下的唯一序列
- 面試官:為什麼jdk動態代理只能代理介面實現類?
- 揭開記憶體屏障的神祕面紗
- 微服務4:服務註冊與發現
- 我就奇了怪了,STW到底是怎麼做到的
- 這樣使用 IDEA ,效率提升10倍!| IDEA 高效使用指南
- 從hotspot原始碼層面剖析Java的多型實現原理
- 垃圾回收全集之十二:GC 調優的實戰篇—Weak, Soft 及 Phantom 引用
- JVM的多型是如何實現的