Java動態代理模式的奧祕

👉本文章全部文字純原創,若是須要轉載,請註明轉載出處,謝謝!😘前端

👉本文中全部的代碼和運行結果都是在amazon corretto openjdk 1.8環境中的,若是你不是使用該環境,可能會略有誤差。另外爲了代碼看起來清晰整潔,將全部代碼中的異常處理邏輯所有拿去了。java

一些廢話

哈嘍,各位讀者您們好,很久不見!距離上一篇我寫的文章已經半個月有餘,沒辦法,我也是菜鳥一枚,並且我寫文章有原則,每一篇都必需要醞釀得夠深入,高質量,可以直擊靈魂深處......若是隻是淺嘗輒止我寧肯不浪費這時間,並且有些內容我也不會正在學習中,因此我輸出的頻率必然是低的,可是質量必然是高的。😎不廢話,下面開始咱們今天的主題。git

今天我要跟你們聊的是Java當中的動態代理模式。相信每個學過Java的朋友,只要是對GOF23設計模式有簡單瞭解過的,或者看過我github上面之前學習時記的筆記,或多或少是據說過代理模式的。這一模式能夠說是GOF23全部設計模式中應用最普遍,但又最難以理解的一種模式,尤爲是其中的動態代理模式,可是其功能之強大,應用場景之廣天然就體現出其重要性。有些場景要是沒有使用這一模式,就會變得很難實現。能夠這麼說,我所瞭解過的或者閱讀過源碼的開源框架,底層幾乎沒有不用到代理模式的,尤爲是接下去本文要說的重點-動態代理模式。所以,在文章的最後,我也會以一個在mybatis底層使用動態代理模式解決的經典場景做爲本文結束。github

代理

首先,咱們先來講說代理。何爲代理?來看張圖。這就是咱們平常租房的場景,客戶來一個陌生城市須要租一個房子,可是他人生地不熟,根本不知道行情,也不知道地段,更沒有房東的聯繫方式,因此,他會去找相似我愛我家之類的租房中介,而這些個中介手上會有大量房子的信息來源,天然會有個房東的聯繫方式,進而和房東取得聯繫,從而達到租房的目的。這個場景就是一個經典的代理模式的體現。數據庫

proxy-1

靜態代理

既然說到動態代理,天然聯想到確定會有靜態代理。下面咱們就先從簡單的開始,以上面租房的這個例子,用Java代碼實現靜態代理。apache

首先在代理模式(甭管靜態仍是動態)結構中,確定會有一個真實角色(Target),也是最後真正執行業務邏輯的那個對象,好比上圖中的房東(由於最後租的房子全部權是他的,也是和他去辦租房合同等手續),另外會有一個代理角色(Proxy),好比上圖中的房產中介(他沒有房產全部權),而且這個角色會必然實現一個與真實角色相同的抽象接口(Subject),爲何呢?由於雖然這個出租的房子不是他的,可是是經他之手幫忙牽線搭橋出租出去的,也就是說,他和房東都會有出租房產的行爲。另外代理角色會持有一個真實角色的引用,又是爲何呢?由於他並不會(或者是不能)真正處理業務邏輯(由於房子不是他的唄),他會將真正的邏輯委託給真實角色處理。可是這個代理角色也不是一無可取,除了房子不是他的,可是他還能夠給你乾點跑腿的工做嘛,好比幫你挑選最好的地段,挑選合適的價格等等,等你租房後出現漏水,或者電器啥的壞了能夠幫你聯繫維修人員等等。以下代碼所示:編程

//公共抽象接口 - 出租的人
public interface Person {
    void rent();
}

//真實角色 - 房東
public class Landlord implements Person{
    public void rent() {
        System.out.println("客官請進,我家的房子又大又便宜,來租個人吧...");
    }
}

//代理角色 - 房產中介
public class Agent implements Person{
    Person landlord;

    public Agent(Person landlord) {
        this.landlord = landlord;
    }

    public void rent() {
      	//前置處理
        System.out.println("通過前期調研,西湖邊的房子環境挺好的...");
      	//委託真實角色處理
        landlord.rent();
      	//後置處理
        System.out.println("房子漏水,幫你聯繫維修人員...");
    }
}

//客戶端
public class Client {
    public static void main(String[] args) {
        Person landlord = new Landlord();
        Person agent = new Agent(landlord);
        agent.rent();
    }
}

//輸出結果:
通過前期調研,西湖邊的房子環境挺好的...
客官請進,我家的房子又大又便宜,來租個人吧...
房子漏水,幫你聯繫維修人員...
複製代碼

