Java 動態代理(Dynamic proxy) 小結

代理模式

基本概念

不管是靜態代理仍是動態代理, 其本質都是代理模式的一種實現, 那麼什麼是代理模式呢?
代理模式, 即給某一個對象提供一個代理, 並由代理對象控制對原對象的引用.
代理模式其實取材於實際生活, 例如咱們生活中常見的房屋租賃代理, 咱們在租房時, 通常不是直接和房東打交道, 而是和中間商打交道, 即中間商代理了房東, 咱們經過中間商完成與房東的間接溝通.
代理模式主要涉及三個角色:數據庫

  • Subject: 抽象角色, 聲明真實對象和代理對象的共同接口.segmentfault

  • Proxy: 代理角色, 它是真實角色的封裝, 其內部持有真實角色的引用, 而且提供了和真實角色同樣的接口, 所以程序中能夠經過代理角色來操做真實的角色, 而且還能夠附帶其餘額外的操做.數組

  • RealSubject: 真實角色, 代理角色所表明的真實對象, 是咱們最終要引用的對象.瀏覽器

這三個角色的 UML 圖以下(圖片引用自維基百科)緩存

clipboard.png

代理模式的優勢

  • 代理模式可以協調調用者和被調用者, 在必定程度上下降了系統的耦合度.網絡

  • 代理模式能夠提供更大的靈活性ide

代理模式的缺點

  • 因爲在客戶端和真實主題之間增長了代理對象, 所以有些類型的代理模式可能會形成請求的處理速度變慢this

  • 實現代理模式須要額外的工做, 有些代理模式的實現 很是複雜spa

代理模式的經常使用實現

  • 遠程代理(remote proxy): 用本地對象來表明一個遠端的對象, 對本地對象方法的調用都會做用於遠端對象. 遠程代理最多見的例子是 ATM 機, 這裏 ATM 機充當的就是本地代理對象, 而遠端對象就是銀行中的存取錢系統, 咱們經過 ATM 機來間接地和遠端系統打交道.線程

  • 虛擬代理(virtual proxy): 虛擬代理是大型對象或複雜操做的佔位符. 它經常使用的場景是實現延時加載或複雜任務的後臺執行. 例如當一個對象須要很長的時間來初始化時, 那麼能夠先建立一個虛擬代理對象, 當程序實際須要使用此對象時, 才真正地實例化它, 這樣就縮短了程序的啓動時間, 即所謂的延時加載.

  • 保護代理(protect proxy): 控制對一個對象的訪問, 能夠給不一樣的用戶提供不一樣級別的使用權限. 例如咱們能夠在代理中檢查用戶的權限, 當權限不足時, 禁止用戶調用此對象的方法.

  • 緩存代理(cache proxy): 對實際對象的調用結果進行緩存. 例如一些複雜的操做, 如數據庫讀取等, 能夠經過緩存代理將結果存儲起來, 下次再調用時, 直接返回緩存的結果.

  • 圖片代理(image proxy): 當用戶須要加載大型圖片時, 能夠經過代理對象的方法來進行處理, 即在代理對象的方法中, 先使用一個線程向客戶端瀏覽器加載一個小圖片, 而後在後臺使用另外一個線程來調用大圖片的加載方法將大圖片加載到客戶端.

關於靜態代理

爲了弄懂 Java 的動態代理, 咱們首先來了解一下靜態代理吧.
首先舉一個例子, 假設咱們須要實現一個從不一樣存儲介質(例如磁盤, 網絡, 數據庫)加載圖片的功能, 那麼使用靜態代理的方式的話, 須要實現以下工做:

  • 定義一個加載圖片的接口

  • 實現實際操做對象(LoadFromDisk, LoadFromNet, LoadFromDB)

  • 實現代理對象

根據上面的流程, 咱們實現的代碼以下:
接口:

/**
 * @author xiongyongshun
 * @version 1.0
 * @created 16/10/7
 */
public interface LoadImage {
    Image loadImage(String name);
}

代理:

public class LoadImageProxy implements LoadImage {
    private LoadImage loadImageReal;

    public LoadImageProxy(LoadImage loadImageReal) {
        this.loadImageReal = loadImageReal;
    }

