【設計模式】第十二篇:車票購買場景中的代理模式講解

BWH_Steven發表於2021-01-27

早在 Spring AOP 篇的講解中,我已經寫過關於 AOP 部分是如何用代理模式進行一個處理的,今天相對規範的把這幾種方式來整理一下,因為代理模式相對來說程式碼複雜一點點,所以我們選擇先講解其概念,再使用程式碼具體演示

一 代理模式的概念

(一) 什麼是代理模式

定義:給某個物件提供一個代理物件,用來控制對這個物件的訪問

簡單的舉個例子就是:買火車、飛機票等,我們可以直接從車站售票視窗進行購買,這就是使用者直接在官方購買,但是我們很多地方的店鋪或者一些路邊的亭臺中都可以進行火車票的代售,使用者直接可以在代售點購票,這些地方就是代理物件

(二) 使用代理物件有什麼好處呢?

  • 功能提供的這個類(火車站售票處),可以更加專注於主要功能的實現,比如安排車次以及生產火車票等等
  • 代理類(代售點)可以在功能提供類提供方法的基礎上進行增加實現更多的一些功能

這個動態代理的優勢,帶給我們很多方便,它可以幫助我們實現無侵入式的程式碼擴充套件,也就是在不用修改原始碼的基礎上,同時增強方法

補充:缺點就是類的數量,以及複雜度,請求處理速度會相對增大

(三) 分類

代理模式分為靜態和動態代理兩大種,動態代理又能分為大致兩種,所以基本的來說有三種

  • ① 靜態代理

  • ② 動態代理

    • ① 基於介面的動態代理 —— JDK 動態代理

    • ② 基於子類的動態代理 —— Cglib 動態代理

    • ③ javassist 動態代理(這裡不做演示)

說明:

靜態:由程式設計師建立代理類或特定工具自動生成原始碼再對其編譯,在程式執行前代理類的 .class 檔案就已經存在了。

動態:在程式執行時,運用反射機制動態建立而成

二 程式碼演示

我們下面演示的背景是來自一個火車票買票的案例,這個案例即,例如買一張800塊的火車票,你可以直接在火車站(不考慮現在移動12306等購買,只是例子別較真)買,或者去一個代理點買,但是代理點要賺錢的,所以你買 800 的火車票,你就需要給代理點 1000,收200 手續費,先別管黑不黑心了,我們先看看怎麼實現

(一) 傳統實現

建立官方售票處介面

public interface RailwayTicketProducer {
    /**
     * 售票服務
     * @param price
     */
    void saleTicket(float price);
}

下面自然就是實現類

public class RailwayTicketProducerImpl implements RailwayTicketProducer {
    public void saleTicket(float price) {
        System.out.println("代理銷售火車票(扣20%手續費),【官方車站】收到車票錢:" + price);
    }
}

直接呼叫

public class Test {
    public static void main(String[] args) {
        RailwayTicketProducer ticketProducer = new RailwayTicketProducerImpl();
        ticketProducer.saleTicket(1000);
    }
}

(二) 靜態代理

前面的官方售票處介面和實現類還是一樣的,但是代理銷售點和官方購票點不一樣,其多了一個額外的處理,那就是收 20 % 手續費(真黑啊),所以意味著代理售票點的 售票方法 saleTicket 被增強了

所以我們首先建立一個 代理售票點 ProxyRailwayTicketProducerImpl 類,然後實現官方售票點的介面 RailwayTicketProducer,通過組合引入的方式,將 RailwayTicketProducerImpl 引入,然後在它的 saleTicket 方法中就可以進行額外的業務書寫或者說方法增強內容了

比如這裡多了一個 price * 0.8f 的簡單處理,這樣就扣掉了兩成的費用

public class ProxyRailwayTicketProducerImpl implements RailwayTicketProducer {

    private RailwayTicketProducerImpl ticketProducer;

    public ProxyRailwayTicketProducerImpl() {
    }

    public ProxyRailwayTicketProducerImpl(RailwayTicketProducerImpl ticketProducer) {
        this.ticketProducer = ticketProducer;
    }

    @Override
    public void saleTicket(float price) {
        ticketProducer.saleTicket(price * 0.8f);
    }
}

測試一下,先 new 一個真實的處理物件,然後建立代理物件,將真實物件傳入,再通過代理物件呼叫 saleTicket ,這樣真正呼叫 saleTicket 的還是原先的 RailwayTicketProducerImpl ,但是被增強的部分也同樣被執行了,例如那個 乘以 0.8 的操作

public class Test {
	RailwayTicketProducerImpl ticketProducer = new RailwayTicketProducerImpl();
    // 代理物件
    ProxyRailwayTicketProducerImpl proxy = new ProxyRailwayTicketProducerImpl(ticketProducer);
        proxy.saleTicket(1000);
    }
}

測試結果:

代理銷售火車票(扣20%手續費),【官方車站】收到車票錢:800.0

(三) 動態代理(基於介面-jdk)

和前面的傳統方式隔著有點遠了,這裡寫完整一下

建立官方售票處(類和介面)

RailwayTicketProducer 介面

public interface RailwayTicketProducer {
    /**
     * 售票服務
     * @param price
     */
    void saleTicket(float price);
}

RailwayTicketProducerImpl 類

實現類中,我們後面只對銷售車票方法進行了增強,售後服務並沒有涉及到

/**
 * 生產廠家具體實現
 */
public class RailwayTicketProducerImpl implements RailwayTicketProducer{