靜態代理模式實現相對比較簡單,並且比較好理解,也確實實現了代理的效果。可是很遺憾,幾乎沒有一個開源框架的內部是採用靜態代理來實現代理模式的。那是爲何呢?緣由很簡單,從上面這個例子能夠看出,靜態代理模式中的真實角色和代理角色緊耦合了。怎麼理解?設計模式

下面來舉個例子幫助理解靜態代理模式的缺點,深刻理解靜態代理的缺點對於理解動態代理的應用場景是相當重要的。由於動態代理的誕生就是爲了解決這一問題。數組

仍是以上面的租房的場景,假設我如今須要你實現以下需求:有多個房東,而且每一個房東都有多套房子出租,你怎麼用Java設計?按照上面的靜態代理模式的思路,你也許會有以下實現(僞代碼),緩存

第一種方案:

public class Landlord01 implements Person{
    public void rent01() { ... }
  	public void rent02() { ... }
  	public void rent03() { ... }
}

public class Landlord02 implements Person{
    public void rent01() { ... }
  	public void rent02() { ... }
  	public void rent03() { ... }
}

public class Landlord03 implements Person{
    public void rent01() { ... }
  	public void rent02() { ... }
  	public void rent03() { ... }
}

... 可能還有不少房東,省略

public class Agent01 implements Person{
    Person landlord01;
    //省略構造器等信息
    public void rent() {landlord01.rent();}
}
public class Agent02 implements Person{
    Person landlord02;
    //省略構造器等信息
    public void rent() {landlord02.rent();}
}
public class Agent03 implements Person{
    Person landlord03;
    //省略構造器等信息
    public void rent() {landlord03.rent();}
}

...
複製代碼

上面這種方案是爲每一個房東配一個對應的中介處理租房相關事宜。這種方案問題很是明顯,每個真實角色都須要手動建立一個代理角色與之對應,而這些代理類的邏輯有可能都是很類似的,所以當真實角色數量很是多時,會形成代理類數量膨脹問題和代碼重複冗餘,方案不可取。

第二種方案:

public class Landlord01 implements Person{
    public void rent01() { ... }
  	public void rent02() { ... }
  	public void rent03() { ... }
}

public class Landlord02 implements Person{
    public void rent01() { ... }
  	public void rent02() { ... }
  	public void rent03() { ... }
}

public class Landlord03 implements Person{
    public void rent01() { ... }
  	public void rent02() { ... }
  	public void rent03() { ... }
}

public class Agent implements Person{
    Person landlord01;
  	Person landlord02;
  	Person landlord03;
    //省略構造器等信息
    public void rent01() { ... }
   	public void rent02() { ... }
   	public void rent03() { ... }
}
複製代碼

第二種方案只建立一個代理角色,同時代理多個真實角色,這看上去貌似解決了第一種方案的弊病,可是同時引入了新的問題。那就是形成了代理類的膨脹。設計模式中有條重要原則——單一職責原則。這個代理類違反了該原則。當這個代理類爲了代理其中某個真實角色時,須要將全部的真實角色的引用所有傳入,顯然太不靈活了。仍是不可取。

並且有沒有發現靜態代理還有兩個很大的問題,第一,當抽象接口一旦修改,真實角色和代理角色必須所有作修改,這違反了設計模式的開閉原則。第二,每次建立一個代理角色,須要手動傳入一個已經存在的真實角色。可是在有些場景下,咱們可能須要在並不知道真實角色的狀況下建立出指定接口的代理。

動態代理

前面作了這麼多鋪墊,終於今天本文的主角——動態代理模式要登場了。此處應該有掌聲......👏而動態代理模式的產生就是爲了解決上面提到的靜態代理全部弊病的。

JDK動態代理的實現關鍵在於java.lang.reflect.Proxy類,其newProxyInstance(ClassLoader loader,Class<?>[] interfaces, InvocationHandler h)方法是整個JDK動態代理的核心,用於生成指定接口的代理對象。這個方法有三個參數,分別表示加載動態生成的代理類的類加載器ClassLoader,代理類須要實現的接口interfaces以及調用處理器InvocationHandler,這三個參數一個比一個難以理解,說實話,我第一次學動態代理模式時,看到這三個參數也是一臉懵逼的狀態。動態代理模式之因此比較難理解關鍵也是這個緣由。放心,後面會一一詳解。但在這以前,咱們先作一下熱身,先用代碼簡單使用一下JDK的動態代理功能。代碼以下:

//公共抽象接口和真實角色和靜態代理的例子中代碼相同,省略

//自定義調用處理器
public class RentHandler implements InvocationHandler {
    Person landlord;
  
    public RentHandler(Person landlord) {
        this.landlord = landlord;
    }
	//客戶端對代理對象發起的全部請求都會被委託給該方法
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      	//前置處理
        System.out.println("通過前期調研,西湖邊的房子環境挺好的...");
      	//委託給真實角色處理業務邏輯
        method.invoke(landlord, args);
      	//後置處理
        System.out.println("房子漏水,幫你聯繫維修人員...");
        return null;
    }
}

//客戶端
public class Client2 {
    public static void main(String[] args) {
        Person landlord = new Landlord();
        Person proxy = (Person) Proxy.newProxyInstance(
          ClassLoader.getSystemClassLoader(), //加載代理類的類加載器
          new Class[]{Person.class}, //代理的接口
          new RentHandler(landlord));//自定義調用處理器實現
        proxy.rent();
    }
}

//輸出結果:
通過前期調研,西湖邊的房子環境挺好的...
客官請進,我家的房子又大又便宜,來租個人吧...
房子漏水,幫你聯繫維修人員...
複製代碼

能夠看出,動態代理輕鬆的實現了代理模式,而且輸出了和靜態代理相同的結果,然而咱們並無寫任何的代理類,是否是很神奇?下面咱們就來深度剖析JDK實現的動態代理的原理。

Proxy.newProxyInstance()

在上面實現的JDK動態代理代碼中,核心的一行代碼就是調用Proxy.newProxyInstance(),傳入類加載器等參數,而後一頓神奇的操做後竟然就直接返回了咱們所須要的代理對象,所以咱們就從這個神奇的方法開始提及......

進入這個方法的源碼中,如下是這個方法的核心代碼,邏輯很是清楚,使用getProxyClass0獲取一個Class對象,其實這個就是最終生成返回的代理代理類的Class對象,而後使用反射方式獲取有參構造器,並傳入咱們的自定義InvocationHandler實例建立其對象。由此咱們其實已經能夠猜想,這個動態生成的代理類會有一個參數爲InvocationHandler的構造器,這一點在以後會獲得驗證。

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException {
		... //省略一些非空校驗,權限校驗的邏輯
        //返回一個代理類,這個是整個方法的核心,後續會作詳細剖析
        Class<?> cl = getProxyClass0(loader, intfs);
		//使用反射獲取其有參構造器,constructorParams是定義在Proxy類中的字段,值爲{InvocationHandler.class}
        final Constructor<?> cons = cl.getConstructor(constructorParams);
		//使用返回建立代理對象
        return cons.newInstance(new Object[]{h});

}
複製代碼

那如今很明顯了,關鍵的核心就在於getProxyClass0()方法的邏輯了,因而咱們繼續深刻虎穴查看其源碼。

private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) {
        if (interfaces.length > 65535) {
            throw new IllegalArgumentException("interface limit exceeded");
        }
        return proxyClassCache.get(loader, interfaces);
}
複製代碼

最開始就是檢驗一下實現接口數量,而後執行proxyClassCache.get()。proxyClassCache是一個定義在Proxy中的字段,你就將其當作一個代理類的緩存。這個也好理解,稍後你們會看到,動態代理類生成過程當中會伴隨大量的IO操做,字節碼操做還有反射操做,仍是比較消耗資源的。若是須要建立的代理類數量特別多,性能會比較差。因此Proxy提供了緩存機制,將已經生成的代理類緩存,當獲取時,會先從緩存獲取,若是獲取不到再執行生成邏輯。

咱們繼續進入proxyClassCache.get()。這個方法看起來比較費勁,由於我使用的是JDK8,這邊用到了大量的Java8新增的函數式編程的語法和內容,由於這邊不是專門講Java8的,因此我就不展開函數式編程的內容了。之後有機會在其它專題詳述。另外,這邊會有不少對緩存的操做,這個不是咱們的重點,因此也所有跳過,咱們挑重點看,關注一下下面這部分代碼:

public V get(K key, P parameter){
  ... //省略大量的緩存操做
    while (true) {
      if (supplier != null) {
        V value = supplier.get();
        if (value != null) {
          return value;	★
        }
      }
      if (factory == null) {
        factory = new WeakCache.Factory(key, parameter, subKey, valuesMap); ▲
      }

      if (supplier == null) {
        supplier = valuesMap.putIfAbsent(subKey, factory);
        if (supplier == null) {
          supplier = factory;
        }
      } else {
        if (valuesMap.replace(subKey, supplier, factory)) {
          supplier = factory;
        } else {
          supplier = valuesMap.get(subKey);
        }
      }
    }
}
複製代碼

