前言
之前已經用了5篇文章完整解釋了java動態代理的原理,本文將會為這個系列補上最後一塊拼圖,展示java動態代理的使用方式和應用場景
主要分為以下4個部分
1.為什麼要使用java動態代理
2.如何使用java動態代理
3.框架中java動態代理的應用
4.java動態代理的基本原理
1.為何要使用動態代理
在設計模式中有一個非常常用的模式:代理模式。學術一些來講,就是為某些物件的某種行為提供一個代理物件,並由代理物件完全控制該行為的實際執行。
通俗來說,就是我想點份外賣,但是手機沒電了,於是我讓同學用他手機幫我點外賣。在這個過程中,其實就是我同學(代理物件)幫我(被代理的物件)代理了點外賣(被代理的行為),在這個過程中,同學可以完全控制點外賣的店鋪、使用的APP,甚至把外賣直接吃了都行(對行為的完全控制)。
因此總結一下代理的4個要素:
代理物件
被代理的行為
被代理的物件
行為的完全控制
從實際編碼的角度來說,我們假設遇到了這樣一個需求,需要記錄下一些方法的執行時間,於是最簡單的方式當然就是在方法的開頭記錄一個時間戳,在return之前記錄一個時間戳。但如果方法的流程很複雜,例如:
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前都增加一行記錄時間戳的程式碼,很麻煩。於是我們想到可以由方法的呼叫者來記錄時間,例如:
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程式碼的問題。
於是,我們就可以考慮建立一個代理物件,讓它負責幫我們統一記錄時間戳,例如:
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方法,就會自動記錄下時間戳,而對於使用者來說是感知不到區別的。如下示例:
public class Invoker {
private Proxy executor;
public void invoke() {
executor.execute(1, 2);
}
}
上面展示的代理,就是一個典型的靜態代理,“靜態”體現在代理方法是我們直接編碼在類中的。
接著我們就遇到了下一個問題,如果Executor新增了一個方法,同樣要記錄時間,那我們就不得不修改Proxy的程式碼。並且如果其他類也有同樣的需求,那就需要新建不同的Proxy類才能較好的實現該功能,同樣非常麻煩。
那麼我們就需要將靜態代理升級成為動態代理了,而“動態”正是為了優化前面提到的2個靜態代理遇到的問題。
2.如何使用java動態代理
建立java動態代理需要使用如下類
java.lang.reflect.Proxy
呼叫其newProxyInstance方法,例如我們需要為Map建立一個代理:
Map mapProxy = (Map) Proxy.newProxyInstance(
HashMap.class.getClassLoader(),
new Class[]{Map.class},
new InvocationHandler(){...}
);
我們接著就來分析這個方法。先檢視其簽名:
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
ClassLoader型別的loader:被代理的類的載入器,可以認為對應4要素中的被代理的物件。
Class陣列的interfaces:被代理的介面,這裡其實對應的就是4要素中的被代理的行為,可以注意到,這裡需要傳入的是介面而不是某個具體的類,因此表示行為。
InvocationHandler介面的h:代理的具體行為,對應的是4要素中的行為的完全控制,當然也是java動態代理的核心。
最後返回的物件Object對應的是4要素中的代理物件。
接著我們來示例用java動態代理來完成記錄方法執行時間戳的需求:
首先定義被代理的行為,即介面:
public interface ExecutorInterface {
void execute(int x, int y);
}
接著定義被代理的物件,即實現了介面的類:
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介面的類:
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方法時即可使用。
定義代理的行為如下:
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如何使用代理,這裡為了方便演示我們是在建構函式中例項化代理物件,在實際使用時可以採用依賴注入或者單例等方式來例項化:
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中查詢是否已經有快取了,如果沒有快取再去讀取資料庫。我們就可以定義如下的一個切面和行為,然後在需要該功能的方法上增加相應註解即可,而不再需要每個方法單獨寫邏輯了。如下示例:
@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 請求,介面如下:
public interface OrderInterface {
/**
* 生成一張新訂單
*/
void addOrder();
}
rpc框架可以生成介面的代理物件,例如:
public class SimpleRpcFrame {
/**
* 建立一個遠端請求代理物件
*/
public static <T> T getRemoteProxy(Class<T> service) {
return (T) Proxy.newProxyInstance(service.getClassLoader(),
new Class<?>[]{service},
new RpcHandler(service));
}
/**
* 處理具體遠端呼叫的類
*/
static class RpcHandler implements InvocationHandler {
private Class service;
public RpcHandler(Class service) {
this.service = service;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/**
* 根據介面名和方法名,發起一次特定的網路請求,獲取資料
*/
Object result = RemoteCallUtility.request(service.getName(), method.getName(), args);
return result;
}
}
}
而客戶端呼叫的時候不需要介面的具體實現,只需要通過rpc框架獲取介面的代理即可,此時究竟是採用http協議或者直接通過socket請求資料都交由框架負責了,例如:
public class RpcInvoker {
public void invoke() {
OrderInterface order = SimpleRpcFrame.getRemoteProxy(OrderInterface.class);
order.addOrder();
}
}
RPC中的代理則是完全不需要原執行邏輯,而是完全地控制了行為的執行過程
那麼框架使用java動態代理的示例就介紹到此。
4.java動態代理的基本原理
之前我已經通過5篇文章完整介紹了java動態代理的實現原理,不過因為實在有些晦澀,所以這裡我拋棄細節程式碼的解析,使得大家儘量從直覺的角度來理解其基本原理。
假設我們還是實現一開始的新增時間戳的功能,此時,我們需要如下程式碼獲取其代理:
ExecutorInterface executor = (ExecutorInterface) Proxy.newProxyInstance(
Executor.class.getClassLoader(),
new Class[]{ExecutorInterface.class},
new TimeLogHandler()
);
executor.execute(1, 2);
此時,我們列印一下executor的實際類名、所實現的介面和父類的名稱,得到結果如下:
類名:com.sun.proxy.$Proxy11
父類:java.lang.reflect.Proxy
實現介面:ExecutorInterface
因此,生成的代理類有如下3個特點:
1.繼承了Proxy類
2.實現了我們傳入的介面
3.以$Proxy+隨機數字的命名
接著我們還是需要略微檢視一下newProxyInstance方法的原始碼,只需要關心下面幾行核心程式碼,如下:
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
...
/**
* 根據我們傳進來的介面建立一個類
*/
Class<?> cl = getProxyClass0(loader, intfs);
...
/**
* 找到類的構造方法,該構造方法獲取一個InvocationHandler型別的引數
*/
final Constructor<?> cons = cl.getConstructor(constructorParams);
...
/**
* 通過構造方法生成代理類的例項
*/
return cons.newInstance(new Object[]{h});
}
因此總結一下動態代理物件建立的過程
1.根據我們傳入的介面動態地建立一個Class
2.獲取類的建構函式
3.將InvocationHandler作為引數傳入建構函式,例項化代理物件的例項,並將其返回
當然,這裡最核心的方法自然是類的建立,簡而言之,就是在執行時,一個位元組一個位元組地構造一個位元組陣列,而這個位元組陣列正是一個.class位元組碼,然後通過一個native方法,將其轉化為我們執行時的Class類。
再通俗一些來說:平時我們使用的類都是預先編譯好的.class檔案,而動態代理則是直接在執行時通過組裝一個byte陣列的方式建立一個.class檔案,這樣應該就是比較好理解了吧。
如果對這個byte陣列是如何構建的有興趣,那麼歡迎看一下我之前寫的5篇文章,裡面不僅介紹了動態代理的原始碼,還能深入瞭解一下class位元組碼更細節的結構
1.https://www.cnblogs.com/tera/p/13267630.html
2.https://www.cnblogs.com/tera/p/13280547.html
3.https://www.cnblogs.com/tera/p/13336627.html
4.https://www.cnblogs.com/tera/p/13419025.html
5.https://www.cnblogs.com/tera/p/13442018.html
最後,我們來看一下這個生成出來的代理類究竟長啥樣,正符合我們之前總結出的代理物件的3個特點(在之前的文章中也有展示如何看到該內容)。特別注意的是因為所有的類都是繼承自Object,因此除了我們自己介面中定義的方法,還會有Object類的種的方法:
public final class $Proxy11 extends Proxy implements ExecutorInterface {
private static Method m1;
private static Method m2;
private static Method m0;
private static Method m3;
public $Proxy11(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final void execute(int var1, int var2) throws {
try {
super.h.invoke(this, m3, new Object[]{var1, var2});
} catch (RuntimeException | Error var4) {
throw var4;
} catch (Throwable var5) {
throw new UndeclaredThrowableException(var5);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
m3 = Class.forName("cn.tera.aopproxy.proxyuse.ExecutorInterface").getMethod("execute", Integer.TYPE, Integer.TYPE);
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
到此,java動態代理的基本介紹就結束了
最後我們總結一下java動態代理的思想和原理
1.代理的4要素:代理物件、被代理的行為、被代理的物件、行為的完全控制
2.代理的應用:方便地為某些行為新增一些共同的邏輯(Spring AOP)或者是將行為的執行完全交由代理控制(RPC)
3.java動態代理的原理:在執行時構建一個class位元組碼陣列,並將其轉換成一個執行時的Class物件,然後構造其例項