    public void saleTicket(float price) {
        System.out.println("代理銷售火車票(扣20%手續費),【官方車站】收到車票錢:" + price);
    }
}

Client 類

這個類,就是客戶類,在其中,通過代理物件,實現購票的需求

首先先來說一下如何建立一個代理物件:答案是 Proxy類中的 newProxyInstance 方法

注意:既然叫做基於介面的動態代理,這就是說被代理的類,也就是文中官方銷售車票的類最少必須實現一個介面,這是必要的!

public class Client {

    public static void main(String[] args) {
        RailwayTicketProducer producer = new RailwayTicketProducerImpl();

        //動態代理
        RailwayTicketProducer proxyProduce = (RailwayTicketProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
                producer.getClass().getInterfaces(),new MyInvocationHandler(producer));

        //客戶通過代理買票
        proxyProduce.saleTicket(1000f);
    }
}

newProxyInstance 共有三個引數 來解釋一下:

  • ClassLoader:類載入器

    • 用於載入代理物件位元組碼,和被代理物件使用相同的類載入器
  • Class[]:位元組碼陣列

    • 為了使被代理物件和的代理物件具有相同的方法,實現相同的介面,可看做固定寫法
  • InvocationHandler:如何代理,也就是想要增強的方式

    • 也就是說,我們主需要 new 出 InvocationHandler,然後書寫其實現類,是否寫成匿名內部類可以自己選擇

    • 如上述程式碼中 new MyInvocationHandler(producer) 例項化的是我自己編寫的一個 MyInvocationHandler類,實際上可以在那裡直接 new 出 InvocationHandler,然後重寫其方法,其本質也是通過實現 InvocationHandler 的 invoke 方法實現增強

MyInvocationHandler 類

這個 invoke 方法具有攔截的功能,被代理物件的任何方法被執行,都會經過 invoke

public class MyInvocationHandler implements InvocationHandler {

    private  Object implObject ;

    public MyInvocationHandler (Object implObject){
        this.implObject=implObject;
    }

    /**
     * 作用:執行被代理物件的任何介面方法都會經過該方法
     * 方法引數的含義
     * @param proxy   代理物件的引用
     * @param method  當前執行的方法
     * @param args    當前執行方法所需的引數
     * @return        和被代理物件方法有相同的返回值
     * @throws Throwable
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object returnValue = null;
        //獲取方法執行的引數
        Float price = (Float)args[0];
        //判斷是不是指定方法(以售票為例)
        if ("saleTicket".equals(method.getName())){
            returnValue = method.invoke(implObject,price*0.8f);
        }
        return returnValue;
    }
}

在此處,我們獲取到客戶購票的金額,由於我們使用了代理方進行購票,所以代理方會收取一定的手續費,所以使用者提交了 1000 元,實際上官方收到的只有800元,這也就是這種代理的實現方式,

執行結果:

代理銷售火車票(扣20%手續費),【官方車站】收到車票錢:800.0

(四) 動態代理(基於子類-cglib)

上面方法簡單的實現起來也不是很難,但是唯一的標準就是,被代理物件必須提供一個介面,而現在所講解的這一種就是一種可以直接代理普通 Java 類的方式,同時在演示的時候,我會將代理方法直接以內部類的形式寫出,就不單獨建立類了,方便大家與上面對照

增加 cglib 依賴座標

<dependencies>
	<dependency>
		<groupId>cglib</groupId>
		<artifactId>cglib</artifactId>
        <version>3.2.4</version>
    </dependency>
</dependencies>

TicketProducer 類

注意:這裡只是一個普通類了

/**
 * 生產廠家
 */
public class TicketProducer {

    public void saleTicket(float price) {
         System.out.println("代理銷售火車票(扣20%手續費),【官方車站】收到車票錢:" + price);
    }
}

Enhancer 類中的 create 方法就是用來建立代理物件的

而 create 方法又有兩個引數

  • Class :位元組碼
    • 指定被代理物件的位元組碼
  • Callback:提供增強的方法
    • 與前面 invoke 作用是基本一致的
    • 一般寫的都是該介面的子介面實現類:MethodInterceptor
public class Client {

    public static void main(String[] args) {
        // 由於下方匿名內部類,需要在此處用final修飾
        final TicketProducer ticketProducer = new TicketProducer();

        TicketProducer cglibProducer =(TicketProducer) Enhancer.create(ticketProducer.getClass(), new MethodInterceptor() {

            /**
             * 前三個三個引數和基於介面的動態代理中invoke方法的引數是一樣的
             * @param o
             * @param method
             * @param objects
             * @param methodProxy   當前執行方法的代理物件
             * @return
             * @throws Throwable
             */
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                Object returnValue = null;
                //獲取方法執行的引數
                Float price = (Float)objects[0];
                //判斷是不是指定方法(以售票為例)
                if ("saleTicket".equals(method.getName())){
                    returnValue = method.invoke(ticketProducer,price*0.8f);
                }
                return returnValue;
            }
        });
        cglibProducer.saleTicket(1000f);
    }
}

執行結果:

代理銷售火車票(扣20%手續費),【官方車站】收到車票錢:800.0

三 結構圖

  • 抽象主題(Subject)類:通過介面或抽象類宣告真實主題和代理物件實現的業務方法

  • 真實主題(Real Subject)類:代理物件所代表的真實物件,是最終要引用的物件,其實現了抽象主題中的具體業務

  • 代理(Proxy)類:提供了與真實主題相同的介面,其內部含有對真實主題的引用,它可以訪問、控制或擴充套件真實主題的功能

相關文章