這個代碼很是有意思,是一個死循環。或許你和我同樣,徹底看不懂這代碼是啥意思,不要緊,能夠仔細觀察一下這代碼你就會發現柳暗花明。這個方法最後會須要返回一個從緩存或者新建立的代理類,而這整個死循環只有一個出口,沒錯就是帶★這一行,而value是經過supplier.get()得到,Supplier是一個函數式接口,表明了一種數據的獲取操做。咱們再觀察會發現,supplier是經過factory賦值而來的。而factory是經過▲行建立出來的。WeakCache.Factory剛好是Supplier的實現。因此咱們進入WeakCache.Factory的get(),核心代碼以下,經觀察能夠發現,返回的數據最終是經過valueFactory.apply()返回的。

public synchronized V get() {
		... //省略一些緩存操做
        V value = null;
        value = Objects.requireNonNull(valueFactory.apply(key, parameter));
        ... //省略一些緩存操做
        return value;
}
複製代碼

apply是BiFunction的一個抽象方法,BiFunction又是一個函數式接口。而valueFactory是經過WeakCache的構造器傳入,是一個ProxyClassFactory對象,而其恰好就是BiFunction的實現,顧名思義,這個類就是專門用來建立代理類的工廠類。

proxy-2

進入ProxyClassFactory的apply()方法,代碼以下:

Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
		//對每個指定的Class校驗其是否能被指定的類加載器加載以及校驗是不是接口,動態代理只能對接口代理,至於緣由,後面會說。
        for (Class<?> intf : interfaces) {
            Class<?> interfaceClass = null;
                interfaceClass = Class.forName(intf.getName(), false, loader);
            if (interfaceClass != intf) {
                throw new IllegalArgumentException(
                        intf + " is not visible from class loader");
            }          	
            if (!interfaceClass.isInterface()) {
                throw new IllegalArgumentException(
                        interfaceClass.getName() + " is not an interface");
            }
            if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                throw new IllegalArgumentException(
                        "repeated interface: " + interfaceClass.getName());
            }
        }
		//下面這一大段是用來指定生成的代理類的包信息
		//若是全是public的,就是用默認的com.sun.proxy,
		//若是有非public的,全部的非public接口必須處於同一級別包下面,而該包路徑也會成爲生成的代理類的包。
        String proxyPkg = null;     
        int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

        for (Class<?> intf : interfaces) {
            int flags = intf.getModifiers();
            if (!Modifier.isPublic(flags)) {
                accessFlags = Modifier.FINAL;
                String name = intf.getName();
                int n = name.lastIndexOf('.');
                String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                if (proxyPkg == null) {
                    proxyPkg = pkg;
                } else if (!pkg.equals(proxyPkg)) {
                    throw new IllegalArgumentException(
                            "non-public interfaces from different packages");
                }
            }
        }

        if (proxyPkg == null) {
            proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
        }

        long num = nextUniqueNumber.getAndIncrement();
				//代理類最後生成的名字是包名+$Proxy+一個數字
        String proxyName = proxyPkg + proxyClassNamePrefix + num;
				//生成代理類的核心
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, interfaces, accessFlags);★
            return defineClass0(loader, proxyName,
                    proxyClassFile, 0, proxyClassFile.length);
    }
複製代碼

經過上面代碼不難發現,生成代理類的核心代碼在★這一行,會使用一個ProxyGenerator生成代理類(以byte[]形式存在)。而後將生成獲得的字節數組轉換爲一個Class對象。進入ProxyGenerator.generateProxyClass()。ProxyGenerator處於sun.misc包,不是開源的包,由於我這邊使用的是openjdk,因此能夠直接查看其源碼,若是使用的是oracle jdk的話,這邊只能經過反編譯class文件查看。

public static byte[] generateProxyClass(final String name, Class<?>[] interfaces, int accessFlags) {
        ProxyGenerator gen = new ProxyGenerator(name, interfaces, accessFlags);
        final byte[] classFile = gen.generateClassFile();

        if (saveGeneratedFiles) {
            //省略一堆IO操做
        }
        return classFile;
 }
複製代碼

上述邏輯很簡單,就是使用一個生成器調用generateClassFile()方法返回代理類,後面有個if判斷我簡單提一下,這個做用主要是將內存中動態生成的代理類以class文件形式保存到硬盤。saveGeneratedFiles這個字段是定義在ProxyGenerator中的字段,

