目錄java
一句話歸納:java動態代理經過反射機制,可在不修改原代碼的狀況下添加新的功能,應用於多種場景,簡單、實用、靈活,是java開發必學知識,本文將對動態代理使用進行詳細介紹。git
最近開發項目過程當中須要使用動態代理來實現功能,趁此機會把動態代理的知識點進行了一次梳理。在java開發過程當中,當須要對已有的代碼(方法)先後添加新功能,而不想修改或不方便修改原代碼的狀況下,如須要在執行某個已有的方法先後輸出日誌,以記錄方法執行的記錄,這個時候,動態代理就派上用場了。動態代理能夠有如下幾使用場景:github
動態代理首先是代理,對應的有靜態代理,而後是動態代理,在Spring中還有動態代理的應用-AOP(面向切面編程)。本文針對這些內容,梳理了如下幾個知識點:spring
本文所涉及到的靜態代理、反射、動態代理及AOP使用有相應的示例代碼:https://github.com/mianshenglee/my-example/tree/master/dynamic-proxy-demo
,讀者可結合一塊兒看。apache
代理,即調用者不須要跟實際的對象接觸,只跟代理打交道。現實中典型的例子就是買房者,賣房者及房產中介。賣房者做爲委託方,委託房產中介做爲代理幫賣房,而買房者只須要跟房產中介打交道便可。這樣就能夠作到委託者與買房者解耦。再來看如下的圖,就能夠了解代理模式(定義:爲其它對象提供代理以控制這個對象的訪問)了:編程
Proxy
至關於房產中介,RealSubject
就是賣房者,Client
就是買房者,operation
方法就是委託的內容,Proxy
與RealSubject
共同實現一個接口,以表示他們的操做一致。後端
按照上面的代理模式的代碼實現,其實就是靜態代理了。靜態意思是代理類Proxy
是在代碼在編譯時就肯定了,而不是在代碼中動態生成。以下,咱們在示例代碼中,接口中有兩個函數(doAction1
及doAction2
),對應的實現類:springboot
/** * 服務實現類:委託類 **/ public class ServiceImpl implements IService { @Override public void doAction1() { System.out.println(" do action1 ");} @Override public void doAction2() { System.out.println(" do action2 ");} }
如今的需求是須要在doAction1
方法執行前和執行後輸出日誌以便於跟蹤,而後對doAction2
方法的執行時間進行計算,但又不容許使用修改ServiceImpl
類,這個時候,經過一個代理類就能夠實現。以下:前後端分離
/** * 服務代理類:代理類 **/ public class ServiceProxy implements IService { /** * 關聯實際委託類 **/ private ServiceImpl serviceImpl; public ServiceProxy(ServiceImpl serviceImpl) {this.serviceImpl = serviceImpl;} @Override public void doAction1() { System.out.println(" proxy log begin "); serviceImpl.doAction1(); System.out.println(" proxy log end "); } @Override public void doAction2() { StopWatch stopWatch = new StopWatch(); stopWatch.start("timeCalculation"); serviceImpl.doAction2(); stopWatch.stop(); System.out.println(stopWatch.prettyPrint()); } }
客戶端執行時,只須要使用代理類執行對應的方法便可,以下:ide
ServiceProxy serviceProxy = new ServiceProxy(new ServiceImpl()); serviceProxy.doAction1();
執行的結果以下:
從上面代碼能夠發現,靜態代理很簡單,能夠很快實現咱們打印日誌及計算執行用時的需求。但靜態需求有它的侷限性,就是當接口中的函數增長的時候,代理類中會出現不少臃腫、重複的代碼。好比上述接口如有100個函數,其中50個函數須要打印日誌,50個函數須要計算用時,那麼,代理類中,像doAction1
這樣的日誌輸出代碼就要寫50次,像doAction2
這樣使用StopWatch
計時的代碼一樣須要須要寫50次。一旦出現重複的代碼,就應該知道這個代碼須要優化了。既然多個函數用了相同的代碼,有沒有一種方式只須要把這代碼寫一次,而後應用到多個函數?這個時候就須要動態代理。
前面提到,使用動態代理解決靜態代理中重複代碼的問題,其實就像是把所有須要代理執行的函數當作是一個能夠動態執行的函數,把這個函數像針線同樣,織入到須要執行的額外代碼中間。如前面的日誌輸出,把函數織入到日誌輸出的代碼中間。怎樣能把函數動態執行?這就須要用到JAVA的反射技術了,這也是動態代理的關鍵。
JAVA的反射技術其實就是在運行狀態中,動態獲取類的屬性和方法,也能夠夠調用和操做這個類對象的方法和屬性,這種功能就叫作反射。使用反射,能夠動態生成類對象,而不用像以前的代碼(使用new)靜態生成。也能夠動態地執行類對象的方法。在示例代碼中的reflection
包及ReflectionTest
類展現瞭如何動態執行某個類對象的方法。以下,定義了某個類及它的方法:
public class ReflectionService { public void doSomething(){ System.out.println(" logging reflection service"); } }
使用反射,動態生成這個類對象,並使用invoke
來執行doSomething
方法。
//加載類 Class<?> refClass = Class.forName("me.mason.demo.proxy.refrection.ReflectionService"); //生成類對象 Object refClassObject = refClass.getConstructor().newInstance(); //調用類對象方法 Method method = refClass.getDeclaredMethod("doSomething"); method.invoke(refClassObject);
從以上代碼可知道,只要知道類路徑和它定義的方法名,就能夠動態來執行這個方法了,這裏動態的意思就是不把須要執行的代碼寫死在代碼中(編譯時即肯定),而是靈活的在運行時才生成。
知道了反射機制能夠動態執行類對象,就容易理解動態代理了。在JDK中,已默認提供了動態代理的實現,它的關鍵點也是在於經過反射執行invoke
來動態執行方法,主要實現流程以下:
InvocationHandler
,由它來實現invoke
方法,執行代理函數Proxy
類根據類的加載器及接口說明,建立代理類,同時關聯委託類invoke
方法,完成代理在示例代碼中JdkLogProxyHandler
類是日誌輸出代理類,代碼以下:
/** * 日誌動態代理類:JDK實現 **/ public class JdkLogProxyHandler implements InvocationHandler { private Object targetObject; @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(" jdk dynamic proxy log begin "); Object result = method.invoke(targetObject, args); System.out.println(" jdk dynamic proxy log end "); return result; } /** * 根據委託類動態產生代理類 * @param targetObject 委託類對象 * @return 代理類 */ public Object createPorxy(Object targetObject){ this.targetObject = targetObject; return Proxy.newProxyInstance(targetObject.getClass().getClassLoader() ,targetObject.getClass().getInterfaces(),this); } }
在客戶端使用時,須要產生代理類,對的日誌輸出,執行以下(執行輸出結果與靜態代理功能一致):
@Test void testLogProxy() { JdkLogProxyHandler logProxyHandler = new JdkLogProxyHandler(); IService proxy = (IService)logProxyHandler.createPorxy(new ServiceImpl()); proxy.doAction1(); System.out.println("############"); proxy.doAction2(); }
這裏把日誌輸出代理做爲一類,把函數執行計時做爲一類(
JdkTimeProxyHandler
),關注代理內容自己,而不是針對委託類的函數。這裏的日誌輸出和函數執行計時,就是切面(後面會提到)。
能夠比較一下,使用這種動態代理,與前面靜態代理的區別:
invoke
來完成,就像把函數織入到代碼中。這樣就解決了前面靜態代理的侷限。JDK默認提供的動態代理機制使用起來很簡單方便,但它也有相應的限制,就是隻能動態代理實現了接口的類,若是類沒有實現接口,只是單純的一個類,則沒有辦法使用InvocationHandler
的方式來動態代理了。此時,就須要用到CGLIB來代理。
CGLIB(Code Generator Library)是一個強大的、高性能的代碼生成庫。CGLIB代理主要經過對字節碼的操做,爲對象引入間接級別,以控制對象的訪問。針對上面沒有實現接口的類,CGLIB主要是經過繼承來完成動態代理的。在使用方法上,主要也是有3個步驟:
MethodInterceptor
接口,在intercept
方法中實現代理內容(如日誌輸出)Enhancer
及委託類生成代理類intercept
方法的實現以下所示是使用CGLIB來實現類的動態代理:
/** * 日誌動態代理:cglib實現 **/ public class CglibLogProxyInterceptor implements MethodInterceptor { @Override public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { System.out.println(" cglib dynamic proxy log begin "); Object result = methodProxy.invokeSuper(object, args); System.out.println(" cglib dynamic proxy log begin "); return result; } /** * 動態建立代理 * * @param cls 委託類 * @return */ public static <T> T createProxy(Class<T> cls) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(cls); enhancer.setCallback(new CglibLogProxyInterceptor()); return (T) enhancer.create(); } }
從上面代碼可知道,代理類是經過Enhancer
設置委託類爲父類(setsuperclass
),並把當前的intercept
方法做爲回調,以此建立代理類,在客戶端執行代理時,則會執行回調,從而達到代理效果,客戶端執行以下:
@Test void testLogProxy() { CglibService proxy = CglibLogProxyInterceptor.createProxy(CglibService.class); proxy.doAction1(); System.out.println("############"); proxy.doAction2(); }
前面提到JDK的默認動態代理和CGLIB動態代理,在Spring中,AOP(面向切面編程)就是使用這兩個技術實現的(若是有實現接口的類使用JDK動態代理,沒有實現接口的類則使用CGLIB)。具體到在Spring應用中,如何使用AOP進行切面編程,示例代碼中使用springboot工程,模擬提供user的增刪改查的REST接口,經過切面對全部Service
類的函數統一進行日誌輸出。
關於AOP的概念,從理解這兩個問題開始,即代理髮生在什麼地方,以什麼樣的形式添加額外功能代碼。
在AOP編程中,上面提到的概念,都有對應的註解進行使用,經過註解,就能夠實現切面功能。
Springboot有提供aop的starter,添加如下依賴,便可使用AOP相關功能。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
本示例的需求是對service包下全部類的所有函數統一進行日誌輸出。所以咱們定義一個LogAopAspect
做爲這個日誌輸出功能的切面(使用註解@Aspect
),使用@Pointcut
來肯定輸出點的匹配規則是service這個包下全部類的所有函數。當真正某個函數執行時,經過動態代理執行通知(使用註解@Before
、@After
,@Around
等)。具體的輸出動做,也就是在這些通知裏。
@Slf4j @Aspect @Component public class LogAopAspect { /** * 切點:對service包中全部方法進行織入 */ @Pointcut("execution(* me.mason.demo.proxy.springaop.service.*.*(..))") private void allServiceMethodPointCut() {} @Before("allServiceMethodPointCut()") public void before() { log.info(" spring aop before log begin ");} @AfterReturning("allServiceMethodPointCut()") public void after() { log.info(" spring aop before log end ");} /** * 環繞通知,須要返回調用結果 */ @Around("allServiceMethodPointCut()") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { log.info(" spring aop around log begin "); try { return proceedingJoinPoint.proceed(); } finally { log.info(" spring aop around log end "); } } }
經過上面的類定義,便可完成動態代理,而不須要像上面的JDK和GCLIB那樣本身實現接口來操做。
- AOP的底層實現依然是使用JDK和CGLIB來實現動態代理的,若類有實現接口則使用JDK,沒有則使用CGLIB。
- Pointcut的定義規則是指示器+正則式,指示器有參數定義(agrs),執行方法(execution),指定對象(target),指定類型(within)及相應的註解(使用@開頭)。正則式中
*
表示任何內容,(..)
表示任意參數匹配。示例中execution(* me.mason.demo.proxy.springaop.service.*.*(..))
表示對執行方法進行攔截,攔截的是me.mason.demo.proxy.springaop.service
包下的全部類的全部函數,返回值不限,參數不限。- 環繞通知(Around)須要有返回值來返回鏈接點執行後的結果。
本文對JAVA的動態代理知識進行了梳理,先從代理模式提及,使用靜態代理實現簡單的外加功能,接着經過講述了JAVA動態代理使用到的反射機制,並經過示例實現JDK和CGLIB兩種動態代理功能,最後結合springboot示例,使用AOP編程,實現對關心的類進行日誌輸出的切面功能。經過動態代理,咱們能夠把一些輔助性的功能抽取出來,在不修改業務邏輯的狀況下,完成輔助功能的添加。因此當你須要添加新功能,又不想修改原代碼的狀況下,就用動態代理吧!
本文配套的示例,demo地址:https://github.com/mianshenglee/my-example/tree/master/dynamic-proxy-demo
,有興趣的能夠跑一下示例來感覺一下。
https://zgljl2012.com/dong-tai-dai-li/
https://juejin.im/post/5ad3e6b36fb9a028ba1fee6a
https://juejin.im/post/591c5fe5a22b9d0058439333
https://juejin.im/post/5b9b1c8be51d450e9942fae4
關注個人公衆號(搜索Mason技術記錄
),獲取更多技術記錄: