動態代理

yuan發表於2023-02-08

前言:
本小節你將收穫:瞭解代理模式、學會如何實現動態代理、深入探究動態代理的實現原理
@Author:Akai-yuan
@更新時間:2023/2/7

代理模式介紹
代理模式是一種設計模式,提供了對目標物件額外的訪問方式,即透過代理物件訪問目標物件,這樣可以在不修改原目標物件的前提下,提供額外的功能操作,擴充套件目標物件的功能。
簡而言之,代理模式就是設定一箇中間代理來控制訪問原目標物件,以達到增強原物件的功能和簡化訪問方式。比如租房子,我們不能直接聯絡的房東,但是我們可以上網找到中介,中介幫你聯絡到房東,但是中介會進行一些附加操作,比如中間商賺差價。

動態代理實現過程

newProxyInstance方法 - 建立代理物件

即使是動態代理也是代理模式,那麼肯定要有一個方法來建立代理物件,下面這個方法就是用於建立代理物件,即Proxy類下的newProxyInstance方法:

public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,
InvocationHandler h) throws IllegalArgumentException

引數介紹:
loader: 一個ClassLoader物件,定義了由哪個ClassLoader物件來對生成的代理物件進行載入,獲取被代理物件的ClassLoader即可(使用class類下的getClassLoader方法)。
interfaces:一個Interface物件的陣列,被代理物件所實現的所有介面
h: 一個InvocationHandler物件,就是實現InvocationHandler介面的類,表示的是當動態代理物件在呼叫方法的時候,會關聯到哪一個InvocationHandler物件上。

invoke方法 - 呼叫真實物件的方法

jdk動態代理有關的類主要是Proxy類和InvocationHandler介面,兩者都位於java.lang.reflect包,可見它們都是和反射有關的。關於InvocationHandler介面,他只有一個方法:invoke方法(注意不是Mehod類的invoke方法):

Object  invoke(Object proxy, Method method, Object[] args)  throws  Throwable

引數介紹:
**proxy: **指代我們所代理的那個真實物件的物件,也就是代理物件
**method: **指代的是我們所要呼叫真實物件的某個方法
**args: **method方法的引數,以陣列形式表示

編寫實現InvocationHandler介面的類
要動態的建立代理物件的話,我們首先需要編寫一個實現InvocationHandler介面的類,然後重寫其中的invoke方法,其中target是需要被代理的物件(真實物件)。

public class MyProxy implements InvocationHandler {
    /**
     * 被代理的物件,即真實物件,只需要透過某種方式從本類外部獲取即可
     */
    private Object target;
 
    public MyProxy() {
 
    }
 
