代理模式——用法與動態代理底層詳解

代理模式作爲框架中經常使用的設計模式,它對代碼的拓展性提升可以說是相當高,從aop的實現,到單元測試,再到Mybatis的Mapper接口實現類,都有代理模式的影子。

本篇主要講解代理模式的使用,動態代理的功能和JVM底層如何處理動態代理。

目錄

一、靜態代理的實現

1.實現一個房東

2.實現黑中介

3.租房試一試

4.靜態代理如何玩出花來?

二、動態代理的實現

1.黑中介生成器(newProxyInstance方法)詳解

2.InvocationHandler接口詳解

3.使用動態代理

4.程序分析和整理

三、動態代理原理分析


 

一、靜態代理的實現

代理是什麼意思呢,舉個例子,我是房東,我有一棟房子急租,但是我很忙,怎麼辦呢?當然是找黑……中介啦。

客戶如果需要租房子,那麼就去找黑中介就行了,這個中介也不一定只會做房產中介,也可以做些別的,反正就是不務正業的那種。

1、實現一個房東

房東的功能相當簡單,只需要租售房子(rentHouse())就行了。

public interface Rent {

    void rentSth();
}


public class Landlord implements Rent{
    //此處實現Rent接口,之後就可以對Rent接口進行拓展,比如租車,或者py交易也行
    @Override
    public void rentSth() {
        rentHouse();
    }

    public void rentHouse(){
        System.out.println("租售房子");
    }
}

2.實現黑中介

黑中介只需要幫助房東租房子即可,但是爲了方便和客戶對接,因此,黑中介也需要繼承Rent接口

public class Intermediary implements Rent{

    //因爲中介本身是沒有房子的,因此,需要有一個房東的存在,讓黑中介能幹活
    private Rent rent;
    //如果對靜態代理理解比較深的話,這個代理的對象可以有很多個,根據業務場景的需要,用代理的不同對象處理不同的事情。

    public Intermediary(Rent rent) {
        this.rent = rent;
    }

    @Override
    public void rentSth() {
        rent.rentSth();
    }

    public void getMoney(int money){
        System.out.println("給房東"+money*0.1+"元");
        System.out.println("獲利"+money*0.9+"元");
    }
}

3.租房試一試

public class Tenant {
    public static void main(String[] args) {
        //找黑中介租房子
        Intermediary intermediary = new Intermediary(new Landlord());
        intermediary.rentSth();
        intermediary.getMoney(3000);
    }
}

運行結果如下:

4.靜態代理如何玩出花來?

雖然看了靜態代理之後,覺得實現起來好像沒啥用?但是實際上,靜態代理已經可以做很多事情了,比如,可以用靜態代理,讓代理的對象的方法按照你設定的順序進行執行(參考Spring AOP的功能),玩法很多。

但是靜態代理有一個致命的缺點,當然,這也是所有靜態代碼都有的缺點:不夠靈活,代理的對象必須要預先編譯好

 

二、動態代理的實現

動態代理的在功能上基本上和靜態代理相差無幾,但是它是基於反射包下的產物,所謂反射,也就是程序在運行的過程中,使JVM加載其他class文件並且操作的一個過程。

基於反射的代理模式就解決了靜態代理不夠靈活的一個缺點,下面我們看下動態代理的使用方法。

1.黑中介生成器(newProxyInstance方法)詳解

 

這裏依舊拿上面的代碼爲例子,房東和Rent接口都是必須要的,而黑中介我們就不需要了,動態代理會替我們在程序運行的時候,生成一個黑中介。

Proxy.newProxyInstance();//Proxy(代理的意思)這個類提供的靜態方法可以產生一個黑中介



//詳細看一下這個方法需要的參數:
public static Object newProxyInstance(
ClassLoader loader,           
Class<?>[] interfaces,        
InvocationHandler h)
        throws IllegalArgumentException

 

  • ClassLoader loader:該參數是被代理對象的類加載器,如果是我們寫的普通類,使用applicationClassLoader就可以了,也就是ClassLoader.getSystemClassLoader(),這個加載器也是默認加載器;有興趣的可以瞭解一下Java類加載機制(雙親委派模型)。

  • Class<?>[] interfaces:該參數是被代理對象的class對象,因爲可以代理多個類,因此用數組傳值。

  • InvocationHandler h:雖然反射生成的黑中介可以代理房東的所有行爲,但是程序員得告訴黑中介,應該怎麼做。InvocationHandler是黑中介的行爲處理器。(此處括號裏的內容可以忽略。實際上,動態代理生成的對象,本質上重寫了該對象的所有方法,使其所有方法中都只有一個功能,調用InvocationHandler對象的invoke方法。)

