【設計模式】-代理模式及動態代理詳解

黑米麵包派發表於2021-04-12

代理模式

代理模式是一種結構性設計模式,讓你能夠提供物件的替代品或其佔位符。代理控制著對於原物件的訪問,並允許在將請求提交給物件前後進行一些處理。

代理模式結構

  1. 服務介面(ServiceInterface) 宣告瞭服務介面提供的功能。代理必須遵循該介面才能偽裝成物件
  2. 服務(Service)類,提供具體的一些實用的業務邏輯
  3. 代理(Proxy)類包含一個指向服務物件的引用成員變數,代理完成其交代的任務(例如延遲載入,記錄日誌,訪問控制或者快取等)後會將請求傳遞給服務物件,通常情況下,代理會對其服務物件的整個宣告週期進行管理。
  4. 客戶端(Client) 能通過同一介面與服務或與代理進行互動,所以你可以在一些需要服務物件的程式碼中實用代理。

案例分析

我們有一個常用的資料庫訪問介面,大量的客戶端都是對資料庫進行直接的訪問,對系統資源的消耗特別大,並且有很多的重複查詢操作。

直接訪問資料庫,可能會非常的慢

這時候我們考慮加入快取,當需要重複的查詢時直接從快取中獲取資料返回到客戶端,節省系統開銷,並記錄一下每一個客戶端訪問花費的時間。

代理模式建議新建一個與原服務物件介面相同的代理類, 然後更新應用以將代理物件傳遞給所有原始物件客戶端。 代理類接收到客戶端請求後會建立實際的服務物件, 並將所有工作委派給它。

代理將自己偽裝成資料庫物件,可以在客戶端不知道的情況下做快取查詢操作並記錄其訪問時間或日誌

程式碼實現

定義查詢資料庫的介面

public interface DataService {
    // 通過ID查詢資料
    String getById(Integer id);
}

具體的資料庫查詢業務類

public class DataServiceImpl implements DataService{

    // 模擬資料
    final Map<Integer,String> dataMap = new HashMap<Integer,String>(){{
        for (int i = 0; i < 10; i++) {
            put(i,"data_"+ i);
        }
    }};

    @Override
    public String getById(Integer id) {
        // 模擬資料庫查詢的耗時
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return dataMap.get(id);
    }
}

建立代理類,偽裝業務類

public class DataServiceProxy implements DataService{
    DataService dataService;
    // 快取
    Map<Integer,String> cacheMap = new HashMap<>();
    
    public DataServiceProxy(DataService dataService) {
        this.dataService = dataService;
    }

    @Override
    public String getById(Integer id) {
        // 記錄訪問的開始時間
        final long start = System.currentTimeMillis();
        String result = null;
        // 優先從快取獲取
        String cache = getCache(id);
        if (cache == null){
            result = dataService.getById(id);
            // 放入快取中
            putCache(id,result);
        }else {
            result = cache;
        }
        final long end = System.currentTimeMillis();
        System.out.println("耗時:" + (end - start) + "ms");
        return result;
    }

    // 快取資訊
    private void putCache(Integer id,String value){
        cacheMap.put(id,value);
    }
    // 獲取快取資訊
    private String getCache(Integer id){
        return cacheMap.get(id);
    }
    
}

客戶端

@Test
public void ProxyTest() {
    DataService dataService = new DataServiceImpl();
    DataServiceProxy dataServiceProxy = new DataServiceProxy(dataService);
    dataServiceProxy.getById(1);
    // 第二次查詢
    dataServiceProxy.getById(1);
    dataServiceProxy.getById(1);
}

這種代理模式的設計方式,我們一般稱之為靜態代理:由編碼人員建立完成或由特定工具生成原始碼,在編譯時就已經將介面、被代理類、代理類等確定類下來,在程式執行之前,代理類的位元組碼檔案已經生成了。如果由其他的代理內容,可能需要新建很多的程式碼來實現。

動態代理

與靜態代理最大的區別在於,動態代理類是在程式執行時建立的代理。例如在上面的例子中DataServiceProxy代理類是我們自己定義的,在程式執行之前就已經編譯完成。在動態代理中,代理類不是在程式碼中定義,而是在程式執行時根據我們的需要在Java程式碼中動態生成的。

Java中我們提到動態代理,一般繞不開JDK動態代理和CGLIB動態代理。

JDK動態代理

利用JDK自帶的代理類來完成,相當於利用一個攔截器(需實現介面InvocationHanlder)配合反射機制生成一個實現代理類的匿名介面,在呼叫具體的方法前呼叫InvocationHanlder來處理。

我們依舊使用DataService介面和DataServiceImpl業務類來完成一個動態代理的案例。

  1. 建立被代理類的介面和業務類(已經有了)
  2. 建立InvocationHanlder介面的實現類,在invoke方法中實現代理的邏輯
  3. 通過Proxy的靜態方法newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h)建立一個代理物件。
public class JDKProxy implements InvocationHandler {
    // 被代理物件
    private Object object;

    // 快取
    Map<Integer,String> cacheMap = new HashMap<>();

    public JDKProxy(Object object) {
        this.object = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 只代理其中的查詢方法
        if (method.getName().equals("getById")){
            // 引數
            Integer id = (Integer) args[0];
            // 記錄訪問的開始時間
            final long start = System.currentTimeMillis();
            String result = null;
            // 優先從快取獲取
            String cache = getCache(id);
            if (cache == null){
                // 代理執行
                result =(String) method.invoke(object,args);
                // 放入快取中
                putCache(id,result);
            }else {
                result = cache;
            }
            final long end = System.currentTimeMillis();
            System.out.println("耗時:" + (end - start) + "ms");
            return result;
        }else {
            return method.invoke(object,args);
        }
    }