private final static boolean saveGeneratedFiles =
        java.security.AccessController.doPrivileged(
            new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles")).booleanValue();
複製代碼

我簡單說一下,AccessController.doPrivileged這個玩意會去調用java.security.PrivilegedAction的run()方法,GetBooleanAction這個玩意就實現了java.security.PrivilegedAction,在其run()中會經過Boolean.getBoolean()從系統屬性中獲取sun.misc.ProxyGenerator.saveGeneratedFiles的值,默認是false,若是想要將動態生成的class文件持久化,能夠往系統屬性中設置爲true。

咱們重點進入ProxyGenerator.generateClassFile()方法,代碼以下:

private byte[] generateClassFile() {
        addProxyMethod(hashCodeMethod, Object.class);
        addProxyMethod(equalsMethod, Object.class);
        addProxyMethod(toStringMethod, Object.class);
        for (Class<?> intf : interfaces) {
            for (Method m : intf.getMethods()) {
                addProxyMethod(m, intf);
            }
        }
        for (List<ProxyGenerator.ProxyMethod> sigmethods : proxyMethods.values()) {
            checkReturnTypes(sigmethods);
        }
        methods.add(generateConstructor());

        for (List<ProxyGenerator.ProxyMethod> sigmethods : proxyMethods.values()) {
            for (ProxyGenerator.ProxyMethod pm : sigmethods) {

                fields.add(new ProxyGenerator.FieldInfo(pm.methodFieldName,
                        "Ljava/lang/reflect/Method;",
                        ACC_PRIVATE | ACC_STATIC));

                methods.add(pm.generateMethod());
            }
        }
        methods.add(generateStaticInitializer());
        if (methods.size() > 65535) {
            throw new IllegalArgumentException("method limit exceeded");
        }
        if (fields.size() > 65535) {
            throw new IllegalArgumentException("field limit exceeded");
        }

        cp.getClass(dotToSlash(className));
        cp.getClass(superclassName);
        for (Class<?> intf : interfaces) {
            cp.getClass(dotToSlash(intf.getName()));
        }

        cp.setReadOnly();

        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        DataOutputStream dout = new DataOutputStream(bout);

        dout.writeInt(0xCAFEBABE);
        // u2 minor_version;
        dout.writeShort(CLASSFILE_MINOR_VERSION);
        // u2 major_version;
        dout.writeShort(CLASSFILE_MAJOR_VERSION);

        cp.write(dout);             // (write constant pool)

        // u2 access_flags;
        dout.writeShort(accessFlags);
        // u2 this_class;
        dout.writeShort(cp.getClass(dotToSlash(className)));
        // u2 super_class;
        dout.writeShort(cp.getClass(superclassName));

        // u2 interfaces_count;
        dout.writeShort(interfaces.length);
        // u2 interfaces[interfaces_count];
        for (Class<?> intf : interfaces) {
            dout.writeShort(cp.getClass(
                    dotToSlash(intf.getName())));
        }

        // u2 fields_count;
        dout.writeShort(fields.size());
        // field_info fields[fields_count];
        for (ProxyGenerator.FieldInfo f : fields) {
            f.write(dout);
        }

        // u2 methods_count;
        dout.writeShort(methods.size());
        // method_info methods[methods_count];
        for (ProxyGenerator.MethodInfo m : methods) {
            m.write(dout);
        }
        // u2 attributes_count;
        dout.writeShort(0);

        return bout.toByteArray();
    }
複製代碼

若是沒有學過Java虛擬機規範中關於字節碼文件結構的知識的話,上面這段代碼確定是看得一頭霧水,由於本文主要是講解動態代理,加上我的對Java虛擬機的掌握也是菜鳥級別,因此下面就簡單闡述一下關於字節碼結構的內容以便你們理解上面這塊代碼,可是不展開詳說。

Class文件結構簡述

在Java虛擬機規範中,Class文件是一組二進制流,每一個Class文件會對應一個類或者接口的定義信息,固然,Class文件並非必定以文件形式存在於硬盤,也有可能直接由類加載器加載到內存。每個Class文件加載到內存後,通過一系列的加載、鏈接、初始化過程,而後會在方法區中造成一個Class對象,做爲外部訪問該類信息的的惟一入口。按照Java虛擬機規範,Class文件是具備很是嚴格嚴謹的結構規範,由一系列數據項組成,各個數組項之間沒有分隔符的結構緊湊排列。每一個數據項會有相應的數據類型,以下表就是一個完整Class文件結構的表。

proxy-3

其中名稱一列就是組成Class文件的數據項,限於篇幅這邊就不展開詳細解釋每一項了,你們有興趣能夠本身去查點資料瞭解一下,左邊是其類型,主要分兩類,像u2,u4這類是無符號數,分別表示2個字節和4個字節。以info結尾的是表結構,表結構又是一個複合類型,由其它的無符號數和其餘的表結構組成。

我這邊以相對結構簡單的field_info結構舉個例子,field_info結構用來描述接口或者類中的變量。它的結構以下:

proxy-4

其它的表結構method_info,attribute_info也都是相似,都會有本身特有的一套結構規範。

好了,簡單瞭解一下Class文件結構後,如今再回到咱們的主題來,咱們再來研究ProxyGenerator.generateClassFile()方法內容就好理解了。其實這個方法就作了一件事情,就是根據咱們傳入的這些個信息,再按照Java虛擬機規範的字節碼結構,用IO流的方式寫入到一個字節數組中,這個字節數組就是代理類的Class文件。默認狀況這個Class文件直接存在內存中,爲了更加深刻理解動態代理原理,該是時候去看看這個文件究竟是啥結構了。怎麼看?還記得前面提到過的sun.misc.ProxyGenerator.saveGeneratedFiles嗎?只要咱們往系統屬性中加入該參數並將其值設爲true,就會自動將該方法生成的byte[]形式的Class文件保存到硬盤上,以下代碼:

public class Client2 {
    public static void main(String[] args) {
      	//加入該屬性並設置爲true
        System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        Person landlord = new Landlord();
        Person proxy = (Person) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Person.class}, new RentHandler(landlord));
        proxy.rent();
    }
}
複製代碼

