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

tera發表於2020-11-01

前言

之前已經用了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物件,然後構造其例項

相關文章