【設計模式】動態代理

其實這篇文章已經寫完很久了,但是最近沉迷於JUC源碼,所以一直放在草稿箱沒有發。這幾天把AQS以及相關的子類擼了一遍,寫了幾篇JUC源碼解讀,準備發到公號上面來,於是乎決定把草稿箱的這篇文章先解決了

 

我上一篇文章已經通過代購的例子討論了靜態代理模式,也知道了它不滿足開閉原則,擴展性差的缺點,因此,衍生出了動態代理。動態代理分兩種,一種是基於接口的,也是jdk本身支持的,還有一種是基於類的,需引入第三方類cglib來實現,今天我們主要討論基於接口的動態代理(看這篇之前建議看看我前一篇講靜態代理的文章,代購小哥發家史還是值得學習的

 

一.  概述

其實動態代理的類之間的關係和靜態代理是一樣

JDK的動態代理類需要依靠兩個類(接口)來實現,Proxy靜態類,InvocationHandler接口。Proxy類的職責是產生代理Class對象與實例;InvocationHandle接口的職責是負責調用服務和增強服務的(這兩個類與接口都嚴格的遵守了設計模式中的單一職責原則)

 

是不是有點懵逼,我們還是繼續通過代購小哥來理解吧(不瞭解代購小哥發家史的,可以看看我公號文章:“通過代購理解:靜態代理”)

 

二.  如何使用動態代理

 

代購小哥生意越做越大,一個人已經忙不過來了,所以他決定召集周圍的代購同行,成立一個代購公司。以後來一個顧客,代購公司就分配一個代購小哥哥給他,這樣,就可以滿足更多妹子的買買買需求啦

 

這個代購公司的員工就是由Proxy靜態類來生成,員工所需要執行的服務則是由實現InvocationHandler接口的類來確定的,我們看下代碼吧。

 

首先我們建立包包商店和鞋鞋商店的接口與實現類

public interface IBagShop {
    void buyBag(String num);
}
public class BagShop implements IBagShop {
    @Override
    public void buyBag(String money) {
        System.out.println("買了一個價值"+money+"限量款包包!");
    }
}
public interface IShoesShop {
    void buyShoes(String num);
}
public class ShoesShop implements IShoesShop{
    @Override
    public void buyShoes(String money) {
        System.out.println("買了一雙價值" + money + "的喬丹!!");
    }
}

然後我們再看看代理公司類咋寫

public class PurchasingAgentCompany implements InvocationHandler {

    private Object shop;
    
    public void setShop(Object shop) {
        this.shop = shop;
    }

    //通過Proxy獲取動態代理的對象(代購小哥)
    public Object getProxyInstance() {
        return Proxy.newProxyInstance(shop.getClass().getClassLoader(), shop.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("代購售前服務:砍價");
        Object ret = method.invoke(shop, args);
        System.out.println("代購售後服務:送小禮品");
        return ret;
    }
}

我們可以看到代購公司PurchasingAgentCompany類繼承了InvocationHandler接口,並實現了其invoke方法來調用服務和增強服務

而getProxyInstance方法中使用了Prxoy靜態類的newProxyInstance方法來獲取代理對象

最後我們創建一個妹子類來找代購公司買東西

public class Girl {
    public static void main(String[] args) {
        // 包包商店 與 鞋鞋商店
        IBagShop bagShop = new BagShop();
        IShoesShop shoesShop = new ShoesShop();
        // 成立一家代購公司
        PurchasingAgentCompany company = new PurchasingAgentCompany();

        // 一個顧客買包(所以代購公司需要引入實際的服務提供者:包包商店)
        company.setShop(bagShop);
        // 代購公司分配了一個包包商店的代購bagShopProxy
        IBagShop bagShopProxy = (IBagShop) company.getProxyInstance();
        // 代購買包包
        bagShopProxy.buyBag("10萬");

        System.out.println("------------------------------------");
        // 一個顧客買鞋子(所以代購公司需要引入實際的服務提供者:謝謝商店)
        company.setShop(shoesShop);
        // 代購公司分配了一個鞋鞋商店的代購shoesShopProxy
        IShoesShop shoesShopProxy = (IShoesShop) company.getProxyInstance();
        // 代購買鞋鞋
        shoesShopProxy.buyShoes("1萬");

    }
}

我們可以看到,如果我們顧客還需要代購公司代購傢俱,我們只需要擴展傢俱接口與其實現類,妹子就可以直接通過代購公司類來購買了,而代購公司的代碼不需要動一下。所以動態代理類很好的保證了開閉原則。我們看下輸出結果

 

 

通過上面的例子與代碼,相信大家已經知道如何通過Proxy靜態類和InvocationHandler接口來實現動態代理了。

 

但大家現在一定還是會一臉懵逼,Proxy是如何創造出代理對象的呢?代理對象又是怎麼通過實現InvocationHandler的invoke方法來調用服務呢?動態代理用起來是很簡單的,但如何實現動態代理纔是我們需要去關注的。下面我們通過源碼來看看是如何實現動態代理的。

 

三.  源碼解讀

 

前面一節我們知道,方法newProxyInstance是生成動態代理對象的,而生成動態代理對象可以分爲兩步,首先我們需要生成Class對象,然後再利用反射技術通過Class對象和InvocationHandler對象來生成Class對象的實例對象,即動態代理對象。那我們按照步驟來分析源碼吧

 

我們看下Proxy靜態類的newProxyInstance方法

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
    // 省略 ...
    final Class<?>[] intfs = interfaces.clone(); 
    // 省略 ...
    Class<?> cl = getProxyClass0(loader, intfs);

    // 省略 ...
    final Constructor<?> cons = cl.getConstructor(constructorParams);
    // 省略 ...
    return cons.newInstance(new Object[]{h});
}

可以看到,上面就4行源碼,爲什麼只有4行呢?因爲與生成代理對象直接相關的源碼就這4行啊,其他都是安全檢查校驗之類的,所以我們抓主幹,其他的大家有興趣可以自己看看(我反正是沒太多興趣,哈哈,至少我現階段是沒必須要去追求這種細枝末節與主幹無關的邏輯)我們先解釋下這4行代碼是幹啥的:

 

  1. 我們首先將傳入的接口通過clone方法複製一份(即代購實例中的IShoesShop與IBagShop接口),

  2. 通過傳入類加載器與接口(被代理對象的類加載器與其實現的接口)給getProxyClass0方法生成代理Class對象(即代購例子中的ShoesShop和BagShop類的代理Class對象),

  3. 通過Class對象獲得構造器

  4. 通過構造器生成Class對象的實例對象(構造器的參數是實現了InvocationHandler接口的對象,即代購例子中的代購公司PurchasingAgentCompany)

 

所以第1、2行代碼是生成我們代理Class對象的,我們先把重點放在弄清楚如何生成代理Class對象,並且知道這個代理Class對象到底是長啥樣的,只有清楚這兩點,我們才能理解3、4行代碼。

 

既然要知道如何生成代理Class對象,我們就要進入getProxyClass0方法 

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

getProxyClass0方法首先會檢查下接口長度,如果沒超過65535字節,則會通過調用WeakCache類中的get方法。proxyClassCache即爲WeakCache的實例。

 

我們繼續進入proxyClassCache.get方法 

public V get(K key, P parameter) {
    // 省略 ...
    Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
    // 省略 ...
}

subKeyFactory是Proxy靜態類中的內部靜態類ProxyClassFactory的實例,我們看這個名字就知道(工廠類啊),最終生成的代理Class對象應該就在這個內部靜態類ProxyClassFactory中的某個方法中,沒錯,就是apply方法中。好,我們進入subKeyFactory.apply方法中看看

public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
    // 省略 ...
    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);
    try {
        return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);
    } catch (ClassFormatError e) {
        throw new IllegalArgumentException(e.toString());
    }
    // 省略 ...
}

