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

前言

以前已經用了5篇文章完整解釋了java動態代理的原理,本文將會爲這個系列補上最後一塊拼圖,展現java動態代理的使用方式和應用場景html

主要分爲如下4個部分java

1.爲何要使用java動態代理redis

2.如何使用java動態代理spring

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對象,而後構造其實例

相關文章
相關標籤/搜索