    @Override
    public Image loadImage(String name) {
        return loadImageReal.loadImage(name);
    }
}

使用:

public class App {
    public static void main(String[] args) {
        LoadFromDisk loadFromDisk = new LoadFromDisk();
        LoadImageProxy proxy = new LoadImageProxy(loadFromDisk);
        proxy.loadImage("/tmp/test.png");
    }
}

根據代理模式, 咱們在上面的代碼中展現了一個基本的靜態代理的例子, LoadImageProxy 是代理類, 它會將全部的接口調用都轉發給實際的對象, 並從實際對象中獲取結果. 所以咱們在實例化 LoadImageProxy 時, 提供不一樣的實際對象時, 就能夠實現從不一樣的介質中讀取圖片的功能了.

動態代理的實現

看完了上面的靜態代理的例子, 下面咱們來進入正題吧.
那麼什麼是 Java 的動態代理呢? 其實很簡單, 顧名思義, 所謂動態代理就是 動態地建立代理而且動態地處理所代理對象的方法調用.
在 Java 的動態代理中, 涉及兩個重要的類或接口:

  • Proxy

  • InvocationHandler

關於 Proxy 類

Proxy 主要是提供了 Proxy.newProxyInstance 靜態方法, 其簽名以下:

public static Object newProxyInstance(ClassLoader loader,
                                  Class<?>[] interfaces,
                                  InvocationHandler h)
    throws IllegalArgumentException

此靜態方法須要三個參數:

  • loader: 即類加載器, 指定由哪一個ClassLoader對象來對生成的代理對象進行加載

  • interfaces: 一個Interface對象的數組, 表示的是代理對象所須要實現的接口.

  • h: 即 InvocationHandler 的實現對象. 當調用代理對象的接口時, 實際上會 經過 InvocationHandler.invkoe 將調用轉發給實際的對象.

這個靜態類會返回一個代理對象, 在程序中能夠可經過這個代理對象來對實際對象進行操做.

關於 InvocationHandler 接口

咱們在前面提到過, 在調用 Proxy.newProxyInstance 方法時, 須要傳遞一個 InvocationHandler 接口的實現對象, 那麼這個 InvocationHandler 接口有什麼用呢?
實際上, 在 Java 動態代理中, 咱們都必需要實現這個接口, 它是溝通了代理對象和實際對象的橋樑, 即:

InvocationHandler is the interface implemented by the invocation handler of a proxy instance.
Each proxy instance has an associated invocation handler. When a method is invoked on a proxy instance, the method invocation is encoded and dispatched to the invoke method of its invocation handler.

當咱們調用了代理對象所提供的接口方法時, 此方法調用會被封裝而且轉發到 InvocationHandler.invoke 方法中, 在 invoke 方法中調用實際的對象的對應方法.

InvocationHandler.invoke 方法的聲明以下:

public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable;

這個方法接收三個參數:

  • proxy: 指代理對象, 即 Proxy.newProxyInstance 所返回的對象(注意, proxy 並非實際的被代理對象)

  • method: 咱們所要調用真實對象的方法的 Method 對象

  • args: 調用真實對象某個方法時接受的參數

invoke 方法的返回值是調用的真實對象的對應方法的返回值.

動態代理例子

使用動態代理的步驟很簡單, 能夠歸納爲以下兩步:

  1. 實現 InvocationHandler 接口, 並在 invoke 中調用真實對象的對應方法.

  2. 經過 Proxy.newProxyInstance 靜態方法獲取一個代理對象.

咱們仍是以在靜態代理中展現的加載圖片的例子爲例, 首先加載圖片的接口以下:
接口:

/**
 * @author xiongyongshun
 * @version 1.0
 * @created 16/10/7
 */
public interface LoadImage {
    Image loadImage(String name);
}

接下來咱們須要實現 InvocationHandler 接口:

/**
 * @author xiongyongshun
 * @version 1.0
 * @created 16/10/7
 */
public class DynamicProxyHandler implements InvocationHandler {
    private Object proxied;

    public DynamicProxyHandler(Object proxied) {
        this.proxied = proxied;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Proxy class: " + proxy.getClass() + ", method: " + method + ", args: " + args);
        return method.invoke(proxied, args);
    }
}

