Java設(shè)計(jì)模式之動(dòng)態(tài)代理
動(dòng)態(tài)代理的意義在于生成一個(gè)占位(又稱代理對(duì)象),來代理真實(shí)對(duì)象,從而控制真實(shí)對(duì)象的訪問。
我們首先來談?wù)勈裁词谴砟J?。假設(shè)客戶帶著需求去找公司,顯然不會(huì)直接和軟件工程師談,而是和商務(wù)談,此時(shí)客戶會(huì)認(rèn)為商務(wù)就代表公司,客戶是通過商務(wù)去訪問軟件工程師的。我們就可以認(rèn)為商務(wù)(代理對(duì)象)代理了軟件工程師(真實(shí)對(duì)象),因此,代理的作用就是,在真實(shí)對(duì)象訪問之前或者之后加入對(duì)應(yīng)的邏輯,或者根據(jù)其他規(guī)則控制是否使用真實(shí)對(duì)象。
商務(wù)和軟件工程師是代理和被代理的關(guān)系,客戶是通過商務(wù)去訪問軟件工程師的。此時(shí)客戶就是程序中的調(diào)用者,商務(wù)就是代理對(duì)象,軟件工程師就是真實(shí)對(duì)象。我們需要在調(diào)用者調(diào)用對(duì)象之前產(chǎn)生一個(gè)代理對(duì)象,而這個(gè)代理對(duì)象需要和真實(shí)對(duì)象建立代理關(guān)系,所以代理必須分為兩個(gè)步驟:
- 代理對(duì)象和真實(shí)對(duì)象建立代理關(guān)系
- 實(shí)現(xiàn)代理對(duì)象的代理邏輯方法
在Java中有多種動(dòng)態(tài)代理技術(shù),比如JDK、CGLIB、Javassist、ASM,其中最常用的動(dòng)態(tài)代理技術(shù)有兩種:一種是JDK動(dòng)態(tài)代理,這是JDK自帶的功能;另一種是CGLIB,這是第三方提供的一個(gè)技術(shù)。目前,Spring常用JDK和CGLIB,而MyBatis還使用了Javassist。(CGLIB的底層原理是ASM)
本文只討論兩種最常用的動(dòng)態(tài)代理技術(shù):JDK和CGLIB。在JDk動(dòng)態(tài)代理中必須使用接口,而CGLIB不需要,所以使用CGLIB會(huì)更簡單一點(diǎn)。下面依次討論這兩種技術(shù)。
一.JDK動(dòng)態(tài)代理
JDK動(dòng)態(tài)代理是java.lang.reflect.*包提供的方式。它必須借助一個(gè)接口才能產(chǎn)生代理對(duì)象,所以先定義接口HelloWorld:
public interface HelloWorld { void sayHelloWorld(); }
然后提供實(shí)現(xiàn)類HelloWorldImpl來實(shí)現(xiàn)接口:
public class HelloWorldImpl implements HelloWorld { @Override public void sayHelloWorld(){ System.out.println("Hello World"); } }
這是最簡答的Java接口和實(shí)現(xiàn)類的關(guān)系,此時(shí)可以開始動(dòng)態(tài)代理了。按照我們之前的分析,先要建立代理對(duì)象和真實(shí)對(duì)象的關(guān)系,然后實(shí)現(xiàn)代理邏輯,所以一共分為兩個(gè)步驟。在JDK動(dòng)態(tài)代理中,代理邏輯類必須去實(shí)現(xiàn)java.lang.reflect.InvocationHandler
接口,它定義了一個(gè)invoke方法,并提供接口數(shù)組用于下掛代理對(duì)象。我們自己定義一個(gè)JDK動(dòng)態(tài)代理的類:
public class MyJdkProxyExample implements InvocationHandler{ /** * @description 真實(shí)對(duì)象 **/ private Object target = null; /** * @author haozz * @date 2018-05-21 10:54 * @param target 真實(shí)對(duì)象 * @return 代理對(duì)象 * @throws * @description 建立代理對(duì)象和真實(shí)對(duì)象的代理關(guān)系,并返回代理對(duì)象 **/ public Object getProxy(Object target) { this.target = target; return Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this); } /** * @author haozz * @date 2018-05-21 11:09 * @param proxy 代理對(duì)象 * @param method 當(dāng)前調(diào)度方法 * @param args 當(dāng)前方法參數(shù) * @return 代理結(jié)果 * @throws Throwable 異常 * @description 代理方法邏輯 **/ @Override public Object invoke(Object proxy, Method method,Object [] args) throws Throwable{ System.out.println("進(jìn)入代理邏輯方法"); System.out.println("在調(diào)度真實(shí)對(duì)象之前的服務(wù)"); Object obj = method.invoke(target,args);//相當(dāng)于調(diào)用sayHelloWorld方法 System.out.println("在調(diào)度真實(shí)對(duì)象之后的服務(wù)"); return obj; } }
第一步,通過getProxy()
方法建立代理對(duì)象和真實(shí)對(duì)象的關(guān)系,并返回代理對(duì)象。首先用類的屬性target保存了真實(shí)對(duì)象,然后通過Proxy.newProxyInstance()
方法建立并生成代理對(duì)象,該方法中包含3個(gè)參數(shù):
- 第1個(gè)是類加載器,這里采用了target本身的類加載器;
- 第2個(gè)是把生成的動(dòng)態(tài)代理對(duì)象下掛在哪些接口下,這個(gè)寫法就是放在target實(shí)現(xiàn)的接口下。HelloWorldImpl對(duì)象的接口顯然就是HelloWorld,代理對(duì)象可以這樣聲明:HelloWorld proxy = xxxx;;
- 第3個(gè)是定義實(shí)現(xiàn)方法邏輯的代理類,this表示當(dāng)前對(duì)象,它必須實(shí)現(xiàn)InvocationHandler接口的invoke方法,它就是代理邏輯方法的現(xiàn)實(shí)方法。
第二步,通過invoke方法實(shí)現(xiàn)代理邏輯方法。invoke方法的3個(gè)參數(shù):
- proxy,代理對(duì)象,就是getProxy方法生成的對(duì)象;
- method,當(dāng)前調(diào)度的方法;
- args,當(dāng)前調(diào)度方法的參數(shù)。
當(dāng)我們使用了代理對(duì)象調(diào)度方法后,它就會(huì)進(jìn)入到invoke方法里面。
Object obj = method.invoke(target,args);
這行代碼相當(dāng)于調(diào)度真實(shí)對(duì)象的方法,只是通過反射實(shí)現(xiàn)。類比前面的例子,proxy相當(dāng)于商務(wù),target相當(dāng)于軟件工程師,getProxy方法就是建立商務(wù)和軟件工程師之間的代理關(guān)系,invoke方法就是商務(wù)邏輯。
測試JDK動(dòng)態(tài)代理:
@Test public void testJdkProxy(){ MyJdkProxyExample jdkProxy = new MyJdkProxyExample(); //綁定關(guān)系,因?yàn)閽煸诮涌贖elloWorld下,所以聲明代理對(duì)象HelloWorld proxy HelloWorld proxy = (HelloWorld) jdkProxy.getProxy(new HelloWorldImpl()); //此時(shí)HelloWorld對(duì)象已經(jīng)是一個(gè)代理對(duì)象,它會(huì)進(jìn)入代理的邏輯方法invoke里 proxy.sayHelloWorld(); }
首先通過getProxy方法綁定了代理關(guān)系,然后在代理對(duì)象調(diào)度sayHelloWorld方法時(shí)進(jìn)入了代理的邏輯,測試結(jié)果如下:
進(jìn)入代理邏輯方法
在調(diào)度真實(shí)對(duì)象之前的服務(wù)
Hello World
在調(diào)度真實(shí)對(duì)象之后的服務(wù)
此時(shí),在調(diào)度打印Hello World之前和之后都可以加入相關(guān)的邏輯,甚至可以不調(diào)度Hello World的打印。
個(gè)人小結(jié):JDK動(dòng)態(tài)代理要求真實(shí)對(duì)象和代理對(duì)象之間是實(shí)現(xiàn)類和接口的關(guān)系,創(chuàng)建一個(gè)代理邏輯類實(shí)現(xiàn)InvocationHandler接口,其中g(shù)etProxy方法用于生成代理對(duì)象,然后重寫invoke方法。
二.CGLIB動(dòng)態(tài)代理
JDK動(dòng)態(tài)代理必須提供接口才能使用,在一些不能提供接口的環(huán)境中,只能采用其他第三方技術(shù),比如CGLIB動(dòng)態(tài)代理,這里提供CGLIB動(dòng)態(tài)代理的相關(guān)jar包,供學(xué)習(xí)和測試使用。它的優(yōu)勢在于不需要提供接口,只要一個(gè)非抽象類就能實(shí)現(xiàn)動(dòng)態(tài)代理。
CGLIB原理:動(dòng)態(tài)生成一個(gè)要代理類的子類,子類重寫要代理的類的所有不是final的方法。在子類中采用方法攔截的技術(shù)攔截所有父類方法的調(diào)用,順勢織入橫切邏輯。它比使用java反射的JDK動(dòng)態(tài)代理要快。CGLIB底層:使用字節(jié)碼處理框架ASM,來轉(zhuǎn)換字節(jié)碼并生成新的類。不鼓勵(lì)直接使用ASM,因?yàn)樗竽惚仨殞?duì)JVM內(nèi)部結(jié)構(gòu)包括class文件的格式和指令集都很熟悉。CGLIB缺點(diǎn):對(duì)于final方法,無法進(jìn)行代理。CGLIB廣泛地被許多AOP的框架使用,例如Spring AOP和dynaop。Hibernate使用CGLIB來代理單端single-ended(多對(duì)一和一對(duì)一)關(guān)聯(lián)。
我們以下面這個(gè)類為例:
public class ReflectServiceImpl { public void sayHello(String name){ System.out.println("Hello "+name); } }
它不存在實(shí)現(xiàn)任何接口,所以不能使用JDK動(dòng)態(tài)代理,這里采用CGLIB動(dòng)態(tài)代理技術(shù):
public class MyCglibProxyExample implements MethodInterceptor{ /** * @author haozz * @date 2018-05-21 15:11 * @param cls Class類 * @return Class類的CGLIB對(duì)象 * @throws * @description 生成CGLIB代理對(duì)象 **/ public Object getProxy(Class cls){ //CGLIB增強(qiáng)類對(duì)象 Enhancer enhancer = new Enhancer(); //設(shè)置增強(qiáng)類型 enhancer.setSuperclass(cls); //定義代理邏輯對(duì)象為當(dāng)前對(duì)象,要求當(dāng)前對(duì)象實(shí)現(xiàn)MethodInterceptor方法 enhancer.setCallback(this); //生成并返回代理對(duì)象 return enhancer.create(); } /** * @author haozz * @date 2018-05-21 15:17 * @param proxy 代理對(duì)象 * @param method 目標(biāo)方法 * @param args 目標(biāo)方法參數(shù) * @param methodProxy 方法代理 * @return 代理邏輯返回 * @throws Throwable 異常 * @description 代理邏輯方法 **/ @Override public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy)throws Throwable{ System.out.println("調(diào)用真實(shí)對(duì)象前"); //CGLIB反射調(diào)用真實(shí)對(duì)象方法 Object result = methodProxy.invokeSuper(proxy,args); System.out.println("調(diào)用真實(shí)對(duì)象后"); return result; } }
這里用了CGLIB的加強(qiáng)者Enhancer(net.sf.sglib.proxy.Enhancer),通過設(shè)置超類的方法(setSuperclass),然后通過setCallback方法設(shè)置哪個(gè)類為它的代理類。其中,參數(shù)this表示當(dāng)前對(duì)象,那就要求用this這個(gè)對(duì)象實(shí)現(xiàn)接口MethodInterceptor(net.sf.sglib.proxy.MethodInterceptor)的方法intercept,然后返回代理對(duì)象。那么此時(shí)當(dāng)前類的intercept方法就是其代理邏輯方法,其參數(shù)內(nèi)容見代碼注解,我們?cè)诜瓷湔鎸?shí)對(duì)象方法前后進(jìn)行了打印,CGLIB是通過如下代碼完成的:
Object result = methodProxy.invokeSuper(proxy,args);
測試一下CGLIB動(dòng)態(tài)代理:
@Test public void testCglibProxy(){ MyCglibProxyExample cglibProxy = new MyCglibProxyExample(); ReflectServiceImpl obj = (ReflectServiceImpl) cglibProxy.getProxy(ReflectServiceImpl.class); obj.sayHello("haozz"); }
得到結(jié)果:
調(diào)用真實(shí)對(duì)象前
Hello haozz
調(diào)用真實(shí)對(duì)象后
個(gè)人小結(jié):CGLIB動(dòng)態(tài)代理相對(duì)簡單一些,通過字節(jié)碼技術(shù)為需要代理的類生成一個(gè)子類,它不需要提供接口,只需寫一個(gè)代理邏輯類并實(shí)現(xiàn)MethodInterceptor接口,其中g(shù)etProxy方法用于生成代理對(duì)象,然后重寫intercept方法即可。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。如果你想了解更多相關(guān)內(nèi)容請(qǐng)查看下面相關(guān)鏈接
相關(guān)文章
SpringBoot Controller接收參數(shù)的幾種常用方式
這篇文章主要介紹了SpringBoot Controller接收參數(shù)的幾種常用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07Flink自定義Sink端實(shí)現(xiàn)過程講解
這篇文章主要介紹了Flink自定義Sink端實(shí)現(xiàn)過程,在Fink官網(wǎng)中sink端只是給出了常規(guī)的write api.在我們實(shí)際開發(fā)場景中需要將flink處理的數(shù)據(jù)寫入kafka,hbase kudu等外部系統(tǒng)2023-01-01mybatis參數(shù)String與Integer類型的判斷方式
這篇文章主要介紹了mybatis參數(shù)String與Integer類型的判斷方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03java調(diào)用遠(yuǎn)程服務(wù)器的shell腳本以及停止的方法實(shí)現(xiàn)
這篇文章主要介紹了java調(diào)遠(yuǎn)程服務(wù)器的shell腳本以及停止的方法實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03