JAVA設計模式之動態代理

代理:設計模式

代理是一種常用的設計模式,其目的就是爲其他對象提供一個代理以控制對某個對象的訪問。代理類負責爲委託類預處理消息,過濾消息並轉發消息,以及進行消息被委託類執行後的後續處理。

圖 1. 代理模式

爲了保持行爲的一致性,代理類和委託類通常會實現相同的接口,所以在訪問者看來兩者沒有絲毫的區別。通過代理類這中間一層,能有效控制對委託類對象的直接訪問,也可以很好地隱藏和保護委託類對象,同時也爲實施不同控制策略預留了空間,從而在設計上獲得了更大的靈活性。Java 動態代理機制以巧妙的方式近乎完美地實踐了代理模式的設計理念。


一、創建被代理的類以及接口

創建接口:

package com.nudt.design.proxy;

/**
 * Created by jeffrey on 15-12-2.
 */
public interface Moveable {
    public void move();
}

創建被代理類:

package com.nudt.design.proxy;

import java.util.Random;

/**
 * Created by jeffrey on 15-12-2.
 */
public class Car implements Moveable{
    @Override
    public void move() {
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //System.out.println("");
}

二、創建一個實現InvocationHandler接口的類,並且實現該接口的invoke方法。java.lang.reflect.InvocationHandler:這是調用處理器接口,它自定義了一個 invoke 方法,用於集中處理在動態代理類對象上的方法調用,通常在該方法中實現對委託類的代理訪問。

清單 2. InvocationHandler 的核心方法
// 該方法負責集中處理動態代理類上的所有方法調用。第一個參數既是代理類實例,第二個參數是被調用的方法對象
// 第三個方法是調用參數。調用處理器根據這三個參數進行預處理或分派到委託類實例上發射執行
Object invoke(Object proxy, Method method, Object[] args)

每次生成動態代理類對象時都需要指定一個實現了該接口的調用處理器對象(參見 Proxy 靜態方法 4 的第三個參數)。
package com.nudt.design.jdkProxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * Created by jeffrey on 15-12-3.
 */
public class TimeHandler implements InvocationHandler {
    private Object target;

    public TimeHandler(Object target) {
        this.target = target;
    }

    /*
    * 參數:
    * o:被代理對象
    * method:被代理對象的方法
    * objects:被代理對象方法的參數
    * 返回:
    * object對象,即object方法method的返回值
    * */
    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {

        long startTime = System.currentTimeMillis();
        method.invoke(target);
        long endTime = System.currentTimeMillis();
        System.out.println("汽車結束行駛,用時"+(endTime - startTime)+"毫秒");

        return null;
    }
}


三、在測試主函數裏面,調用Proxy的靜態方法,創建一個代理實例,該代理實例也是一個實現了被代理類所實現接口的對象。java.lang.reflect.Proxy:這是 Java 動態代理機制的主類,它提供了一組靜態方法來爲一組接口動態地生成代理類及其對象。

清單 1. Proxy 的靜態方法
// 方法 1: 該方法用於獲取指定代理對象所關聯的調用處理器
static InvocationHandler getInvocationHandler(Object proxy) 

// 方法 2:該方法用於獲取關聯於指定類裝載器和一組接口的動態代理類的類對象
static Class getProxyClass(ClassLoader loader, Class[] interfaces) 

// 方法 3:該方法用於判斷指定類對象是否是一個動態代理類
static boolean isProxyClass(Class cl) 

// 方法 4:該方法用於爲指定類裝載器、一組接口及調用處理器生成動態代理類實例
static Object newProxyInstance(ClassLoader loader, Class[] interfaces, 
    InvocationHandler h)

四、通過代理實例來調用被代理對象的方法

三、四步代碼如下:

package com.nudt.design.jdkProxy;

import com.nudt.design.proxy.Car;
import com.nudt.design.proxy.Moveable;

import java.lang.reflect.Proxy;

/**
 * Created by jeffrey on 15-12-3.
 */
public class test {
    public static void main(String[] args) {
        //定義一個被代理類
        Car car = new Car();
        //獲取被代理類的類名
        Class<?> cls = car.getClass();
        //創建一個動態代理事件處理器
        TimeHandler h = new TimeHandler(car);
        //創建一個代理m
        /*參數:
        * 類加載器
        * 實現的接口列表
        * 事件處理器
        * 返回值:
        * 動態產生的代理對象(也實現了跟被代理對象一樣的接口)
        * */
        Moveable m = (Moveable)Proxy.newProxyInstance(cls.getClassLoader(),cls.getInterfaces(),h);
        //運行代理
        m.move();
    }
}

其中java.lang.ClassLoader:這是類裝載器類,負責將類的字節碼裝載到 Java 虛擬機(JVM)中併爲其定義類對象,然後該類才能被使用。Proxy 靜態方法生成動態代理類同樣需要通過類裝載器來進行裝載才能使用,它與普通類的唯一區別就是其字節碼是由 JVM 在運行時動態生成的而非預存在於任何一個 .class 文件中。


接下來讓我們來了解一下 Java 動態代理機制的一些特點。

首先是動態生成的代理類本身的一些特點。1)包:如果所代理的接口都是 public 的,那麼它將被定義在頂層包(即包路徑爲空),如果所代理的接口中有非 public 的接口(因爲接口不能被定義爲 protect 或 private,所以除 public 之外就是默認的 package 訪問級別),那麼它將被定義在該接口所在包(假設代理了 com.ibm.developerworks 包中的某非 public 接口 A,那麼新生成的代理類所在的包就是 com.ibm.developerworks),這樣設計的目的是爲了最大程度的保證動態代理類不會因爲包管理的問題而無法被成功定義並訪問;2)類修飾符:該代理類具有 final 和 public 修飾符,意味着它可以被所有的類訪問,但是不能被再度繼承;3)類名:格式是“$ProxyN”,其中 N 是一個逐一遞增的阿拉伯數字,代表 Proxy 類第 N 次生成的動態代理類,值得注意的一點是,並不是每次調用 Proxy 的靜態方法創建動態代理類都會使得 N 值增加,原因是如果對同一組接口(包括接口排列的順序相同)試圖重複創建動態代理類,它會很聰明地返回先前已經創建好的代理類的類對象,而不會再嘗試去創建一個全新的代理類,這樣可以節省不必要的代碼重複生成,提高了代理類的創建效率。4)類繼承關係:該類的繼承關係如圖:

