淺談Java動態(tài)代理的實現(xiàn)
一、代理設(shè)計模式
1.1 什么是代理
- 考慮真實的編程場景,項目中存在一個訪問其他數(shù)據(jù)源的接口,包含一個
query()
方法 - 我們已經(jīng)針對這個接口,實現(xiàn)了MySQL、Hive、HBase、MongoDB等作為數(shù)據(jù)源的實現(xiàn)類
- 但是,在測試過程中,我們發(fā)現(xiàn)這些數(shù)據(jù)源的查詢并不是很穩(wěn)定
- 最原始的想法: 在所有實現(xiàn)類
query()
方法中,代碼首部獲取startTime,代碼尾部獲取endTime,通過打印日志的方式,知道每次查詢的耗時
long startTime = System.currentTimeMillis(); logger.info("query mysql start:" + new Date(startTime).toLocaleString()); // 具體的query代碼 ... long endTime = System.currentTimeMillis(); logger.info(String.format("query mysql end: %s, consumed time: %dms", new Date(endTime).toLocaleString(), (endTime-startTime)));
- 直接修改已經(jīng)實現(xiàn)的方法,存在很多缺點:
(1)現(xiàn)在是打印日志,代碼非常簡單,就算query()
方法不是你實現(xiàn)的,你也能很好的完成。
(2)但是如果是其他功能呢?比如,如果查詢失敗,要求你查詢重試
- 因此,在不改變已經(jīng)實現(xiàn)好的
query()
方法前提下,去實現(xiàn)日志打印的功能是最好的方法。 - 進階想法: 我為每個實現(xiàn)類創(chuàng)建一個
包裝
類。
(1)這個包裝
類與實現(xiàn)類一樣,實現(xiàn)了相同的接口。
(2)在query()
方法中,直接調(diào)用實現(xiàn)類的query()
方法,并在調(diào)用前后進行日志打印
(3)對實現(xiàn)類方法的調(diào)用,都改成對包裝
類方法的調(diào)用
long startTime = System.currentTimeMillis(); logger.info("query mysql start:" + new Date(startTime).toLocaleString()); // 使用try-finally JSONObject[] data = null; try { data = mysql.query(); return data; } catch (Exception exception) { throw exception; } finally { long endTime = System.currentTimeMillis(); logger.info(String.format("query mysql end: %s, consumed time: %dms", new Date(endTime).toLocaleString(), (endTime - startTime))); }
- 這時,代理模式的概念就變得非常清晰了:不直接調(diào)用實現(xiàn)類的某個方法,而是通過實現(xiàn)類的代理去調(diào)用。
- 這樣不僅可以實現(xiàn)調(diào)用者與被調(diào)用者之間的解耦合,還可以在不修改調(diào)用者的情況下,豐富功能邏輯。
1.2 代理模式入門
代理模式的UML圖如下
1.subject:
抽象主題角色,是一個接口,定義了一系列的公共對外方法
2.real subject:
真實主題角色,也就是我剛剛提到的實現(xiàn)類,又稱委托類。
委托類實現(xiàn)抽象主題,負責實現(xiàn)具體的業(yè)務(wù)邏輯
3.proxy:
代理主題角色,簡稱代理類。它也實現(xiàn)了抽象主題,用于代理、封裝,甚至增強委托類。
一般通過內(nèi)含委托類,實現(xiàn)對委托類的封裝
4.client:
當訪問具體的業(yè)務(wù)邏輯時,clinet表面是訪問代理類,而實際是訪問被代理類封裝的委托類
代理模式的應(yīng)用場景:目前,就我本人所接觸的使用場景,就是通過代理去打印日志、增強業(yè)務(wù)邏輯 😂
二、Java代理的三種實現(xiàn)
2.1 靜態(tài)代理
- 所謂的靜態(tài)和動態(tài),是相對于字節(jié)碼的生成時機來說的:
(1)靜態(tài)是指字節(jié)碼,即class文件,在編譯時就已經(jīng)生成。
(2)動態(tài)是指字節(jié)碼在運行時動態(tài)生成,而不是編譯時提前生成
- 剛剛,我們針對數(shù)據(jù)查詢的進階方法,實際就是靜態(tài)代理
- 通過為每個委托類創(chuàng)建對應(yīng)的代理類,然后編譯時就可以得到代理類的字節(jié)碼
下面是一個具體的靜態(tài)代理實例:
抽象主題:
public interface Animal { void eat(); }
委托類:Dog和Cat,實現(xiàn)了抽象接口
public class Dog implements Animal { @Override public void eat() { System.out.println("I like eating bone"); } } public class Cat implements Animal { @Override public void eat() { System.out.println("I like eating fish"); } }
代理類:代理類中含有對應(yīng)的委托類,通過調(diào)用委托類的具體實現(xiàn),來封裝委托類
public class DogProxy implements Animal { private Dog dog; public DogProxy(Dog dog) { this.dog = dog; } @Override public void eat() { System.out.print("I'm a "+dog.getClass().getSimpleName() +". "); dog.eat(); } } public class CatProxy implements Animal { private Cat cat; public CatProxy(Cat cat) { this.cat = cat; } @Override public void eat() { System.out.print("I'm a " + cat.getClass().getSimpleName()+". "); cat.eat(); } }
靜態(tài)代理雖然實現(xiàn)簡單、不更改原始的業(yè)務(wù)邏輯,但是仍然存在以下缺點:
1.如果存在多個委托類,則需要創(chuàng)建多個代理類,這樣則會產(chǎn)生過多的代理類。
2.如果抽象主題增加、刪除、修改方法時,委托類和代理類都需要同時修改,不易維護。
2.2 Java自帶的動態(tài)代理
- 靜態(tài)代理存在以上缺點,如果我們能在程序運行時,動態(tài)生成對應(yīng)的代理類,則無需創(chuàng)建并維護過多的代理類
- 使用Java自帶的
java.lang.reflect.Proxy
和java.lang.reflect.InvocationHandler
類,可以實現(xiàn)動態(tài)代理 - 從類的全路徑可以看出,Java的動態(tài)代理實際利用的是反射機制實現(xiàn)的
- 其中,
Proxy
用于創(chuàng)建對應(yīng)接口的代理類。具體代理的是哪個委托類,是由實現(xiàn)InvocationHandler
接口的中介類決定的 - 代理類、委托類、中介類之間的關(guān)系如下
Java自帶的動態(tài)代理具體實現(xiàn):
1.創(chuàng)建抽象主題 —— 與靜態(tài)代理類一致,不再展示
2.創(chuàng)建實現(xiàn)類 —— 與靜態(tài)代理類一致,不再展示
3.實現(xiàn)InvocationHandler
接口,創(chuàng)建中介類
public class AnimalInvokeHandler implements InvocationHandler { private Animal animal; public AnimalInvokeHandler(Animal animal) { this.animal = animal; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("proxy class: " + proxy.getClass().getName()); System.out.printf("proxy instanceof Animal: %b \n", proxy instanceof Animal); System.out.printf("---- Call method: %s, class: %s ----\n", method.getName(), animal.getClass().getSimpleName()); // 通過反射,調(diào)用委托類的方法 Object result = method.invoke(animal, args); return result; } }
4.通過Proxy
創(chuàng)建動態(tài)代理類,實現(xiàn)對抽象主題的代理
public static void main(String[] args) { // 通過Proxy.newProxyInstance創(chuàng)建代理類 // 將代理類轉(zhuǎn)為抽象主題,可以動態(tài)的創(chuàng)建實現(xiàn)了該主題的代理類 // 必須從實現(xiàn)類去獲取需要代理的接口 // 指定中介類,通過中介類實現(xiàn)代理 Animal proxy = (Animal) Proxy.newProxyInstance(Main.class.getClassLoader(), Dog.class.getInterfaces(), new AnimalInvokeHandler(new Dog())); proxy.eat(); }
執(zhí)行結(jié)果:
Java原生的動態(tài)代理,利用反射動態(tài)生成代理類字節(jié)碼ProxyX.class
,然后將其強制轉(zhuǎn)化為抽象主題類型,就能實現(xiàn)對該接口的代理
jdk動態(tài)代理之所以只能代理接口是因為代理類本身已經(jīng)extends了Proxy,而java是不允許多重繼承的,但是允許實現(xiàn)多個接口
Java原生動態(tài)代理,又叫jdk動態(tài)代理,具有以下優(yōu)缺點
1.優(yōu)點: jdk動態(tài)代理,避免了靜態(tài)代理需要創(chuàng)建并維護過多的代理類的
2.缺點: jdk動態(tài)代理只能代理接口,因為Java的單繼承原則:代理類本身已經(jīng)繼承了Proxy
類,就不能再繼承其他類,只能實現(xiàn)委托類的抽象主題接口。
2.3 cglib實現(xiàn)動態(tài)代理
- jdk動態(tài)代理存在只能代理接口的問題,是十分不方便的。
- 考慮以下場景:
一個類中有兩個方法methodA
:轉(zhuǎn)賬到其他賬戶,methodB
:查詢賬戶余額。
如果用戶訪問methodA
,希望先提示用戶檢查賬戶信息是否正確;
如果用戶訪問methodB
,希望在用戶查詢完余額后,提示用戶關(guān)注銀行的微信公眾號。
- 上述類已經(jīng)成功用于業(yè)務(wù)場景了,我們想要實現(xiàn)這些增強功能,最好不要更改其原始代碼,而是通過代理實現(xiàn)功能增強
- 這時,便可以使用
cglib
實現(xiàn)對類的動態(tài)代理 —— 小白看了實現(xiàn)后,可能會傾向于說這就是類似Java web的攔截器 😂
三、cglib動態(tài)代理的實現(xiàn)
添加maven依賴
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.2.11</version> </dependency>
簡化版的銀行系統(tǒng)類
public class BankSystem { // 轉(zhuǎn)賬 public boolean transferAccount(double amount, String address) { System.out.printf("Send %f dollars to account %s", amount, address); // 轉(zhuǎn)賬成功 return true; } // 查詢賬戶余額 public String queryBalance(){ System.out.printf("Query account balance success"); return "Account balance: 2400 dollars"; } }
為轉(zhuǎn)賬方法創(chuàng)建攔截器
public class TransferInterceptor implements MethodInterceptor { /** * * @param o,表示要增強的對象,其實就是代理中的委托類 * @param method,被攔截的方法,其實就是委托類中被代理的方法 * @param objects,被攔截方法的入?yún)?,如果是基本?shù)據(jù)類型需要傳入包裝類型 * @param methodProxy,對method的代理,通過invokeSuper(),實現(xiàn)對method的調(diào)用 * @return java.lang.Object * @author sunrise * @date 2021/5/23 10:43 上午 **/ @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { before(objects); Object result = methodProxy.invokeSuper(o, objects); return result; } private void before(Object[] args){ System.out.printf("Please check: you will send %.2f dollar to account %s.\n", args[0], args[1]); } }
為余額查詢方法創(chuàng)建攔截器
public class QueryBalanceInterceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { // 調(diào)用預(yù)發(fā)插敘方法 Object result = proxy.invokeSuper(obj, args); after(); return result; } private void after() { System.out.printf("Please pay attention to WeChat official account.\n"); } }
創(chuàng)建filter,實現(xiàn)方法與攔截器的映射
public class BankFilter implements CallbackFilter { @Override public int accept(Method method) { if ("transferAccount".equals(method.getName())) { // 使用攔截器列表中的第1個攔截器 return 0; } // 使用攔截器列表中的第2個攔截器 return 1; } }
使用cglib實現(xiàn)動態(tài)代理 —— 我認為就是方法攔截 😂
public static void main(String[] args) { // 新建攔截器 TransferInterceptor transferInterceptor = new TransferInterceptor(); QueryBalanceInterceptor queryBalanceInterceptor = new QueryBalanceInterceptor(); // 創(chuàng)建工具類 Enhancer enhancer = new Enhancer(); // 設(shè)置委托類,在cglib中,這是cglib需要繼承的超類 enhancer.setSuperclass(BankSystem.class); // 設(shè)置多個攔截器 enhancer.setCallbacks(new Callback[]{transferInterceptor, queryBalanceInterceptor}); // 實現(xiàn)攔截器和方法的映射,即為不同的方法配置不同的攔截器 enhancer.setCallbackFilter(new BankFilter()); // 創(chuàng)建代理類 BankSystem proxy = (BankSystem) enhancer.create(); // 執(zhí)行轉(zhuǎn)賬,調(diào)用TransferInterceptor boolean ok = proxy.transferAccount(1024.28, "lucy"); System.out.println("transfer money success: " + ok); // 查詢余額,調(diào)用QueryBalanceInterceptor proxy.queryBalance(); }
cglib總結(jié):
1.優(yōu)點1: cglib基于類實現(xiàn)動態(tài)代理,通過ASM字節(jié)碼框架動態(tài)生成委托類的子類,并使用方法攔截器實現(xiàn)對委托類方法的攔截。
2.優(yōu)點2: 基于ASM字節(jié)碼框架動態(tài)生成代理類,比jdk動態(tài)生成代理類更加高效
3.缺點: 通過繼承委托類創(chuàng)建動態(tài)代理類,因此不能代理final委托類或委托類中的final方法。
四、面試常見問題
java中代理的實現(xiàn)
共有三種方法:靜態(tài)代理、JDK動態(tài)代理、cglib動態(tài)代理
- 靜態(tài)代理: 為每個實現(xiàn)類都創(chuàng)建一個對應(yīng)的代理類,需要創(chuàng)建并維護大量的代理類
- jdk動態(tài)代理: 通過
Proxy.newProxyInstance()
為抽象主題創(chuàng)建代理類,被代理的委托類包含在InvocationHandler
類中,由InvocationHandler
類的invoke
方法通過反射實現(xiàn)對委托類方法的調(diào)用 - cglib動態(tài)代理:通過ASM字節(jié)碼框架,繼承委托類以創(chuàng)建代理類。代理類通過方法攔截器,實現(xiàn)對委托類方法的攔截
三種代理方式的比較
1.靜態(tài)代理,需要創(chuàng)建并維護大量的委托類
2.jdk動態(tài)代理,避免了靜態(tài)類的上述缺點,但只能代理接口(Java單繼承原則,代理類已經(jīng)繼承了Proxy
類)
3.cglib動態(tài)代理,可以實現(xiàn)對類的代理,并通過方法攔截器實現(xiàn)對委托類(父類)方法的攔截;使用強大的ASM字節(jié)碼框架,更加高效;通過繼承實現(xiàn)對類的代理,使其無法代理final類或類中的final方法
為何調(diào)用代理類的方法,會自動進入InvocationHandler
的invoke()
方法?
- 通過
newProxyInstance()
創(chuàng)建代理類時,會為代理類設(shè)置InvocationHandler
:h
- 動態(tài)生成的代理類字節(jié)碼,通過反編譯可以發(fā)現(xiàn),它實現(xiàn)了抽象主題中的每個方法
- 方法的實現(xiàn),是調(diào)用內(nèi)部成員
h.invoke()
方法
this.h.invoke(this, method, args);
因此,調(diào)用代理類的方法時,實際上會調(diào)用InvocationHandler
的invoke()
方法
到此這篇關(guān)于淺談Java動態(tài)代理的實現(xiàn)的文章就介紹到這了,更多相關(guān)Java動態(tài)代理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
從java反編譯及字節(jié)碼角度探索分析String拼接字符串效率
這篇文章主要介紹了從java反編譯及字節(jié)碼角度探索分析String拼接字符串效率,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-12-12Java實戰(zhàn)之仿天貓商城系統(tǒng)的實現(xiàn)
這篇文章主要介紹了如何利用Java制作一個基于SSM框架的迷你天貓商城系統(tǒng),文中采用的技術(shù)有JSP、Springboot、SpringMVC、Spring等,需要的可以參考一下2022-03-03spring-data-redis自定義實現(xiàn)看門狗機制
redission看門狗機制是解決分布式鎖的續(xù)約問題,本文主要介紹了spring-data-redis自定義實現(xiàn)看門狗機制,具有一定的參考價值,感興趣的可以了解一下2024-03-03使用Java動態(tài)創(chuàng)建Flowable會簽?zāi)P偷氖纠a
動態(tài)創(chuàng)建流程模型,尤其是會簽(Parallel Gateway)模型,是提升系統(tǒng)靈活性和響應(yīng)速度的關(guān)鍵技術(shù)之一,本文將通過Java編程語言,深入探討如何在運行時動態(tài)地創(chuàng)建包含會簽環(huán)節(jié)的Flowable流程模型,需要的朋友可以參考下2024-05-05SpringMVC中@RequestMapping注解的實現(xiàn)
RequestMapping是一個用來處理請求地址映射的注解,本文主要介紹了SpringMVC中@RequestMapping注解的實現(xiàn),具有一定的參考價值,感興趣的可以了解一下2024-01-01springboot+dynamicDataSource動態(tài)添加切換數(shù)據(jù)源方式
這篇文章主要介紹了springboot+dynamicDataSource動態(tài)添加切換數(shù)據(jù)源方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-01-01JSch教程使用sftp協(xié)議實現(xiàn)服務(wù)器文件載操作
這篇文章主要為大家介紹了JSch如何使用sftp協(xié)議實現(xiàn)服務(wù)器文件上傳下載操作,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步2022-03-03