再次運行,神奇的一幕發生了,工程中多了一個類,沒錯,這就是JDK動態代理生成的代理類,由於咱們的接口是public修飾,因此採用默認包名com.sun.proxy,類名以$Proxy開頭,後面跟一個數字,和預期徹底吻合。完美!🤩

proxy-5

那麼就讓咱們反編譯一下這個class文件看看它的內容來一探究竟......

下面是反編譯獲得的代理類的內容,

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

    public $Proxy0(InvocationHandler var1) throws { ②
        super(var1);
    }

    public final boolean equals(Object var1) throws {	④
        return (Boolean) super.h.invoke(this, m1, new Object[]{var1});
    }

    public final void rent() throws {	③
        super.h.invoke(this, m3, (Object[]) null);
    }

    public final String toString() throws {	④
        return (String) super.h.invoke(this, m2, (Object[]) null);
    }

    public final int hashCode() throws {	④
        return (Integer) super.h.invoke(this, m0, (Object[]) null);
    }

    static {	①
        m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
        m3 = Class.forName("com.dujc.mybatis.proxy.Person").getMethod("rent");
        m2 = Class.forName("java.lang.Object").getMethod("toString");
        m0 = Class.forName("java.lang.Object").getMethod("hashCode");
    }
}
複製代碼

👉有幾個關注點

  • 標註①的是一個靜態代碼塊,當代理類一被加載,會馬上初始化,用反射方式獲取獲得被代理的接口中方法和Object中equals(),toString(),hashCode()方法的Method對象,並將其保存在屬性中,爲後續請求分派作準備。
  • 標註②的是帶有一個帶有InvocationHandler類型參數的構造器,這個也驗證了咱們以前的猜想,沒錯,代理類會經過構造器接收一個InvocationHandler實例,再觀察標記★的地方,代理類繼承了Proxy類,其實代理類會經過調用父類構造器將其保存在Proxy的屬性h中,天然會繼承給當前這個代理類,這個InvocationHandler實例爲後續請求分派作準備。同時由此咱們也能夠得出結論,Proxy是全部的代理類的父類。另外再延伸,由於Java是一門單繼承語言,因此意味着代理類不可能再經過繼承其餘類的方式來擴展。因此,JDK動態代理無法對不實現任何接口的類進行代理,緣由就在於此。這或許也是動態代理模式很少的缺點之一。若是須要繼承形式的類代理,可使用CGLIB等類庫。
  • 標註③的是咱們指定接口Person中的方法,標註④的是代理類繼承自Object類中的equals(),toString(),hashCode()方法。再觀察這些方法內部實現,全部的方法請求所有委託給以前由構造器傳入的InvocationHandler實例的invoke()方法處理,將當前的代理類實例,各方法的Method對象和方法參數傳入,最後返回執行結果。由此得出結論,動態代理過程當中,所指定接口的方法以及Object中equals(),toString(),hashCode()方法會被代理,而Object其餘方法則並不會被代理,並且全部的方法請求所有都是委託給咱們本身寫的自定義InvocationHandler的invoke()方法統一處理,哇塞,O了,這樣的處理實在太優雅了!