可以看裏面ProxyGenerator.generateProxyClass這個方法嗎,只要有代理類名字(代理類的名字是apply方法中生成的,代碼被我省略掉了)和被代理類所繼承的接口(爲啥需要接口,因爲接口裏面有我們需要的方法呀),我們就可以通過ProxyGenerator.generateProxyClass方法產生二進制數據類型的Class文件ProxyGenerator這個類是sun公司提供的sun.misc包中的一個類,這裏我們就不展開了)

 

再通過defineClass0方法將此class文件通過類加載器生成Class對象放在元空間(1.8之前叫方法區),看到沒,Class對象就是這樣產生的

 

我們一般都是先寫java文件,然後編譯成class文件,最後再生成class對象及其實例但是我們生成的代理Class文件是直接通過generateProxyClass方法在內存中生成的,它沒有java文件,所以我們不知道這個代理對象到底長啥樣,這裏我們可以用反編譯將生成的class文件反編譯成java文件

 

我們寫了一個小工具類來反編譯生成BagShop的代理Class文件的java文件(工具代碼和反編譯網址貼在最後了) 


import com.yy.demo16_proxyPattern.IBagShop;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class IbagShop extends Proxy implements IBagShop {

   private static Method m1;
   private static Method m3;
   private static Method m2;
   private static Method m0;

   public IbagShop(InvocationHandler var1) throws  {
      super(var1);
   }

   public final boolean equals(Object var1) throws  {
      try {
         return ((Boolean)super.h.invoke(this, m1, new Object[]{var1})).booleanValue();
      } catch (RuntimeException | Error var3) {
         throw var3;
      } catch (Throwable var4) {
         throw new UndeclaredThrowableException(var4);
      }
   }

   public final void buyBag(String var1) throws  {
      try {
         super.h.invoke(this, m3, 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)).intValue();
      } catch (RuntimeException | Error var2) {
         throw var2;
      } catch (Throwable var3) {
         throw new UndeclaredThrowableException(var3);
      }
   }

   static {
      try {
         m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
         m3 = Class.forName("com.yy.demo16_proxyPattern.IBagShop").getMethod("buyBag", new Class[]{Class.forName("java.lang.String")});
         m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
         m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
      } catch (NoSuchMethodException var2) {
         throw new NoSuchMethodError(var2.getMessage());
      } catch (ClassNotFoundException var3) {
         throw new NoClassDefFoundError(var3.getMessage());
      }
   }
}

