1. 程式人生 > >Java動態代理——框架中的應用場景和基本原理

Java動態代理——框架中的應用場景和基本原理

## **前言** 之前已經用了5篇文章完整解釋了java動態代理的原理,本文將會為這個系列補上最後一塊拼圖,展示java動態代理的使用方式和應用場景 主要分為以下4個部分 **1.為什麼要使用java動態代理** **2.如何使用java動態代理** **3.框架中java動態代理的應用** **4.java動態代理的基本原理** ## 1.為何要使用動態代理 在設計模式中有一個非常常用的模式:代理模式。學術一些來講,就是為某些物件的某種行為提供一個代理物件,並由代理物件完全控制該行為的實際執行。 通俗來說,就是我想點份外賣,但是手機沒電了,於是我讓同學用他手機幫我點外賣。在這個過程中,其實就是我**同學(代理物件**)幫**我(被代理的物件)**代理了**點外賣(被代理的行為)**,在這個過程中,同學可以完全控制點外賣的店鋪、使用的APP,甚至把外賣直接吃了都行**(對行為的完全控制)**。 因此總結一下代理的4個要素: #### **代理物件** #### **被代理的行為** #### **被代理的物件** #### **行為的完全控制** 從實際編碼的角度來說,我們假設遇到了這樣一個需求,需要記錄下一些方法的執行時間,於是最簡單的方式當然就是在方法的開頭記錄一個時間戳,在return之前記錄一個時間戳。但如果方法的流程很複雜,例如: ```java public class Executor { public void execute(int x, int y) { log.info("start:{}", System.nanoTime()); if (x == 3) { log.info("end:{}", System.nanoTime()); return; } for (int i = 0; i < 100; i++) { if (y == 5) { log.info("end:{}", System.nanoTime()); return; } } log.info("end:{}", System.nanoTime()); return; } } ``` 我們需要在每一個return前都增加一行記錄時間戳的程式碼,很麻煩。於是我們想到可以由方法的呼叫者來記錄時間,例如: ```java public class Invoker { private Executor executor = new Executor(); public void invoke() { log.info("start:{}", System.nanoTime()); executor.execute(1, 2); log.info("end:{}", System.nanoTime()); } } ``` 我們又遇到一個問題,如果該方法在很多地方呼叫,或者需要記錄的方法有多個,那麼依然會面臨重複手動寫log程式碼的問題。 於是,我們就可以考慮建立一個代理物件,讓它負責幫我們統一記錄時間戳,例如: ```java public class Proxy { Executor executor = new Executor(); public void execute(int x, int y) { log.info("start:{}", System.nanoTime()); executor.execute(x, y); log.info("start:{}", System.nanoTime()); } } ``` 而在Invoker中,則由直接呼叫Executor中的方法改為呼叫Proxy的方法,當然方法的名字和簽名是完全相同的。當其他地方需要呼叫execute方法時,只需要呼叫Proxy中的execute方法,就會自動記錄下時間戳,而對於使用者來說是感知不到區別的。如下示例: ```java public class Invoker { private Proxy executor; public void invoke() { executor.execute(1, 2); } } ``` 上面展示的代理,就是一個典型的靜態代理,“靜態”體現在代理方法是我們直接編碼在類中的。 接著我們就遇到了下一個問題,如果Executor新增了一個方法,同樣要記錄時間,那我們就不得不修改Proxy的程式碼。並且如果其他類也有同樣的需求,那就需要新建不同的Proxy類才能較好的實現該功能,同樣非常麻煩。 那麼我們就需要將靜態代理升級成為動態代理了,而“動態”正是為了優化前面提到的2個靜態代理遇到的問題。 ## 2.如何使用java動態代理 建立java動態代理需要使用如下類 ```java java.lang.reflect.Proxy ``` 呼叫其newProxyInstance方法,例如我們需要為Map建立一個代理: ```java Map mapProxy = (Map) Proxy.newProxyInstance( HashMap.class.getClassLoader(), new Class[]{Map.class}, new InvocationHandler(){...} ); ``` 我們接著就來分析這個方法。先檢視其簽名: ```java public static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) ``` *ClassLoader型別的loader*:被代理的類的載入器,可以認為對應4要素中的**被代理的物件**。 *Class陣列的interfaces*:被代理的介面,這裡其實對應的就是4要素中的**被代理的行為**,可以注意到,這裡需要傳入的是介面而不是某個具體的類,因此表示行為。 *InvocationHandler介面的h*:代理的具體行為,對應的是4要素中的**行為的完全控制**,當然也是java動態代理的核心。 最後返回的物件Object對應的是4要素中的**代理物件**。 接著我們來示例用java動態代理來完成記錄方法執行時間戳的需求: 首先定義**被代理的行為**,即介面: ```java public interface ExecutorInterface { void execute(int x, int y); } ``` 接著定義**被代理的物件**,即實現了介面的類: ```java public class Executor implements ExecutorInterface { public void execute(int x, int y) { if (x == 3) { return; } for (int i = 0; i < 100; i++) { if (y == 5) { return; } } return; } } ``` 接著是代理的核心,即**行為的控制**,需要一個實現了InvocationHandler介面的類: ```java public class TimeLogHandler implements InvocationHandler { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return null; } } ``` 這個介面中的方法並不複雜,我們還是先分析其簽名 *Object型別的proxy*:最終生成的**代理物件** Method型別的method:被代理的方法。這裡其實是2個要素的複合,即**被代理的物件**是如何執行**被代理的行為**的。因為雖然我們說要對行為完全控制,但大部分時候,我們只是對行為增添一些額外的功能,因此依然是要利用被代理物件原先的執行過程的。 *Object陣列的args*:方法執行的引數 **因為我們的目的是要記錄方法的執行的時間戳,並且原方法本身還是依然要執行的,所以在TimeLogHandler的建構函式中,將一個原始物件傳入,method在呼叫invoke方法時即可使用。** 定義代理的行為如下: ```java public class TimeLogHandler implements InvocationHandler { private Object target; public TimeLogHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { log.info("start:{}", System.nanoTime()); Object result = method.invoke(target, args); log.info("end:{}", System.nanoTime()); return result; } } ``` 接著我們來看Invoker如何使用代理,這裡為了方便演示我們是在建構函式中例項化代理物件,在實際使用時可以採用依賴注入或者單例等方式來例項化: ```java public class Invoker { private ExecutorInterface executor; public Invoker() { executor = (ExecutorInterface) Proxy.newProxyInstance( Executor.class.getClassLoader(), new Class[]{ExecutorInterface.class}, new TimeLogHandler(new Executor()) ); } public void invoke() { executor.execute(1, 2); } } ``` 此時如果Exector新增了任何方法,那麼Invoker和TimeLogHandler將不需要任何改動就可以支援新增方法的的時間戳記錄,有興趣的同學可以自己嘗試一下。 另外如果有其他類也需要用到時間戳的記錄,那麼只需要和Executor一樣,通過Proxy.newProxyInstance方法建立即可,而不需要其他的改動了。 ## **3.框架中java動態代理的應用** 接著我們看一下java動態代理在現在的一些常用框架中的實際應用 ### **Spring AOP** spring aop是我們spring專案中非常常用的功能。 例如我們在獲取某個資料的時候需要先去redis中查詢是否已經有快取了,如果沒有快取再去讀取資料庫。我們就可以定義如下的一個切面和行為,然後在需要該功能的方法上增加相應註解即可,而不再需要每個方法單獨寫邏輯了。如下示例: ```java @Aspect @Component public class TestAspect { /** * 表示所有有cn.tera.aop.RedisPoint註解的方法 * 都會執行先讀取Redis的行為 */ @Pointcut("@annotation(cn.tera.aop.RedisPoint)") public void pointCut() { } /** * 實際獲取數的流程 */ @Around("pointCut()") public Object advise(ProceedingJoinPoint joinPoint) { try { /** * 先去查詢redis */ Object data = RedisUtility.get(some_key); if (data == null) { /** * joinPoint.proceed()表示執行原方法 * 如果redis中沒有快取,那麼就去執行原方法獲取資料 * 然後塞入redis中,下次就能直接獲取到快取了 */ data = joinPoint.proceed(); RedisUtility.put(some_key, data); } return data; } catch (Throwable r) { return null; } } } ``` 而其背後的原理使用的就是java動態代理。當然這裡要求被註解的方法所在的類必須是實現了介面的(回想下Proxy.newProxyInstance方法的簽名),否則就需要使用另外一個GCLib的庫了,不過這就是另外一個故事了,這裡就不展開了。 **Spring AOP中大部分情況下都是給原執行邏輯新增一些東西。** ### RPC框架 在一些rpc框架中,客戶端只需要關注介面的的呼叫,而具體的遠端請求則由框架內部實現,例如我們模擬一個簡單的rpc 請求,介面如下: ```java public interface OrderInterface { /** * 生成一張新訂單 */ void addOrder(); } ``` rpc框架可以生成介面的代理物件,例如: ```java public class SimpleRpcFrame { /** * 建立一個遠端請求代理物件 */ public