動態代理到底有什麼卵用

其實通過上面這一堆講解,動態代理模式中最核心的內容基本都分析完了,相信你們應該對其也有了一個本質的認知。學以至用,技術再牛逼若是無法用在實際工做中也說實話也只能拿來裝逼了。那這個東西到底有什麼卵用呢?其實我之前學完動態代理模式後第一感受是,嗯,這玩意確實挺牛逼的,可是到底有什麼用?沒有一點概念。在閱讀Spring或者Mybatis等經典開源框架中的代碼時,時不時也常常會發現動態代理模式的身影,可是仍是沒有一個直接的感覺。直到最近一段時間我在深刻研究Mybatis源碼時,看到其日誌模塊的設計,內部就是使用了動態代理,突然靈光一閃,大受啓發感受一會兒全想通了......這就是冥冥之中註定的吧?😂因此最後我就拿這個例子給你們講解一下動態代理模式的實際應用場景。

想必使用過Mybatis這一優秀持久層框架的人都注意到過,每當咱們執行對數據庫操做,若是日誌級別是DEBUG,控制檯會打印出一些輔助信息,好比執行的SQL語句,綁定的參數和參數值,返回的結果等,大家有沒有想過這些信息究竟是怎麼來的?

在Mybatis底層的日誌模塊中,有一塊專門用於打印JDBC相關信息日誌的功能。這塊功能是由一系列xxxLogger類構成。其中最頂層的是BaseJdbcLogger,他有4個子類,繼承關係以下圖:

proxy-6

看名字應該就能猜出來是幹啥了,以ConnectionLogger爲例,下面是ConnectionLogger的關鍵代碼:

public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler { ❶

    private final Connection connection;	

    private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
        super(statementLog, queryStack);
        this.connection = conn;	❷
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] params)throws Throwable {
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, params);
        }
        if ("prepareStatement".equals(method.getName())) {
            if (isDebugEnabled()) {
                debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
            }
            PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
            stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
            return stmt;
        } else if ("prepareCall".equals(method.getName())) {
            if (isDebugEnabled()) {
                debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
            }
            PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
            stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
            return stmt;
        } else if ("createStatement".equals(method.getName())) {
            Statement stmt = (Statement) method.invoke(connection, params);
            stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
            return stmt;
        } else {
            return method.invoke(connection, params);
        }
    }

    public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
        InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
        ClassLoader cl = Connection.class.getClassLoader();
        return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
    }
}
複製代碼

怎麼樣?是否是有種熟悉的感受?🙀

