設計模式學習筆記(七)代理模式以及動態代理的實現

歸斯君發表於2022-03-28

代理模式(Proxy Design Pattern)是為一個物件提供一個替身,以控制對這個物件的訪問。即通過代理物件訪問目標物件。被代理的物件可以是遠端物件、建立開銷大的物件或需要安全控制的物件。

一、代理模式介紹

在結束建立型模式的講解後,從這一篇開始就進入到了結構型模式,結構型模式主要是總結一些類和或物件組合在一起的結構。代理模式在不改變原始代理類的情況下,通過引入代理類來給原始類附加功能。

代理模式的主要結構如下:

  1. Subject:抽象主題類,通過介面或抽象類宣告主題和代理物件實現的業務方法
  2. RealSubject:真實主題類,實現Subject中的具體業務,是代理物件所代表的真實物件
  3. Proxy:代理類,其內部含有對真實主題的引用,它可以訪問、控制或擴充套件RealSubject的功能
  4. Client:客戶端,通過使用代理類來訪問真實的主題類

按照上面的類圖,可以實現如下程式碼:

//主題類介面
public interface Subject {
    void Request();
}

//真實的主題類
public class RealSubject implements Subject{

    @Override
    public void Request() {
        System.out.println("我是真實的主題類");
    }
}

//代理類
public class Proxy implements Subject{

    private RealSubject realSubject;

    @Override
    public void Request() {
        if (realSubject == null) {
            realSubject = new RealSubject();
        }
        realSubject.Request();
    }
}

//客戶端
public class Client {
    public static void main(String[] args) {
        Proxy proxy = new Proxy();
        proxy.Request();
    }
}

代理模式有比較廣泛的使用,比如Spring AOPRPC、快取等。在 Java 中,根據代理的建立時期,可以將代理模式分為靜態代理和動態代理,下面就來分別闡述。

二、代理模式實現

動態代理和靜態代理的區分就是語言型別是在執行時檢查還是在編譯期檢查。

2.1 靜態代理

靜態代理是指在編譯期,也就是在JVM執行之前就已經獲取到了代理類的位元組碼資訊。即Java原始碼生成.class檔案時期:

由於在JVM執行前代理類和真實主題類已經是確定的,因此也被稱為靜態代理。

在實際使用中,通常需要定義一個公共介面及其方法,被代理物件(目標物件)與代理物件一起實現相同的介面或繼承相同的父類。其實現程式碼就是第一節中的程式碼。

2.2 動態代理

動態代理,也就是在JVM執行時期動態構建物件和動態呼叫代理方法。

常用的實現方式是反射。反射機制是指程式在執行期間可以訪問、檢測和修改其本身狀態或行為的一種能力,使用反射我們可以呼叫任意一個類物件,以及其中包含的屬性及方法。比如JDK Proxy。

此外動態代理也可以通過ASM(Java 位元組碼操作框架)來實現。比如CGLib。

2.2.1 JDK Proxy

這種方式是JDK自身提供的一種方式,它的實現不需要引用第三方類,只需要實現InvocationHandler介面,重寫invoke()方法即可。程式碼實現如下所示:

public class ProxyExample {

    static interface Car {
        void running();
    }
    static class Bus implements Car {
        @Override
        public void running() {
            System.out.println("bus is running");
        }
    }
    static class Taxi implements Car {
        @Override
        public void running() {
            System.out.println("taxi is runnig");
        }
    }
	//核心部分 JDK Proxy 代理類
    static class JDKProxy implements InvocationHandler {
        private Object target;

        public Object getInstance(Object target) {
            this.target = target;
            //獲得代理物件
            return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
         }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object result = method.invoke(target, args);
            return result;
        }
    }

    public static void main(String[] args) {
        JDKProxy jdkProxy = new JDKProxy();
        Car instance = (Car) jdkProxy.getInstance(new Taxi());
        instance.running();
    }
}

動態代理的核心是實現Invocation介面,我們再看看Invocation介面的原始碼:

public interface InvocationHandler {

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

實際上是通過invoke()方法來觸發代理的執行方法。最終使得實現Invocation介面的類具有動態代理的能力。

動態代理的好處在於不需要和靜態代理一樣提前寫好公共的代理介面,只需要實現Invocation介面就可擁有動態代理能力。下面我們再來看看 CGLib 是如何實現的

2.2.2 CGLib

CGLib 動態代理採取的是建立目標類的子類的方式,通過子類化,我們可以達到近似使用被呼叫者本身的效果。其實現程式碼如下所示:

public class CGLibExample {

    static class car {
        public void running() {
            System.out.println("car is running");
        }
    }

    static class CGLibProxy implements MethodInterceptor {
        private Object target;

        public Object getInstance(Object target) {
            this.target = target;
            Enhancer enhancer = new Enhancer();
            //設定父類為例項類
            enhancer.setSuperclass(this.target.getClass());
            //回撥方法
            enhancer.setCallback(this);
            //建立代理物件
            return enhancer.create();
        }
        @Override
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
            Object result = methodProxy.invokeSuper(o, objects);
            return result;
        }
    }

    public static void main(String[] args) {
        CGLibProxy cgLibProxy = new CGLibProxy();
        car instance = (car) cgLibProxy.getInstance(new car());
        instance.running();
    }
}

從程式碼可以看出CGLib 也是通過實現代理器的介面,然後再呼叫某個方法完成動態代理,不同的是CGLib在初始化被代理類時,是通過Enhancer物件把代理物件設定為被代理類的子類來實現動態代理:

Enhancer enhancer = new Enhancer();
//設定父類為例項類
enhancer.setSuperclass(this.target.getClass());
//回撥方法
enhancer.setCallback(this);
//建立代理物件
return enhancer.create();

2.2.3 JDK Proxy 和 CGLib 的區別

  1. 來源:JDK Proxy 是JDK 自帶的功能,CGLib 是第三方提供的工具
  2. 實現:JDK Proxy 通過攔截器加反射的方式實現;CGLib 基於ASM實現,效能比較高
  3. 介面:JDK Proxy 只能代理繼承介面的類,CGLib 不需要通過介面來實現,它是通過實現子類的方式來完成呼叫

三、代理模式的應用場景

3.1 MapperProxyFactory

在MyBatis 中,也存在著代理模式的使用,比如MapperProxyFactory。其中的newInstance()方法就是生成一個具體的代理來實現功能,程式碼如下:

public class MapperProxyFactory<T> {
  private final Class<T> mapperInterface;
    
  private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();
    
  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  public Class<T> getMapperInterface() {
    return mapperInterface;
  }

  public Map<Method, MapperMethodInvoker> getMethodCache() {
    return methodCache;
  }

  // 建立代理類
  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
}

3.2 Spring AOP

代理模式最常使用的一個應用場景就是在業務系統中開發一些非功能性需求,比如監控、統計、鑑權、限流、事務、日誌等。將這些附加功能與業務功能解耦,放在代理類中統一處理,讓程式設計師只需要關注業務方面的開發。而Spring AOP 的切面實現原理就是基於動態代理

Spring AOP 的底層通過上面提到的 JDK Proxy 和 CGLib動態代理機制,為目標物件執行橫向織入。當Bean實現了介面時, Spring就會使用JDK Proxy,在沒有實現介面時就會使用 CGLib。也可以在配置中強制使用 CGLib:

<aop:aspectj-autoproxy proxy-target-class="true"/>

3.3 RPC 框架的封裝

RPC 框架的實現可以看作成是一種代理模式,通過遠端代理、將網路同步、資料編解碼等細節隱藏起來,讓客戶端在使用 RPC 服務時,不必考慮這些細節。

參考資料

http://c.biancheng.net/view/1359.html

https://kaiwu.lagou.com/course/courseInfo.htm?courseId=59#/detail/pc?id=1768

https://time.geekbang.org/column/article/7489

https://time.geekbang.org/column/article/201823

《Java 重學設計模式》

《大話設計模式》

相關文章