    /**
     *
     * @param target 被代理的物件
     * @return 返回代理物件
     *
     * 我們可以透過Proxy類透過的靜態方法newProxyInstance來建立被代理物件(target)的代理物件
     * target.getClass().getClassLoader()  獲取被代理物件(target)的類載入器
     * target.getClass().getInterfaces()   獲取被代理物件(target)實現的所有介面
     * this                                實現InvocationHandler介面的自定義類,即本類
     */
    public Object getProxy(Object target) {
        this.target = target;
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
 
 
    /**
     *
     * @param proxy 代理物件(代理類的例項)
     * @param method 被代理物件需要執行執行的方法
     * @param args 方法的引數
     * @return 返回被代理物件(target)執行method方法的結果
     * @throws Throwable 丟擲異常
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //前置附加操作
        System.out.println("執行目標方法之前,可以進行附加操作...");
        //透過反射呼叫真實物件的方法
        Object result = method.invoke(target, args);
        //後置附加操作
        System.out.println("執行目標方法之後,還可以進行附加操作...");
        return result;
    }
    
}

測試用例:

public interface UserMapper {
    void add();
}
public class UserMapperImpl implements UserMapper{
    @Override
    public void add() {
        System.out.println("在UserMapperImpl中,進行的了UserMapper的add方法...");
    }
}
public static void main(String[] args) {
        MyProxy myProxy = new MyProxy();
        //userMapper物件為真實物件
        UserMapperImpl userMapper = new UserMapperImpl();
        //建立代理物件
        UserMapper proxy = (UserMapper) myProxy.getProxy(userMapper);
        //列印真實物件的Class
        System.out.println(userMapper.getClass());
        //列印代理物件的Class
        System.out.println(proxy.getClass());
        proxy.add();
    }

思考:
proxy被強制轉化為了UserMaaper,那麼這個add方法是其實現類UserMapperImpl中的add方法嗎?


透過結果可以看到建立出來的代理物件proxy的型別是“com.sun.proxy.$Proxy0”,$Proxy0代理類在系統內部的編號,這其實它並不是程式編譯之後存在虛擬機器中的類,這個類在我們寫的類裡面是不可能找到的。它是執行時動態生成的,即編譯完成後沒有實際對應的class檔案,而是在執行時動態生成類位元組碼,並載入到JVM中。正是因為代理物件是執行時臨時生成的,這就區別於靜態代理的代理物件類需要先進行編譯之後才能建立代理物件,這一點是動態代理和靜態代理最大的區別,利用這點,動態代理模式建立代理物件的方式比靜態代理靈活許多。
當然,動態建立代理物件的話需要透過反射代理方法,比較消耗系統效能,但動態代理模式明顯是利大於弊的。

代理介面

jdk動態代理代理的是介面,其實只要是一個介面,即使它沒有實現類,動態代理還是可以建立出它的代理類的:

 public <T> T getProxyInstance(Class<?> proxyInterface) {
        Class<?>[] interfaces = null;
        Class<?> clazz = null;
        //如果是介面
        if (proxyInterface.isInterface()) {
            clazz = proxyInterface;
            interfaces = new Class[]{proxyInterface};
        } else {
            //如果不是介面則建立一個例項物件
            try {
                target = proxyInterface.newInstance();
            } catch (InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
                logger.severe("MyProxy類利用反射建立例項物件失敗!");
            }
            clazz = target.getClass();
            interfaces = target.getClass().getInterfaces();
        }

        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), interfaces, this);
    }

我們只需要用於一個class陣列接收介面的class即可,這種直接透過介面建立代理物件的應用其中之一就是mybatis框架,其實我們在service層注入的xxxMapper並不是一個實現類(xxxMapper並沒有實現類),注入的其實是一個xxxMapper的代理物件,如:

@SpringBootTest
class ApplicationTests {
    @Resource
    UserMapper userMapper;

    @Test
    void contextLoads() {
        System.out.println(userMapper.getClass());
    }

}

執行結果:

思考:
jdk動態代理可以只代理介面,沒有實現類也可以,那上面提到的proxy.add()方法執行的就不是UserMapperImpl中的add方法,難道是直接執行UserMapper的add()方法?但是UserMapper是一個介面,它的add方法並沒有執行體,那大家覺得proxy.add()方法到底是哪裡的方法呢?

原理探究

代理物件執行方法的原理

public class Test {
    public static void main(String[] args) {
        // 儲存生成的代理類的位元組碼檔案
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        MyProxy myProxy = new MyProxy();

        UserMapperImpl userMapper = new UserMapperImpl();
        UserMapper proxy = (UserMapper) myProxy.getProxy(userMapper);
        System.out.println(userMapper.getClass());
        System.out.println(proxy.getClass());
        proxy.add();
    }
}

我們可以使用"System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");"儲存執行時生成的$Proxy0類
image.png
該類的全部程式碼:

package com.sun.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import wjh.test.UserMapper;

public final class $Proxy0 extends Proxy implements UserMapper {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $Proxy0(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 void add() throws  {
        try {
            super.h.invoke(this, m3, (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);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("wjh.test.UserMapper").getMethod("add");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

透過觀察發現該類繼承了Proxy類和實現了UserMapper介面,並且有一個方法m1,m2,m3,m0

繼續觀察發現這四個方法其實是透過反射獲取的,其中equals、toString、hashCode方法都是Object類的方法,而其中的m3則是獲取的UserMapper介面的add方法。
image.png
然後當執行$Proxy0的add方法時,執行了程式碼的“super.h.invoke(this, m3, (Object[])null);”,這句話我們現在可能看不懂,但從字面上我們可以透過“invoke”和帶括號的三個引數型別來推測這個“invoke”一定是某個類的某個方法,只是我們不清楚這個類是哪個類,這個方法是哪個方法。

思考:
$Proxy0的add方法執行的是“super.h.invoke(this, m3, (Object[])null);”,super可以理解是它的父類Proxy類,那super.h的h又是什麼呢?h.invoke又是什麼呢?我們看看這三個引數“this、m3、(Object[])null”,它們對於的型別依次是Object、Method、Object[],這三個引數是否感覺到有那麼一點點的眼熟,是否感覺好像在哪裡見過?

下面就讓我們看看這個感覺見過,但又想不起來的東西:


可以看到其中的invoke方法的三個引數型別依次也是:Object、Method、Object[],和上面的“super.h.invoke(this, m3, (Object[])null);”引數型別一樣,而且方法名也都是invoke,這時巧合還是必然?如果是必然的,那麼我們就可以推測出:
呼叫$Proxy0的add方法會進入MyProxy的invoke方法後先執行前置附加操作;然後執行“method.invoke(target, args)”,其中的targe就是被代理的物件UserMapperImpl,method就是則是前面透過反射獲取的m3,即UserMapper介面的add方法(由於UserMapperImpl實現於UserMapper,所以執行的其實是target實現的add方法);最後再執行後置附加操作並返回結果。

那麼這個推測是否正確,暫時不清楚,我們需要接著往下看。

思考:
我們上面透過“System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");”這句程式碼已經瞭解到了$Proxy0類是什麼、有什麼,但是我們是不是忽略了這個類到底是怎麼來的呢?

建立代理物件的原理

大致過程:

直觀圖:

我們知道$Proxy0物件(即代理物件)是透過Proxy的newProxyInstance方法建立的,既然要知道這個物件怎麼來的,那麼就必須要閱讀這個方法的原始碼了,在閱讀原始碼之前,我們先回顧一下我們怎麼呼叫了這個方法:

/**
     *
     * @param target 被代理的物件
     * @return 返回代理物件
     *
     * 我們可以透過Proxy類透過的靜態方法newProxyInstance來建立被代理物件(target)的代理物件
     * target.getClass().getClassLoader()  獲取被代理物件(target)的類載入器
     * target.getClass().getInterfaces()   獲取被代理物件(target)實現的所有介面
     * this                                實現InvocationHandler介面的自定義類,即本類
     */
    public Object getProxy(Object target) {
        this.target = target;
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }

接著我們看一下原始碼,下面就是Proxy類的newProxyInstance方法方法的原始碼:

 public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        Objects.requireNonNull(h);

        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        /*
         * Look up or generate the designated proxy class.
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        /*
         * Invoke its constructor with the designated invocation handler.
         */
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }

            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString(), e);
        }
    }