能夠看到, 在實現的 invoke 方法中, 咱們簡單地經過 method.invoke(proxied, args) 來調用了真實對象的方法.

有了 InvocationHandler 後, 咱們就能夠建立代理對象並經過代理對象來操做實際對象了:

public class App {
    public static void main(String[] args) {
        // 實際對象
        LoadFromDisk loadFromDisk = new LoadFromDisk();
        // 經過 Proxy.newProxyInstance 靜態方法建立代理對象
        LoadImage loadImage = (LoadImage) Proxy.newProxyInstance(LoadImage.class.getClassLoader(), new Class[]{LoadImage.class}, new DynamicProxyHandler(loadFromDisk));

        // 經過代理對象操做實際對象.
        loadImage.loadImage("/tmp/test.png");
    }
}

爲何須要使用動態代理

看了 靜態代理動態代理, 有的朋友就會有疑惑了, 明明使用靜態代理就能夠完成的功能, 爲何還須要使用動態代理呢?
我認爲相比靜態代理, 動態代理有兩點優勢:

  • 動態代理具備更強的靈活性, 由於它不用在咱們設計實現的時候就指定某一個代理類來代理哪個被代理對象, 咱們能夠把這種指定延遲到程序運行時由JVM來實現.

  • 動態代理更爲統一與簡潔.

爲何這麼說呢? 咱們仍是以圖片加載的例子說明吧. 如今咱們假設 LoadImage 接口須要提供更多的方法, 而且咱們但願每一個方法調用都記錄 Log. 所以 LoadImage 接口更改以下:

public interface LoadImage {
    // 加載圖片
    Image loadImage(String name);
    // 加載圖片, 並翻轉圖片
    Image loadAndRotateImage(String name);
    // 獲取圖片的縮略圖
    Image loadSmallImage(String name);
}

咱們添加了兩個新的方法: loadAndRotateImage 和 loadSmallImage.
那麼在靜態代理的方法下, 咱們怎麼實現所須要的功能呢? 下面是具體的代碼:

public class LoadImageProxy implements LoadImage {
    private LoadImage loadImageReal;

    public LoadImageProxy(LoadImage loadImageReal) {
        this.loadImageReal = loadImageReal;
    }

    @Override
    public Image loadImage(String name) {
        System.out.println("Call method: loadImage, file name: " + name);
        return loadImageReal.loadImage(name);
    }

    @Override
    public Image loadAndRotateImage(String name) {
        System.out.println("Call method: loadAndRotateImage, file name: " + name);
        return loadImageReal.loadImage(name);
    }

    @Override
    public Image loadSmallImage(String name) {
        System.out.println("Call method: loadSmallImage, file name: " + name);
        return loadImageReal.loadImage(name);
    }
}

上面代碼例子中, 咱們分別實現了 loadImage, loadAndRotateImage 和 loadSmallImage 代理方法, 而且爲每一個方法都添加了 log.
做爲對比, 咱們來看一下使用靜態代理時的代碼實現吧:

public class DynamicProxyHandler implements InvocationHandler {
    private Object proxied;

    public DynamicProxyHandler(Object proxied) {
        this.proxied = proxied;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Call method: loadImage, file name: " + args[0]);
        return method.invoke(proxied, args);
    }
}

咱們看到, 在使用動態代理時, 咱們除了添加一行 log 輸出外, 沒有進行任何的更改, 而在靜態代理中, 咱們須要分別實現每一個代理方法, 而且在每一個方法中添加日誌輸出. 能夠想象, 當咱們的接口方法比較多時, 使用靜態代理就會形成了大量的代碼修改, 而且在未來咱們須要去除方法調用的 log 時, 靜態代理的方式就十分不便了, 而對於動態代理而言, 僅僅須要修改一兩行代碼而已.

本文由 yongshun 發表於我的博客, 採用 署名-相同方式共享 3.0 中國大陸許可協議.
Email: yongshun1228@gmail .com
本文標題爲: Java 動態代理(Dynamic proxy) 小結
本文連接爲: http://www.javashuo.com/article/p-grlthrhi-mc.html

相關文章
相關標籤/搜索