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