1. 程式人生 > 實用技巧 >Java中幾種常見的設計模式--代理設計模式

Java中幾種常見的設計模式--代理設計模式

一、什麼是代理模式

代理模式是一種設計模式,是在不修改原始碼下,實現目標物件的功能擴充套件。

舉個例子,有個歌手物件Singer,然後這個物件有個方法sing()。

public class Singer {
    public void sing(){
        System.out.println("演唱一首歌曲");
    }
}

假如你希望,通過你的某種方式生產出來的歌手物件,在唱歌前後還要想觀眾問好和答謝,也即對目標物件Singer的sing方法進行功能擴充套件。

 public void sing(){
        System.out.println("向觀眾們問好");
        System.out.println(
"演唱一首歌曲"); System.out.println("歌手答謝"); } }

但是往往你又不能直接對原始碼進行修改,可能是你希望原來的物件還保持原來的樣子,又或許你提供的只是一個可插拔的外掛,甚至你有可能都不知道你要對哪個目標物件進行擴充套件。這時就需要用到java的代理模式了。

代理模式有三種,靜態代理模式、動態代理模式、Cglib代理模式,下面我們分別用這三種模式來實現我們想要的功能。

二、靜態代理模式

在使用靜態代理模式時,我們要首先定義介面或父類,讓代理物件和被代理物件一起實現相同的介面或相同的父類。我們建立的介面如下:

/**
 * 代理類和被代理類共同介面
 
*/ public interface ISinger { void sing(); }
/**
 * 介面實現
 * 目標物件
 */
public class Singer implements ISinger{
    @Override
    public void sing(){
        System.out.println("演唱一首歌曲");
    }
}
/**
 * 代理物件和物件物件實現相同的介面
 *
 */
public class SingerProxy implements  ISinger {
    private ISinger singer;
    
//注入目標物件,以便實現sing()方法 public SingerProxy(ISinger singer){ this.singer=singer; } //對sing()方法進行拓展 @Override public void sing() { System.out.println("向觀眾們問好"); singer.sing(); System.out.println("歌手答謝"); } }

測試程式碼如下:

 @Test
    public void sing() {
        //建立目標物件
        ISinger singer=new Singer();
        //建立代理物件
        ISinger proxySinger=new SingerProxy(singer);
        //執行代理拓展的方法
        proxySinger.sing();

    }

結果如下:

向觀眾們問好
演唱一首歌曲
歌手答謝

這種靜態代理模式的優缺點

優點:簡單直觀。

缺點:代理程式碼必須提前寫出,需要對代理程式碼進行維護。如果介面層發生了變化,也需要修改代理程式碼。

如果能在執行時動態地寫出代理物件,不但減少了一大批代理類的程式碼,也少了不斷維護的煩惱,不過執行時的效率必定受到影響。這種方式就是接下來的動態代理。

三、動態代理模式

動態代理模式有以下特點

  1. 代理物件,不需要實現介面
  2. 代理物件的生成,是利用JDK的API,動態的在記憶體中構建代理物件(需要我們指定建立代理物件/目標物件實現的介面的型別)
  3. 動態代理也叫做:JDK代理,介面代理

代理類所在包:java.lang.reflect.Proxy
JDK實現代理只需要使用newProxyInstance方法,但是該方法需要接收三個引數,完整的寫法是:

static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h )
  • ClassLoader loader:指定目標物件的類載入器,獲取類載入器的方法固定;
  • Class<?>[] interfaces:指定目標物件的介面型別,使用泛型的方式獲取型別;
  • InvocationHandler h:事件處理介面,執行目標物件方法時會觸發事件處理器的方法,會把當前執行目標物件的方法作為引數傳入。

我們可以將建立一個代理工廠類

public class ProxyFactory {
    //建立一個目標物件
    Object target;
    public ProxyFactory(Object target){
        this.target=target;
    }

    //給目標物件生成代理物件
    public Object getProxyInstance(){
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("向觀眾們問好");
                Object returnResult=method.invoke(target,args);
                System.out.println("歌手答謝");
                return returnResult;
            }
        });
    }
}

測試方法如下:

  @Test
    public void TestProxy(){
        //建立目標物件
        ISinger singer=new Singer();
        System.out.println(singer.getClass());
        //建立代理物件
        ISinger proxySinger=(ISinger) new ProxyFactory(singer).getProxyInstance();
        System.out.println(proxySinger.getClass());
        //執行代理方法
        proxySinger.sing();
    }

結果如下:

class com.zc.core.Singer
class com.sun.proxy.$Proxy4
向觀眾們問好
演唱一首歌曲
歌手答謝

總結:以上程式碼只有標黃的部分是需要自己寫出,其餘部分全都是固定程式碼。由於java封裝了newProxyInstance這個方法的實現細節,所以使用起來才能這麼方便,具體的底層原理將會在下一小節說明。

缺點:可以看出靜態代理和JDK代理有一個共同的缺點,就是目標物件必須實現一個或多個介面,加入沒有,則可以使用Cglib代理。

四、Cglib代理模式

Cglib是一個強大的高效能的程式碼生成包,它可以在執行期擴充套件java類與實現java介面.它廣泛的被許多AOP的框架使用,例如Spring AOP和synaop,為他們提供方法的interception(攔截)。Cglib包的底層是通過使用一個小而塊的位元組碼處理框架ASM來轉換位元組碼並生成新的類.不鼓勵直接使用ASM,因為它要求你必須對JVM內部結構包括class檔案的格式和指令集都很熟悉。

使用Cglib的前提條件:

  1. 需要引入cglib的jar檔案,由於Spring的核心包中已經包括了Cglib功能,所以也可以直接引入spring-core-3.2.5.jar
  2. 引入功能包後,就可以在記憶體中動態構建子類
  3. 代理的類不能為final,否則報錯
  4. 目標物件的方法如果為final/static,那麼就不會被攔截,即不會執行目標物件額外的業務方法。

具體操作如下:

/**
 * 目標物件未實現任何介面
 */
public class Singer{
    public void sing(){
        System.out.println("演唱一首歌曲");
    }
}
/**
 *Cglib子類代理工廠
 */
public class ProxyFactory implements MethodInterceptor {
    //建立目標物件
    private Object target;
    public ProxyFactory(Object target){
        this.target=target;
    }
    //給目標物件建立一個代理物件
    public Object getProxyInstance(){
        //建立一個工具類
        Enhancer enhancer=new Enhancer();
        //設定父類
        enhancer.setSuperclass(target.getClass());

        //設定回撥函式
        enhancer.setCallback(this);
        //建立子類(代理物件)
        return enhancer.create();
    }
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("向觀眾們問好");
        //執行目標物件方法
        Object returnResult=method.invoke(target,objects);
        System.out.println("歌手答謝");
        return returnResult;
    }
}

在代理工廠中只有標黃的地方是需要我們修改的,其他部分都是不變的。

測試方法如下:

public class Test {
    public static void main(String[] args){
        //建立目標物件
        Singer singer=new Singer();
        //建立代理物件
        Singer proxySinger=(Singer) new ProxyFactory(singer).getProxyInstance();
        //執行代理方法
        proxySinger.sing();
    }
}

測試結果:

向觀眾們問好
演唱一首歌曲
歌手答謝

總結:三種代理模式各有優缺點和相應的適用範圍,主要看目標物件是否實現了介面。以Spring框架所選擇的代理模式舉例

在Spring的AOP程式設計中:
如果加入容器的目標物件有實現介面,用JDK代理
如果目標物件沒有實現介面,用Cglib代理