對傳入的InvocationHandler類進行判空

首先需要對傳進來的InvocationHandler類(即我們這裡的MyProxy類)進行判空,因為後續使用代理物件$Proxy0呼叫的方法都是MyProxy類類的invoke方法,所以這個類是不可以傳空值進來的。
image.png

檢查真實物件是否有生成代理類的許可權

其中checkProxyAccess的作用是檢查是否有生成代理類的許可權。

image.png

生成或查詢$Proxy0類

然後是下圖的方法,它的作用是查詢或生成代理類$Proxy0,需要的引數是真正物件的類載入器和實現的介面,可見這個方法是重中之重,我們再後續介紹
image.png
前面提到的一個“getProxyClass0(loader, intfs)”還沒有結束,只是知道了它的作用是建立一個臨時代理類(即$Proxy0)或者查詢已經存在虛擬機器中的代理類。下面是該方法的原始碼:
image.png

結果我們發現它又呼叫了“proxyClassCache.get(loader, interfaces)”方法,經查閱資料:瞭解到“proxyClassCache”是快取,其目的是為了複用,同時防止多執行緒重複建立同一個代理類。大家可以點進這個get方法的原始碼檢視這個快取機制。

如果快取中沒有代理類,那麼就會生成一個新的代理類,新的代理類是在上面的ProxyClassFactory中生成的,這個類裡面有一個apply方法,它返回的就是代理類$Proxy0,但是這個方法其實也只是做了一些表面工作:為代理類起名、對傳入的介面陣列infs做一些校驗,對一些需要生成代理類的引數進行判空...... 而真正生成代理類的方法是這個方法裡面呼叫的“ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags)”方法,它的返回值是二進位制陣列,在介紹這個方法之前,我們有必要簡單瞭解位元組碼檔案的結構:

相關文章