👉觀察上面代碼,能夠得出如下幾點結論:

  • ConnectionLogger實現了InvocationHandler,經過構造器傳入真實Connection對象,這是一個真實對象,並將其保存在屬性,後續請求會委託給它執行。其靜態方法newInstance()內部就是經過Proxy.newProxyInstance()並傳入類加載器等一系列參數返回一個Connection的代理對象給前端。該方法最終會在DEBUG日誌級別下被org.apache.ibatis.executor.BaseExecutor.getConnection()方法調用返回一個Connection代理對象。

  • 前面說過,JDK動態代理會將客戶端全部的請求所有派發給InvocationHandler的invoke()方法,即上面ConnectionLogger中的invoke()方法。invoke()方法當中,不難發現,Mybatis對於Object中定義的方法,統一不作代理處理,直接調用返回。對於prepareStatement(),prepareCall(),createStatement()這三個核心方法會統一委託給真實的Connection對象處理,而且在執行以前會以DEBUG方式打印日誌信息。除了這三個方法,Connection其它方法也會被真實的Connection對象代理,可是並不會打印日誌信息。咱們以prepareStatement()方法爲例,當真實的Connection對象調用prepareStatement()方法會返回PreparedStatement對象,這又是一個真實對象,可是Mybatis並不會將該真實對象直接返回,並且經過調用PreparedStatementLogger.newInstance()再次包裝代理,看到這個方法名字,我相信聰明的您都能猜到這個方法的邏輯了。沒錯,PreparedStatementLogger類的套路和ConnectionLogger一模一樣。這邊我再貼回PreparedStatementLogger的代碼,

    public final class PreparedStatementLogger extends BaseJdbcLogger implements InvocationHandler {
    
        private final PreparedStatement statement;
    
        private PreparedStatementLogger(PreparedStatement stmt, Log statementLog, int queryStack) {
            super(statementLog, queryStack);
            this.statement = stmt;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, params);
            }
            if (EXECUTE_METHODS.contains(method.getName())) {
                if (isDebugEnabled()) {
                    debug("Parameters: " + getParameterValueString(), true);
                }
                clearColumnInfo();
                if ("executeQuery".equals(method.getName())) {
                    ResultSet rs = (ResultSet) method.invoke(statement, params);
                    return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
                } else {
                    return method.invoke(statement, params);
                }
            } else if (SET_METHODS.contains(method.getName())) {
                if ("setNull".equals(method.getName())) {
                    setColumn(params[0], null);
                } else {
                    setColumn(params[0], params[1]);
                }
                return method.invoke(statement, params);
            } else if ("getResultSet".equals(method.getName())) {
                ResultSet rs = (ResultSet) method.invoke(statement, params);
                return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
            } else if ("getUpdateCount".equals(method.getName())) {
                int updateCount = (Integer) method.invoke(statement, params);
                if (updateCount != -1) {
                    debug(" Updates: " + updateCount, false);
                }
                return updateCount;
            } else {
                return method.invoke(statement, params);
            }
        }
    
        public static PreparedStatement newInstance(PreparedStatement stmt, Log statementLog, int queryStack) {
            InvocationHandler handler = new PreparedStatementLogger(stmt, statementLog, queryStack);
            ClassLoader cl = PreparedStatement.class.getClassLoader();
            return (PreparedStatement) Proxy.newProxyInstance(cl, new Class[]{PreparedStatement.class, CallableStatement.class}, handler);
        }
    }
    複製代碼

    這個代碼的邏輯我就不講了,思路幾乎和ConnectionLogger徹底一致。無非是攔截的方法不一樣,由於此次被代理對象是PreparedStatement,因此此次會去攔截都是PreparedStatement的方法,好比setXXX()系列,executeXX()系列等方法。而後在指定方法執行先後添加須要的DEBUG日誌信息,perfect!以getResultSet方法爲例,PreparedStatement對象調用getResultSet()後,會返回真實的ResultSet對象,可是同樣的套路,並不會直接將該真實對象返回,而是由調用ResultSetLogger.newInstance()再次將該ResultSet對象包裝,ResultSetLogger的代碼相信聰明的您不須要我再花篇幅講了。

    這個時候,再回過頭思考一下,上面這個場景下,若是是採用靜態代理是否是根本無法完成了?由於,每個數據庫鏈接都會產生一個新的Connection對象,而每個Connection對象每次調用preparedStatement()方法都會產生一個新的PreparedStatement對象,而每個PreparedStatement對象每次調用getResultSet()又都會產生一個新的ResultSet對象,跟上面的多個房東出租房子一個道理,就會產生不可勝數處理邏輯極其類似的代理類,因此,這纔是開源框架底層不採用靜態代理的本質緣由!一切都豁然開朗了!😍

結束

好了,關於JDK動態代理的核心原理部分到這裏算所有講解完畢了,其實咱們聊了這麼多,都是圍繞着java.lang.reflect.Proxy.newProxyInstance()這個方法展開的。其實在Proxy類中,還有一個getProxyClass()方法,這個只須要傳入加載代理類的類加載器和指定接口就能夠動態生成其代理類,我一開始說到靜態代理弊病的時候說過,靜態代理建立代理時,真實角色必需要存在,不然這個模式無法進行下去,可是JDK動態代理能夠作到在真實角色不存在的狀況下就返回該接口的代理類。至於Proxy其它的方法都比較簡單了,此處再也不贅述。

今天和你們一塊兒探索JDK動態代理模式原理的技術之旅到此結束,但願這篇文章能夠給你們帶來學習或者工做上的幫助,也不枉我一個字一個字的手敲了這麼多字......🥺之後相信你們對莫測高深的動態代理模式也不會再談「動態代理」色變了。接下去,我會繼續抽出空閒時間給你們分享本身學習工做過程踩過的坑,思考過的成果,分享他人同時也對本身的知識掌握輸出整理,也但願你們能夠繼續關注我,我們下次不見不散。😋


  • 今天的技術分享就分享到這裏,感謝您百忙抽出這麼長時間閱讀個人文章😊。
  • 另外,個人學習過程當中一些記錄和心得,還有文章都會首先在個人github上更新,不嫌棄能夠star關注一下。
    個人github主頁:github.com/dujunchen/B…
相關文章
相關標籤/搜索