可以看到,這個類繼承了Proxy類,實現了IBagShop接口。我們還可以看到一個有參構造方法,參數是InvocationHandler類型,然後我們再看buyBag這個方法,裏面方法的調用語句爲:

super.h.invoke(this, m3, new Object[]{var1});

這不就是h通過invoke來調用方法嗎(對invoke不瞭解的可以補一下反射相關的知識)。那h是啥,不就是構造函數傳進來的InvocationHander嗎?我們再回到最開始的newProxyInstance方法,看下它的第3、4行代碼:

final Constructor<?> cons = cl.getConstructor(constructorParams);
return cons.newInstance(new Object[]{h});

上面第一行代碼是獲取代理Class對象的構造器,第二行代碼就是通過構造器生成代理Class對象的實例對象,即我們的動態代理對象。

 

最後總結一下生成動態代理對象的步驟

  1. sun.misc.generateProxyClass類的generateProxyClass方法根據傳入的被代理對象繼承的接口信息直接生成代理對象的Class文件

  2. defineClass0方法通過被代理對象類加載器將代理對象的Class文件加載成Class對象

  3. 我們通過反射技術獲得Class對象的有參構造器,參數是實現了InvocationHandler接口的代理類

  4. 最後通過構造器生成動態代理對象

最後的最後,我把前文提到的工具代碼和反編譯網站貼出來

 


package com.yy.demo16_proxyPattern;

import com.yy.demo1_mapStruct.User;
import sun.misc.ProxyGenerator;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * @Author: 24只羊
 * @Description:
 * @Date: 2020-01-17
 */
public class ProxyGeneratorUtil {

    private static String DEFAULT_CLASS_NAME = "$Proxy";

    public static byte [] saveGenerateProxyClass(String proxyName, Class clazz) {

        byte[] classFile = ProxyGenerator.generateProxyClass(proxyName, new Class[]{clazz});
        FileOutputStream out = null;

        try {
            String filePath = "這裏填寫自己" + proxyName + ".class";
            out = new FileOutputStream(filePath);
            out.write(classFile);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return classFile;
    }

    public static void main(String[] args) {
        Class<?> interfaces [] = {User.class};
        saveGenerateProxyClass("程序員進階之路")
    }
}

在線反編譯鏈接地址:http://javare.cn/

 

(完)

 

歡迎大家關注我的公衆號 “程序員進階之路”,裏面記錄了一個非科班程序員的成長之路

                                                        

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章