    // 快取資訊
    private void putCache(Integer id,String value){
        cacheMap.put(id,value);
    }
    // 獲取快取資訊
    private String getCache(Integer id){
        return cacheMap.get(id);
    }
}

InvocationHandler介面詳解

InvocationHandler介面是proxy代理例項的呼叫處理程式實現的一個介面,每一個proxy代理例項都有一個關聯的呼叫處理程式;在代理例項呼叫方法(Method)時,方法呼叫被編碼分派到呼叫處理程式的invoke方法。

每一個動態代理類的呼叫處理程式都必須實現InvocationHandler介面,並且每個代理類的例項都關聯到了實現該介面的動態代理類呼叫處理程式中,當我們通過動態代理物件呼叫一個方法時候,這個方法的呼叫就會被轉發到實現InvocationHandler介面類的invoke方法來呼叫,看如下invoke方法:

/**
* proxy:代理類代理的真實代理物件com.sun.proxy.$Proxy0(按次序進行,每生成一個 +1)
* method:我們所要呼叫某個物件真實的方法的Method物件
* args:指代代理物件方法傳遞的引數
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

Client

客戶端在呼叫時的方式也和靜態代理不一樣,最終是使用代理類$Proxy來進行方法的呼叫

@Test
public void JDKProxyTest() {
    DataService dataService = new DataServiceImpl();
    JDKProxy jdkProxy = new JDKProxy(dataService);
    // 獲取代理物件
    DataService dataServiceProxy = (DataService) Proxy.newProxyInstance(DataService.class.getClassLoader(), new Class[]{DataService.class}, jdkProxy);
    dataServiceProxy.getById(1);
    dataServiceProxy.getById(1);
}

其執行的結果是一樣的,都完成了代理內容。

Proxy類詳解

Proxy類就是用來建立一個代理物件的類,它提供了很多方法,我們最常用的是newProxyInstance方法。

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

newProxyInstance就是建立一個代理類物件,它接收三個引數:

  • loader:指定代理類的類載入器(我們傳入當前測試類的類載入器)
  • interfaces:一個interface物件陣列,代理類需要實現的介面(我們傳入被代理類實現的介面,這樣生成的代理類和被代理類就實現了相同的介面)
  • h:一個InvocationHandler物件,表示的是當動態代理物件呼叫方法的時候會關聯到哪一個InvocationHandler物件上,用來處理方法的呼叫。這裡傳入我們自己實現的handler

CGLIB動態代理

利用asm開源包,對代理物件類的class檔案載入進來,通過修改其位元組碼生成子類來處理。

  1. 匯入cglib-xxx.jar包,這裡包含了asmcglib
  2. 建立MethodInterceptor介面的實現類,在intercept方法中實現代理的邏輯
  3. 編寫getCglibProxy方法(自定義)返回代理類物件

Pom匯入cglb

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

重寫MethodInterceptor

public class CglibProxy implements MethodInterceptor {
    // 被代理物件,便於通用,可以寫成Object
    private Object object;

    // 快取
    Map<Integer,String> cacheMap = new HashMap<>();

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // 只代理其中的查詢方法
        if (method.getName().equals("getById")) {
            // 引數
            Integer id = (Integer) args[0];
            // 記錄訪問的開始時間
            final long start = System.currentTimeMillis();
            String result = null;
            // 優先從快取獲取
            String cache = getCache(id);
            if (cache == null) {
                result = (String)method.invoke(object,args);
                // 放入快取中
                putCache(id, result);
            } else {
                result = cache;
            }
            final long end = System.currentTimeMillis();
            System.out.println("耗時:" + (end - start) + "ms");
            return result;
        } else {
            return method.invoke(object, args);
        }
    }

    // 獲取代理物件 這裡採用了範型的寫法,更直觀的傳入被代理類,然後返回代理物件
    public <T> T getCglibProxy(T t){
        this.object = t;//為目標物件target賦值
        Enhancer enhancer = new Enhancer();
        //設定父類,因為Cglib是針對指定的類生成一個子類,所以需要指定父類
        enhancer.setSuperclass(object.getClass());
        //設定回撥
        enhancer.setCallback(this);
        //建立並返回代理物件
        Object result = enhancer.create();
        return (T) result;
    }

    // 快取資訊
    private void putCache(Integer id,String value){
        cacheMap.put(id,value);
    }
    // 獲取快取資訊
    private String getCache(Integer id){
        return cacheMap.get(id);
    }
}

Client

@Test
public void CGLBProxyTest(){
    // 被代理類 這裡可以不用介面宣告哦
    DataService dataService = new DataServiceImpl();
    CglibProxy cglibProxy = new CglibProxy();
    // 獲取代理物件
    DataService proxy = cglibProxy.getCglibProxy(dataService);
    proxy.getById(1);
    proxy.getById(1);
}

可以發現兩種動態代理的寫法基本差不多,基本的思路都是生成代理類,攔截,反射,獲取真正的代理類方法,執行。那麼兩種方式有什麼區別和用法呢?

JDK代理和CGLIB代理的區別

  1. JDK動態代理只能對實現了介面的類生成代理,而不能針對類 ,使用的是 Java反射技術實現,生成類的過程比較高效。
  2. CGLIB是針對類實現代理,主要是對指定的類生成一個子類,覆蓋其中的方法 ,使用asm位元組碼框架實現,相關執行的過程比較高效,生成類的過程可以利用快取彌補,因為是繼承,所以該類或方法最好不要宣告成final
  3. JDK代理是不需要第三方庫支援,只需要JDK環境就可以進行代理
  4. CGLIB必須依賴於CGLIB的類庫,但是它需要類來實現任何介面代理的是指定的類生成一個子類,覆蓋其中的方法,是一種繼承但是針對介面程式設計的環境下推薦使用JDK的代理;

相關文章