Java實(shí)現(xiàn)JDK動態(tài)代理的原理詳解
概念
代理:為控制A對象,而創(chuàng)建出新B對象,由B對象代替執(zhí)行A對象所有操作,稱之為代理。一個代理體系建立涉及到3個參與角色:真實(shí)對象(A),代理對象(B),客戶端。
其中的代理對象(B)起到中介作用,連通真實(shí)對象(A)與客戶端,如果進(jìn)一步拓展,代理對象可以實(shí)現(xiàn)更加復(fù)雜邏輯,比如對真實(shí)對象進(jìn)行訪問控制。
案例
需求:員工業(yè)務(wù)層接口調(diào)用save需要admin權(quán)限,調(diào)用list不需要權(quán)限,沒權(quán)限調(diào)用時拋出異常提示。
靜態(tài)代理
/** * 代理接口 */ public interface IEmployeeService { void save(); void list(); }
/** * 真實(shí)對象 */ public class EmployeeServiceImpl implements IEmployeeService { @Override public void save() { System.out.println("EmployeeServiceImpl-正常的save...."); } @Override public void list() { System.out.println("EmployeeServiceImpl-正常的list...."); } }
/** * 模擬當(dāng)前登錄用戶對象 */ public class SessionHolder { private static String currentUser; public static String getCurrentUser(){ return currentUser; } public static void setCurrentUser(String currentUser){ SessionHolder.currentUser = currentUser; } }
/** * 代理對象 */ public class EmployeeProxy implements IEmployeeService { //真實(shí)對象 private EmployeeServiceImpl employeeService; public EmployeeProxy(EmployeeServiceImpl employeeService){ this.employeeService = employeeService; } @Override public void save() { //權(quán)限判斷 if("admin".equals(SessionHolder.getCurrentUser())){ employeeService.save(); }else{ throw new RuntimeException("當(dāng)前非admin用戶,不能執(zhí)行save操作"); } } @Override public void list() { employeeService.list(); } }
public class App { public static void main(String[] args) { System.out.println("----------------真實(shí)對象--------------------"); EmployeeServiceImpl employeeService = new EmployeeServiceImpl(); employeeService.list(); employeeService.save(); System.out.println("----------------代理對象--------------------"); SessionHolder.setCurrentUser("dafei"); //設(shè)置權(quán)限(當(dāng)前登錄用戶) EmployeeProxy employeeProxy = new EmployeeProxy(employeeService); employeeProxy.list(); employeeProxy.save(); } }
----------------真實(shí)對象-------------------- EmployeeServiceImpl-正常的list.... EmployeeServiceImpl-正常的save.... ----------------代理對象-------------------- EmployeeServiceImpl-正常的list.... Exception in thread "main" java.lang.RuntimeException: 當(dāng)前非admin用戶,不能執(zhí)行save操作 at com.langfeiyes.pattern.proxy.demo.EmployeeProxy.save(EmployeeProxy.java:20) at com.langfeiyes.pattern.proxy.demo.App.main(App.java:16)
使用真實(shí)對象EmployeeServiceImpl 直接調(diào)用時,不管是list 還是save都能直接訪問,但不符合需求上的admin權(quán)限限制。如果使用代理對象EmployeeProxy,可以完成需求實(shí)現(xiàn)。
通過直接創(chuàng)建新類新類代理對象方式完成代理邏輯,這種方式稱之為靜態(tài)代理模式。
JDK動態(tài)代理模式
Java常用的動態(tài)代理模式有JDK動態(tài)代理,也有cglib動態(tài)代理,此處重點(diǎn)講解JDK的動態(tài)代理
還是原來的需求,前面的IEmployeeService EmployeeServiceImpl SessionHolder 都沒變,新加一個JDK代理控制器-EmployeeInvocationHandler
/** * jdk動態(tài)代理控制類,由它牽頭代理類獲取,代理方法的執(zhí)行 */ public class EmployeeInvocationHandler implements InvocationHandler { //真實(shí)對象-EmployeeServiceImpl private Object target; public EmployeeInvocationHandler(Object target){ this.target = target; } //獲取jvm在內(nèi)存中生成代理對象 public Object getProxy(){ return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); } //代理對象控制執(zhí)行方法 //參數(shù)1:代理對象 //參數(shù)2:真實(shí)對象的方法(使用方式得到方法對象) //參數(shù)3:真實(shí)對象方法參數(shù)列表 //此處是代理對象對外暴露的可編輯的方法處理場所,代理對象每調(diào)用一個次方法,就會執(zhí)行一次invoke @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String name = method.getName(); if("save".equals(name) && !"admin".equals(SessionHolder.getCurrentUser())){ throw new RuntimeException("當(dāng)前非admin用戶,不能執(zhí)行save操作"); } return method.invoke(target, args); } }
測試App類稍微改動下:
public class App { public static void main(String[] args) { System.out.println("----------------真實(shí)對象--------------------"); EmployeeServiceImpl employeeService = new EmployeeServiceImpl(); employeeService.list(); employeeService.save(); System.out.println("----------------代理對象--------------------"); SessionHolder.setCurrentUser("dafei"); EmployeeInvocationHandler handler = new EmployeeInvocationHandler(employeeService); IEmployeeService proxy = (IEmployeeService) handler.getProxy(); proxy.list(); proxy.save(); } }
上面代碼一樣可以實(shí)現(xiàn)需求,跟靜態(tài)代理區(qū)別就在于少創(chuàng)建了代理對象。此時存在疑問點(diǎn),沒有創(chuàng)建代理對象,為啥可以實(shí)現(xiàn)代理類調(diào)用呢??
原理分析
先拋出結(jié)論JDK動態(tài)代理底層實(shí)現(xiàn)原理:使用接口實(shí)現(xiàn)方式,運(yùn)行時,在內(nèi)存中動態(tài)構(gòu)建出一個類,然后編譯,執(zhí)行。這個類是一次性的,JVM停止,代理類就消失。
參與角色 要理解JDK動態(tài)代理原理,首先得了解JDK動態(tài)代理涉及到的類
InvocationHandler:真實(shí)對象方法調(diào)用處理器,內(nèi)置invoke方法,其功能:為真實(shí)對象定制代理邏輯
EmployeeInvocationHandler:員工服務(wù)真實(shí)對象方法調(diào)用處理器,此類有3個用途: 1>設(shè)置真實(shí)對象
//真實(shí)對象-EmployeeServiceImpl private Object target; public EmployeeInvocationHandler(Object target){ this.target = target; }
2>定制代理方法實(shí)現(xiàn)邏輯
為真實(shí)對象save方法添加了權(quán)限校驗(yàn)邏輯
//代理對象控制執(zhí)行方法 //參數(shù)1:代理對象 //參數(shù)2:真實(shí)對象的方法(使用方式得到方法對象) //參數(shù)3:真實(shí)對象方法參數(shù)列表 //此處是代理對象對外暴露的可編輯的方法處理場所,代理對象每調(diào)用一個次方法,就會執(zhí)行一次invoke @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String name = method.getName(); if("save".equals(name) && !"admin".equals(SessionHolder.getCurrentUser())){ throw new RuntimeException("當(dāng)前非admin用戶,不能執(zhí)行save操作"); } return method.invoke(target, args); }
3>返回代理對象
方法執(zhí)行完之后,返回一個名為:$ProxyX的代理類(其中的X是序號,一般默認(rèn)為0),這代理類由JDK動態(tài)構(gòu)建出來。
//獲取jvm在內(nèi)存中生成代理對象 public Object getProxy(){ return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); }
Proxy:動態(tài)代理控制類,是JDK動態(tài)生成的$ProxyX類的父類,它作用如下:
1>通過調(diào)用ProxyBuilder 類builder方法構(gòu)建代理對象類
private static Constructor<?> getProxyConstructor(Class<?> caller, ClassLoader loader, Class<?>... interfaces){ return proxyCache.sub(intf).computeIfAbsent( loader, (ld, clv) -> new ProxyBuilder(ld, clv.key()).build() ); }
2>通過newProxyInstance方法返回$ProxyX類的實(shí)例
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) { //... }
$Proxy0:App類運(yùn)行時,JDK動態(tài)構(gòu)建出來的代理類,繼承至Proxy類
public class App { public static void main(String[] args) { //System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true"); System.out.println("----------------真實(shí)對象--------------------"); EmployeeServiceImpl employeeService = new EmployeeServiceImpl(); employeeService.list(); employeeService.save(); System.out.println("----------------代理對象--------------------"); SessionHolder.setCurrentUser("dafei"); EmployeeInvocationHandler handler = new EmployeeInvocationHandler(employeeService); IEmployeeService proxy = (IEmployeeService) handler.getProxy(); proxy.list(); proxy.save(); } }
默認(rèn)情況下JVM是不保存動態(tài)創(chuàng)建代理類字節(jié)碼對象的,可以在main方法中配置代理參數(shù)讓字節(jié)碼保留
//JDK8之前 System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); //JDK8之后 System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
執(zhí)行完之后,會在項(xiàng)目根目錄生成代理類字節(jié)碼對象。
為了方便解讀,將一些不需要的方法剔除之后
$Proxy0類
public class $Proxy0 extends Proxy implements IEmployeeService { private static Method m4; private static Method m3; static { try { m4 = Class.forName("com.langfeiyes.proxy.demo.IEmployeeService") .getMethod("save"); m3 = Class.forName("com.langfeiyes.proxy.demo.IEmployeeService") .getMethod("list"); } catch (Exception e) { e.printStackTrace(); } } public $Proxy0(InvocationHandler var1) throws Throwable { super(var1); } public final void save() throws Throwable { super.h.invoke(this, m4, (Object[])null); } public final void list() throws Throwable{ super.h.invoke(this, m3, (Object[])null); } }
從源碼上看,$Proxy0的特點(diǎn):
- 1>繼承了Proxy類,實(shí)現(xiàn)了IEmployeeService 接口
- 2>通過靜態(tài)塊的方式反射IEmployeeService接口save與list方法,得到他們的方法對象Method
- 3>調(diào)用父類構(gòu)造器,需要傳入InvocationHandler 參數(shù)
- 4>重寫IEmployeeService接口的save list方法靠的是父類Proxy的h屬性.invoke方法
真相大白
下圖所有參與動態(tài)代理的類:
下圖是上圖的操作時序圖,跟著走就對了
到這,JDK動態(tài)代理就ok了。
相關(guān)文章
java學(xué)習(xí)指南之字符串與正則表達(dá)式
在日常Java后端開發(fā)過程中,免不了對數(shù)據(jù)字段的解析,自然就少不了對字符串的操作,這其中就包含了正則表達(dá)式這一塊的內(nèi)容,下面這篇文章主要給大家介紹了關(guān)于java學(xué)習(xí)指南之字符串與正則表達(dá)式的相關(guān)資料,需要的朋友可以參考下2023-05-05Ubuntu安裝JDK與IntelliJ?IDEA的詳細(xì)過程
APT是Linux系統(tǒng)上的包管理工具,能自動解決軟件包依賴關(guān)系并從遠(yuǎn)程存儲庫中獲取安裝軟件包,這篇文章主要介紹了Ubuntu安裝JDK與IntelliJ?IDEA的過程,需要的朋友可以參考下2023-08-08Java?基于Hutool實(shí)現(xiàn)DES加解密示例詳解
這篇文章主要介紹了Java基于Hutool實(shí)現(xiàn)DES加解密,本文通過示例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-08-08Spring cloud踩坑記錄之使用feignclient遠(yuǎn)程調(diào)用服務(wù)404的方法
這篇文章主要給大家介紹了關(guān)于Spring cloud踩坑記錄之使用feignclient遠(yuǎn)程調(diào)用服務(wù)404的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11Java開發(fā)或調(diào)用WebService的幾種方式總結(jié)
java開發(fā)過程中,很多地方都會遇到數(shù)據(jù)傳遞,遠(yuǎn)程獲取數(shù)據(jù)問題,這篇文章主要介紹了Java開發(fā)或調(diào)用WebService的幾種方式的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-06-06Java中4種校驗(yàn)注解詳解(值校驗(yàn)、范圍校驗(yàn)、長度校驗(yàn)、格式校驗(yàn))
這篇文章主要給大家介紹了關(guān)于Java中4種校驗(yàn)注解詳解的相關(guān)資料,分別包括值校驗(yàn)、范圍校驗(yàn)、長度校驗(yàn)、格式校驗(yàn)等,Java注解(Annotation)是一種元數(shù)據(jù),它可以被添加到Java代碼中,并可以提供額外的信息和指令,需要的朋友可以參考下2023-08-08一文詳解Spring任務(wù)執(zhí)行和調(diào)度(小結(jié))
這篇文章主要介紹了一文詳解Spring任務(wù)執(zhí)行和調(diào)度(小結(jié)),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08