圖 2. 動態代理類的繼承圖

由圖可見,Proxy 類是它的父類,這個規則適用於所有由 Proxy 創建的動態代理類。而且該類還實現了其所代理的一組接口,這就是爲什麼它能夠被安全地類型轉換到其所代理的某接口的根本原因。

接下來讓我們瞭解一下代理類實例的一些特點。每個實例都會關聯一個調用處理器對象,可以通過 Proxy 提供的靜態方法 getInvocationHandler 去獲得代理類實例的調用處理器對象。在代理類實例上調用其代理的接口中所聲明的方法時,這些方法最終都會由調用處理器的 invoke 方法執行,此外,值得注意的是,代理類的根類 java.lang.Object 中有三個方法也同樣會被分派到調用處理器的 invoke 方法執行,它們是 hashCode,equals 和 toString,可能的原因有:一是因爲這些方法爲 public 且非 final 類型,能夠被代理類覆蓋;二是因爲這些方法往往呈現出一個類的某種特徵屬性,具有一定的區分度,所以爲了保證代理類與委託類對外的一致性,這三個方法也應該被分派到委託類執行。當代理的一組接口有重複聲明的方法且該方法被調用時,代理類總是從排在最前面的接口中獲取方法對象並分派給調用處理器,而無論代理類實例是否正在以該接口(或繼承於該接口的某子接口)的形式被外部引用,因爲在代理類內部無法區分其當前的被引用類型。

接着來了解一下被代理的一組接口有哪些特點。首先,要注意不能有重複的接口,以避免動態代理類代碼生成時的編譯錯誤。其次,這些接口對於類裝載器必須可見,否則類裝載器將無法鏈接它們,將會導致類定義失敗。再次,需被代理的所有非 public 的接口必須在同一個包中,否則代理類生成也會失敗。最後,接口的數目不能超過 65535,這是 JVM 設定的限制。

最後再來了解一下異常處理方面的特點。從調用處理器接口聲明的方法中可以看到理論上它能夠拋出任何類型的異常,因爲所有的異常都繼承於 Throwable 接口,但事實是否如此呢?答案是否定的,原因是我們必須遵守一個繼承原則:即子類覆蓋父類或實現父接口的方法時,拋出的異常必須在原方法支持的異常列表之內。所以雖然調用處理器理論上講能夠,但實際上往往受限制,除非父接口中的方法支持拋 Throwable 異常。那麼如果在 invoke 方法中的確產生了接口方法聲明中不支持的異常,那將如何呢?放心,Java 動態代理類已經爲我們設計好了解決方法:它將會拋出 UndeclaredThrowableException 異常。這個異常是一個 RuntimeException 類型,所以不會引起編譯錯誤。通過該異常的 getCause 方法,還可以獲得原來那個不受支持的異常對象,以便於錯誤診斷。



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