Java動態代理——框架中的應用場景和基本原理
阿新 • • 發佈:2020-11-02
## **前言**
之前已經用了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