java 委托_动态代理:Java开发必学
1. 引言
最近開發項目過程中需要使用動態代理來實現功能,趁此機會把動態代理的知識點進行了一次梳理。在 java 開發過程中,當需要對已有的代碼(方法)前后添加新功能,而不想修改或不方便修改原代碼的情況下,如需要在執行某個已有的方法前后輸出日志,以記錄方法執行的記錄,這個時候,動態代理就派上用場了。動態代理可以有以下幾使用場景:
- 記錄跟蹤:對函數執行前后統一輸出日志跟蹤執行情況
- 計時:統一對函數執行用時進行計算(前后時間記錄之差)
- 權限校驗:統一在函數執行前進行權限校驗
- 事務:統一對函數作為事務處理
- 異常處理:對某一類函數執行輸出的異常進行統一捕獲處理
- 動態切換數據源:多數據源切換或動態添加數據源
動態代理首先是代理,對應的有靜態代理,然后是動態代理,在 Spring 中還有動態代理的應用-AOP(面向切面編程)。本文針對這些內容,梳理了以下幾個知識點:
- 了解代理模式并實現靜態代理
- 針對動態代理,描述它使用到的反射機制,以及 JDK、CGLIB 兩種動態代理的實現
- 描述如何在在 Spring 中,使用 AOP 實現面向切面編程
本文所涉及到的靜態代理、反射、動態代理及 AOP 使用有相應的示例代碼[1],讀者可結合一起看。
2. 代理模式及靜態代理
2.1 代理模式說明
代理,即調用者不需要跟實際的對象接觸,只跟代理打交道?,F實中典型的例子就是買房者,賣房者及房產中介。賣房者作為委托方,委托房產中介作為代理幫賣房,而買房者只需要跟房產中介打交道即可。這樣就可以做到委托者與買房者解耦。再來看以下的圖,就可以了解代理模式(定義:為其它對象提供代理以控制這個對象的訪問)了:
Proxy相當于房產中介,RealSubject就是賣房者,Client就是買房者,operation方法就是委托的內容,Proxy與RealSubject共同實現一個接口,以表示他們的操作一致。
2.2 靜態代理
按照上面的代理模式的代碼實現,其實就是靜態代理了。靜態意思是代理類Proxy是在代碼在編譯時就確定了,而不是在代碼中動態生成。如下,我們在示例代碼中,接口中有兩個函數(doAction1及doAction2),對應的實現類:
/*** 服務實現類:委托類**/ publicclass ServiceImpl implements IService {@Overridepublic void doAction1() { System.out.println(" do action1 ");}@Overridepublic void doAction2() { System.out.println(" do action2 ");} }現在的需求是需要在doAction1方法執行前和執行后輸出日志以便于跟蹤,然后對doAction2方法的執行時間進行計算,但又不允許使用修改ServiceImpl類,這個時候,通過一個代理類就可以實現。如下:
/*** 服務代理類:代理類**/ publicclass ServiceProxy implements IService {/*** 關聯實際委托類**/private ServiceImpl serviceImpl;public ServiceProxy(ServiceImpl serviceImpl) {this.serviceImpl = serviceImpl;}@Overridepublic void doAction1() {System.out.println(" proxy log begin ");serviceImpl.doAction1();System.out.println(" proxy log end ");}@Overridepublic void doAction2() {StopWatch stopWatch = new StopWatch();stopWatch.start("timeCalculation");serviceImpl.doAction2();stopWatch.stop();System.out.println(stopWatch.prettyPrint());} }客戶端執行時,只需要使用代理類執行對應的方法即可,如下:
ServiceProxy serviceProxy = new ServiceProxy(new ServiceImpl()); serviceProxy.doAction1();執行的結果如下:
2.3 靜態代理局限性
從上面代碼可以發現,靜態代理很簡單,可以很快實現我們打印日志及計算執行用時的需求。但靜態需求有它的局限性,就是當接口中的函數增加的時候,代理類中會出現很多臃腫、重復的代碼。比如上述接口若有 100 個函數,其中 50 個函數需要打印日志,50 個函數需要計算用時,那么,代理類中,像doAction1這樣的日志輸出代碼就要寫 50 次,像doAction2這樣使用StopWatch計時的代碼同樣需要需要寫 50 次。一旦出現重復的代碼,就應該知道這個代碼需要優化了。既然多個函數用了相同的代碼,有沒有一種方式只需要把這代碼寫一次,然后應用到多個函數?這個時候就需要動態代理。
3. 動態代理
前面提到,使用動態代理解決靜態代理中重復代碼的問題,其實就像是把全部需要代理執行的函數看成是一個可以動態執行的函數,把這個函數像針線一樣,織入到需要執行的額外代碼中間。如前面的日志輸出,把函數織入到日志輸出的代碼中間。怎樣能把函數動態執行?這就需要用到 JAVA 的反射技術了,這也是動態代理的關鍵。
3.1 JAVA 反射機制
JAVA 的反射技術其實就是在運行狀態中,動態獲取類的屬性和方法,也可以夠調用和操作這個類對象的方法和屬性,這種功能就叫做反射。使用反射,可以動態生成類對象,而不用像之前的代碼(使用 new)靜態生成。也可以動態地執行類對象的方法。在示例代碼中的reflection包及ReflectionTest類展示了如何動態執行某個類對象的方法。如下,定義了某個類及它的方法:
publicclass 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);從以上代碼可知道,只要知道類路徑和它定義的方法名,就可以動態來執行這個方法了,這里動態的意思就是不把需要執行的代碼寫死在代碼中(編譯時即確定),而是靈活的在運行時才生成。
3.2 JDK 動態代理
3.2.1 JDK 動態代理
知道了反射機制可以動態執行類對象,就容易理解動態代理了。在 JDK 中,已默認提供了動態代理的實現,它的關鍵點也是在于通過反射執行invoke來動態執行方法,主要實現流程如下:
- 實現InvocationHandler,由它來實現invoke方法,執行代理函數
- 使用Proxy類根據類的加載器及接口說明,創建代理類,同時關聯委托類
- 使用代理類執行代理函數,則會調用invoke方法,完成代理
在示例代碼中JdkLogProxyHandler類是日志輸出代理類,代碼如下:
/*** 日志動態代理類:JDK實現**/ publicclass JdkLogProxyHandler implements InvocationHandler {private Object targetObject;@Overridepublic 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來完成,就像把函數織入到代碼中。這樣就解決了前面靜態代理的局限。
3.2.2 JDK 動態代理與限制
JDK 默認提供的動態代理機制使用起來很簡單方便,但它也有相應的限制,就是只能動態代理實現了接口的類,如果類沒有實現接口,只是單純的一個類,則沒有辦法使用InvocationHandler的方式來動態代理了。此時,就需要用到 CGLIB 來代理。
3.4 CGLIB 動態代理
CGLIB(Code Generator Library)是一個強大的、高性能的代碼生成庫。CGLIB 代理主要通過對字節碼的操作,為對象引入間接級別,以控制對象的訪問。針對上面沒有實現接口的類,CGLIB 主要是通過繼承來完成動態代理的。在使用方法上,主要也是有 3 個步驟:
- 實現MethodInterceptor接口,在intercept方法中實現代理內容(如日志輸出)
- 使用Enhancer及委托類生成代理類
- 使用代理類執行函數,就會動態調用intercept方法的實現
如下所示是使用 CGLIB 來實現類的動態代理:
/*** 日志動態代理:cglib實現**/ publicclass CglibLogProxyInterceptor implements MethodInterceptor {@Overridepublic 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*/publicstatic <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(); }4. 動態代理在 Spring 的應用:AOP
前面提到 JDK 的默認動態代理和 CGLIB 動態代理,在 Spring 中,AOP(面向切面編程)就是使用這兩個技術實現的(如果有實現接口的類使用 JDK 動態代理,沒有實現接口的類則使用 CGLIB)。具體到在 Spring 應用中,如何使用 AOP 進行切面編程,示例代碼中使用 springboot 工程,模擬提供 user 的增刪改查的 REST 接口,通過切面對所有Service類的函數統一進行日志輸出。
4.1 AOP 概念
關于 AOP 的概念,從理解這兩個問題開始,即代理發生在什么地方,以什么樣的形式添加額外功能代碼。
- 切面(Aspect):前面提到的日志輸出代理和函數執行計時代理,它們其實都是與業務邏輯無關,只是在各個業務邏輯中都添加功能,這種代理就是切面。將橫切關注點與業務邏輯分離的編程方式,每個橫切關注點都集中在一個地方,而不是分散在多處代碼中。
- 切點(PointCut):明確什么地方需要添加額外功能,這些地方有可能是一類函數(比如有多個函數都需要輸出日志),因此需要使用一定的規則定義是哪一類函數。
- 連接點(JoinPoint):就是具體被攔截添加額外功能的地方,其實就是執行的某一個具體函數,是前面切點定義的其中一個函數。
- 通知(advice):明確以什么樣的形式添加額外功能,可以在函數執行前(before),后(after),環繞(around),函數正常返回后通知(afterReturning)和異常返回后通知(afterThrowing)。
在 AOP 編程中,上面提到的概念,都有對應的注解進行使用,通過注解,就可以實現切面功能。
4.2 AOP 編程
4.2.1 引入 aop 依賴
Springboot 有提供 aop 的 starter,添加以下依賴,即可使用 AOP 相關功能。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId> </dependency>4.2.2 定義切面、切點與通知
本示例的需求是對 service 包下所有類的全部函數統一進行日志輸出。因此我們定義一個LogAopAspect作為這個日志輸出功能的切面(使用注解@Aspect),使用@Pointcut來確定輸出點的匹配規則是 service 這個包下所有類的全部函數。當真正某個函數執行時,通過動態代理執行通知(使用注解@Before、@After,@Around等)。具體的輸出動作,也就是在這些通知里。
@Slf4j @Aspect @Component publicclass 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)需要有返回值來返回連接點執行后的結果。
5. 總結
本文對 JAVA 的動態代理知識進行了梳理,先從代理模式說起,使用靜態代理實現簡單的外加功能,接著通過講述了 JAVA 動態代理使用到的反射機制,并通過示例實現 JDK 和 CGLIB 兩種動態代理功能,最后結合 springboot 示例,使用 AOP 編程,實現對關心的類進行日志輸出的切面功能。通過動態代理,我們可以把一些輔助性的功能抽取出來,在不修改業務邏輯的情況下,完成輔助功能的添加。所以當你需要添加新功能,又不想修改原代碼的情況下,就用動態代理吧!
總結
以上是生活随笔為你收集整理的java 委托_动态代理:Java开发必学的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: rgb红色范围_UI设计教程分享之RGB
- 下一篇: java同步锁synchronized_