2.InvocationHandler接口詳解

該接口只有一個invoke方法,這個方法我們需要實現它,才能代理我們的對象。

因爲之後我們調用任何黑中介的方法,黑中介都會調用invoke方法。

public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
  • Object proxy:這個對象是黑中介自己,儘量不要去使用它,因爲它的所有方法都是調用invoke方法,調用它的任何方法都會進入死遞歸調用。
  • Method method:因爲不論你調用黑中介的哪個方法,最後都會調用invoke方法,因此,需要依靠Method對象來識別到底調用了什麼方法;這個method對象也就是被調用的方法的實例對象。
  • Object[] args:這個是被黑中介調用方法的參數;
  • invoke方法的返回值就是黑中介被調用的方法的返回值。

 

3.使用動態代理

在解釋完黑中介生成器之後,現在可以開始自己動手試一試了。

public class Tenant {
    public static void main(String[] args) {
        Rent rent = (Rent) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Rent.class}, new InvocationHandler() {
            //代理房東
            Rent rent = new Landlord();
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                rent.rentSth();
                System.out.println("我是黑中介,我被抓了");
                return null;
            }
        });
        rent.rentSth();
    }
}

運行結果:

4.程序分析和整理

現在我們的黑中介已經如魚得水了,但是還是存在問題:

因爲調用黑中介的任何方法都會調用invoke方法,因此,如果想代理多個方法的話,就很麻煩了,比如現在調用黑中介的toString方法,他依舊會執行invoke方法,給客戶推銷房子。

所以,修改程序:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       String menthodName = method.getName();
        //根據方法的名字選擇做什麼事情。
       if(menthodName.equals("rentSth")){
             method.invoke(rent,null);
             System.out.println("我是黑中介,我被抓了");
             return null;
       }
       return null;
}

 

 

三、動態代理原理分析

從二中我們可以發現,整個動態代理對我們來說幾乎都是透明的,唯一的黑匣子便是newProxyInstance方法。

不過Proxy既然是位於reflet包下,那麼它的實現必定離不開反射技術。

事實上在最後,它會調用sun.misc.ProxyGenerator.generateProxyClass()方法來完成生成字節碼的動作,並且在運行時產生一個描述代理類的字節碼byte[]數組。

我們可以在main()方法中添加下面的代碼,將這個類文件寫入到本地。

        byte[] bytes = ProxyGenerator.generateProxyClass("$Proxy0", new Class[]{Rent.class});
        try(
                FileOutputStream fos =new FileOutputStream(new File("E:\\$Proxy0.class"))
        ){
            fos.write(bytes);
            fos.flush();
        }catch (Exception e){
            e.printStackTrace();
        }

再次運行main方法後,在E盤下會多出一個字節碼$Proxy0文件

通過反編譯可以得出下面的代碼(爲了方便觀看,我去掉了trycatch代碼塊):

點擊此處獲取反編譯工具jad

public final class $Proxy extends Proxy
    implements Rent
{

    public $Proxy(InvocationHandler invocationhandler)
    {
        super(invocationhandler);
    }

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

    public final String toString()
    {
        
        return (String)super.h.invoke(this, m2, null);
    }

    public final void rentSth()
    {
        super.h.invoke(this, m3, null);
    }

    public final int hashCode()
    {
        return ((Integer)super.h.invoke(this, m0, null)).intValue();
    }

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

    static 
    {
        m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] {
                Class.forName("java.lang.Object")
            });
        m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
        m3 = Class.forName("proxy.Rent").getMethod("rentSth", new Class[0]);
        m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
      
    }
}

不難發現實際上動態生成的對象在調用任何方法的時候,都會交給成員對象InvocationHandler的invoke方法去處理,動態代理也就不那麼神祕了。

 

 

jad工具使用方法:

在命令行下輸入:

jad -o -r -s java -d 生成Java文件路徑位置 字節碼文件位置/類名.class

例如:jad -o -r -s java -d src *.class

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