代理模式

代理模式

代理模式的定義很簡單:給某一對象提供一個代理對象,並由代理對象控制對原對象的引用

 

代理模式的結構

有些情況下,一個客戶不想活着不能夠直接引用一個對象,可以通過代理對象在客戶端和目標對象之間起到中介作用。代理模式中的角色有:

1、抽象對象角色

聲明瞭目標對象和代理對象的共同接口,這樣一來在任何可以使用目標對象的地方都可以使用代理對象

2、目標對象角色

定義了代理對象所代表的目標對象

3、代理對象角色

代理對象內部含有目標對象的引用,從而可以在任何時候操作目標對象;代理對象提供一個與目標對象相同的接口,以便可以在任何時候替代目標對象

 

靜態代理示例

假如一個接口裏面有一個方法,想在調用這個接口的前後都加一點東西,就可以使用代理模式。靜態代理是代理模式最簡單的實現,先定義一個靜態代理接口,裏面有一個print()方法,看一下:

public interface StaticHelloWorld
{
    // 定義一個接口,裏面有一個打印方法
    void print();
}

讓一個子類實現它,打印一句"Hello World"出來:

複製代碼
public class StaticHelloWorldImpl implements StaticHelloWorld
{
    public void print()
    {
        System.out.println("Hello World");
    }
}
複製代碼

給這個接口創建一個代理對象,來實現對接口實現類的代理。注意,這裏的重點是代理對象和實際對象實現的是同一個接口,因爲希望在任何時候讓代理對象替代實際對象:

複製代碼
public class StaticProxy implements StaticHelloWorld
{
    private StaticHelloWorld staticHelloWorld;
    
    public StaticProxy(StaticHelloWorld staticHelloWorldImpl)
    {
        this.staticHelloWorld = staticHelloWorldImpl;
    }
    
    public void print()
    {
        System.out.println("Before Hello World!");
        staticHelloWorld.print();
        System.out.println("After Hello World!");
    }
}
複製代碼

寫一個類去調用代理對象,在代理對象的構造函數中傳入一個實際對象即可:

複製代碼
public class StaticTestMain
{
    public static void main(String[] args)
    {
        StaticHelloWorld shw = new StaticHelloWorldImpl();
        StaticProxy sp = new StaticProxy(shw);
        sp.print();
    }
}
複製代碼

運行結果爲:

Before Hello World!
Hello World
After Hello World!

這個很明顯,就不說了。

 

靜態代理的缺點

靜態代理的特點是靜態代理的代理類是程序員創建的,在程序運行之前靜態代理的.class文件已經存在了

從靜態代理來看,看到靜態代理模式確實可以有一個代理對象來控制實際對象的引用,並通過代理對象來使用實際對象。這種模式在代理量較小的時候還可以,但是代理量一大起來,就存在着三個比較大的缺點:

1、如果想換一種代理內容,比如我在"Hello World"前後不想輸入"Before XXX"和"After XXX"了,想輸出運行前後系統當前時間,就必須新寫一個代理對象。這樣很容易造成代理對象的膨脹。

2、代理內容無法複用,也就是說"Before XXX"和"After XXX"只可以給某一個類使用,另一個類如果也想使用這個代理內容,必須自己也寫一個,同樣,造成的後果就是代理類的無限膨脹

3、接口裏面如果新增了一個方法,實際對象實現了這個方法,代理對象也必須新增內容,去給這個新增方法增加代理內容(假如需要的話)

 

利用JDK中的代理類Proxy實現動態代理的示例

由於靜態代理的侷限性,所以產生了動態代理的概念。看一下,首先還是定義一個動態代理接口:

public interface DynamicHelloWorld
{
    // 動態代理類,有一個print()方法
    String print();
}

寫一個類去實現它,打印方法打印"Enter DynamicHelloWorldImpl.print()"並返回"DynamicHelloWorldImpl":

複製代碼
public class DynamicHelloWorldImpl implements DynamicHelloWorld
{
    public String print()
    {
        System.out.println("Enter DynamicHelloWorldImpl.print()");
        
        return "DynamicHelloWorldImpl";
    }
}
複製代碼

最關鍵的一部分,動態代理類,也是不太好理解的一部分。在Java中,動態代理需要實現InvocationHandler接口。

InvocationHandler接口裏面只有一個方法invoke(),至於如何實現,完全看使用者自己的喜好,沒有固定。可以像下面這樣,把newInstance即生成一個動態代理類的過程放到InvocationHandler的實現類中:

複製代碼
public class DynamicProxy implements InvocationHandler
{
    private Object target;
    
    public Object newInstance(Object target)
    {
        this.target = target;
        
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), 
                target.getClass().getInterfaces(), this);
    }
    
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable
    {
        System.out.println("Before DynamicProxy");
        method.invoke(target, args);
        System.out.println("After DynamicProxy");
        return null;
    }
}
複製代碼

也可以像下面這樣,讓動態代理類在外面生成,只在構造函數中傳入一個target:

複製代碼
public class DynamicProxy implements InvocationHandler
{
    private Object target;
    
    public DynamicProxy(Object target)
    {
        this.target = target;
    }
    
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable
    {
        System.out.println("Before DynamicProxy");
        method.invoke(target, args);
        System.out.println("After DynamicProxy");
        return null;
    }
}
複製代碼

如果是前者的寫法,那麼main函數要這麼寫:

複製代碼
public class DynamicTestMain
{
    public static void main(String[] args) throws Exception
    {
        DynamicProxy dp = new DynamicProxy();
        DynamicHelloWorld dhwi = new DynamicHelloWorldImpl();
        DynamicHelloWorld dhw = (DynamicHelloWorld)dp.newInstance(dhwi);
        dhw.print();
    }
}
複製代碼

如果是後者的寫法,那麼main函數要這麼寫:

複製代碼
public class DynamicTestMain
{
    public static void main(String[] args) throws Exception
    {
        DynamicHelloWorld dhwi = new DynamicHelloWorldImpl();
        InvocationHandler ih = new DynamicProxy(dhwi);
        DynamicHelloWorld dhw = 
                (DynamicHelloWorld)Proxy.
                newProxyInstance(DynamicHelloWorld.class.getClassLoader(), 
                                 new Class<?>[]{DynamicHelloWorld.class}, ih);
        dhw.print();
    }
}
複製代碼

不管哪種寫法,運行結果都是一樣的:

Before DynamicProxy
Enter DynamicHelloWorldImpl.print()
After DynamicProxy

 

動態代理解析

上面兩種寫法,本質上都是一樣的。萬變不離其宗,歸納起來,實現一個動態代理可以總結爲如下四步:

1、獲取要被代理的對象,也就是實際對象

2、實現InvocationHandler接口,生成實際的代理內容

3、利用Proxy.newInstance()方法生成一個代理內容,第三個參數傳入InvocationHandler的實現類

4、代理對象調用接口內部的方法

動態代理,利用動態編譯+反射技術,把對實際對象的方法調用轉換成對傳入的InvocationHandler接口實現類的invoke方法的調用,這是動態代理模式實現的關鍵點。要證明這一點並不難,在程序中打上斷點跟一下代碼即可:

按F5執行下一步,看到程序走到了InvocationHandler實現類的invoke方法中:


執行完methd.invoke(target, args),看到控制檯打印出了"Enter DynamicHelloWorldImpl.print()"這一句語句,說明執行了實際對象DynamicHelloWorldImpl的print()方法:

這證明了我們的結論,把對於實際對象方法的調用轉換爲對於InvocationHandler實現類的invoke方法的調用

 

動態代理的優點

1、最直觀的,類少了很多

2、代理內容也就是InvocationHandler接口的實現類可以複用,可以給A接口用、也可以給B接口用,A接口用了InvocationHandler接口實現類A的代理,不想用了,可以方便地換成InvocationHandler接口實現B的代理

3、最重要的,用了動態代理,就可以在不修改原來代碼的基礎上,就在原來代碼的基礎上做操作,這就是AOP即面向切面編程

 

動態代理的缺點

動態代理有一個最大的缺點,就是它只能針對接口生成代理,不能只針對某一個類生成代理,比方說我們在調用Proxy的newProxyInstance方法的時候,第二個參數傳某個具體類的getClass(),那麼會報錯:

Exception in thread "main" java.lang.IllegalArgumentException: proxy.DynamicHelloWorldImpl is not an interface

這是因爲java.lang.reflect.Proxy的newProxyInstance方法會判斷傳入的Class是不是一個接口:

複製代碼
...
/*
  * Verify that the Class object actually represents an
  * interface.
  */
 if (!interfaceClass.isInterface()) {
 throw new IllegalArgumentException(
    interfaceClass.getName() + " is not an interface");
}
...
複製代碼

而實際使用中,我們爲某一個單獨的類實現一個代理也很正常,這種情況下,我們就可以考慮使用CGLIB(一種字節碼增強技術)來爲某一個類實現代理了。

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