靜態和動態代理模式

前言

代理模式,也稱委託模式,是結構型設計模式之一,何爲代理呢? 在平常生活中就好比叫朋友替你拿個快遞,叫朋友替你作一下做業,叫朋友替你買點東西等等,這個朋友就是你的代理,你把事情委託你的朋友作了,一樣在代碼的世界中也存在代理,並且在你之後閱讀到更多的設計模式時,你會發現不少的設計模式中也有代理模式的影子,代理模式是一個很是重要的設計模式,代理模式分爲靜態代理和動態代理,本文將會經過一個簡單的例子講解靜態代理,而後引出動態代理,而且深刻的探討一下動態代理的實現原理。java

代理模式的定義

爲其餘對象提供一種代理以控制這個對象的訪問。git

使用場景

靜態代理和動態代理都適用於如下場景:github

一、當不想訪問某個對象或訪問某個對象存在困難時,就能夠爲這個對象建立一個代理,經過代理來間接的訪問這個對象;編程

二、若是原始對象有不一樣的訪問權限,可使用代理控制對原始對象的訪問,保護原始對象;設計模式

三、在訪問原始對象時執行一些本身的附加操做;ide

四、爲某個對象在不一樣的內存地址空間提供局部代理,使得系統能夠將服務端的實現隱藏,客戶端沒必要考慮服務端的存在,例如Android中的Binder。函數

下面先簡單的講解一下靜態代理。源碼分析

靜態代理

一、類圖

角色介紹:學習

  • Subject --- 抽象主題類,定義了代理對象和真實對象的共同接口方法,既能夠是接口也能夠是抽象類。
  • RealSubject --- 真實主題類,該類能夠被稱爲被委託類或被代理類,該類定義了代理對象所表示的真實對象,實現了Subject接口,而Client端經過代理類間接的調用的真實主題類中的方法,由其執行真正的業務邏輯。
  • ProxySubject --- 代理類,該類也被稱爲委託類或代理類,該類中持有一個真實主題類的引用,一樣實現了Subject接口,在其實現的接口方法中調用真實主題類中相應的接口方法,以此起到代理的做用。
  • Client --- 客戶端,使用代理。

這個類圖就是靜態代理的結構,在下面小明經過中介租房的例子中會有體現這幾個角色的做用,而動態代理的類圖結構與靜態代理的稍有不一樣,將會在動態代理那裏講到,但動態代理和靜態代理的總體思想是相同的,咱們還須要注意一下下面會提到的一些近義詞:ui

Subject = 公共接口;

ProxySubject = 代理對象 = 代理類 = 委託類 = 代理人;

RealSubject = 真實對象 = 被代理類 = 被委託類 = 被代理人;

在根據不一樣的上下文時我會用不一樣的詞表示Subject 、ProxySubject 和 RealSubject,還有委託和代理這個兩個動詞要根據上下文含義理解。

二、使用靜態代理

使用靜態代理的基本步驟:

一、定義代理對象和真實對象的公共接口;

二、真實對象實現公共接口中的方法;

三、代理對象實現公共接口中的方法,並把方法的邏輯轉發給真實對象。

咱們經過小明買房的這個例子來說解靜態代理,小明想要在大城市租房,可是他平時很忙沒有時間去看房,因而他就找到一個房產中介,把本身的租房意願告訴房產中介,讓房產中介來替本身解決租房問題,很明顯房產中介就是代理人,小明就是被代理的人。

咱們用靜態代理來實現這個過程,首先定義一個租房步驟的公共接口:

//租房步驟公共接口,即Subject角色
public interface IRoom {
    void seekRoom();//找房
    void watchRoom();//看房
    void room();//給錢租房
    void finish();//完成租房
}
複製代碼

4個步驟完成租房,很簡單,而後咱們定義具體的想要租房的人即小明:

//被代理人,想要租房的小明,即RealSubject角色
public class XiaoMing implements IRoom {
    
    @Override
    public void seekRoom() {
        System.out.println("找房");
    }

    @Override
    public void watchRoom() {
        System.out.println("看房");
    }

    @Override
    public void room() {
        System.out.println("給錢租房");
    }

    @Override
    public void finish() {
        System.out.println("完成租房");
    }
}
複製代碼

該類實現了IRoom接口,實現了其中的具體邏輯,可是小明並不會本身去打租房,他委託房產中介去作,因此這裏定義一個房產中介:

//代理人,房產中介,即ProxySubject角色
public class RoomAgency implements IRoom {
    
    private IRoom mRoom;//持有一個被代理人(小明)的引用
    
    public RoomAgency(IRoom room){
        this.mRoom = room;
    }
    
    @Override
    public void seekRoom() {
        mRoom.seekRoom();
    }

    @Override
    public void watchRoom() {
        mRoom.watchRoom();
    }

    @Override
    public void room() {
        mRoom.room();
    }

    @Override
    public void finish() {
        mRoom.finish();
    }
}
複製代碼

在該類中會持有一個被代理人的引用,在這裏指小明,能夠看到房產中介所執行的方法的實質就是簡單的調用被代理人中的方法,下面來看看Client中具體的執行關係:

//客戶端,即Client角色
public class Client {
    public static void main(String[] args){
        //小明想租房
        XiaoMing xiaoMing = new XiaoMing();
        //找一個代理人,房產中介
        RoomAgency roomAgency = new RoomAgency(xiaoMing);
        //房產中介找房
		roomAgency.watchRoom();
        //房產中介看房
        roomAgency.seekRoom();
        //房產中介租房
        roomAgency.room();
        //房產中介完成租房
        roomAgency.finish();
    }
}
複製代碼

輸出結果:

看房
找房
給錢租房
完成租房
複製代碼

上面就是傻瓜式的過程,一看就懂,房產中介代理了小明的找房、看房、租房等過程,能夠看到靜態代理模式仍是很簡單,就是一種委託機制,真實對象將方法委託給代理對象,那麼房產中介繼續代理其餘人能夠嗎? 能夠的,好比XiaoHong也想租房,咱們再定義一個XiaoHong實現IRoom接口,並在Client中給房產中介RoomAgency代理就行。

三、缺點

可是若是小明是想要買房而不是租房,這時房產中介還能知足小明的需求嗎?很顯然不能了,由於這個房產中介它只有替人租房的能力,沒有替人買房的能力,這時就須要更換租房接口爲買房接口,再定義一個專門買房的的房產中介,你會發現我每次更換接口,都須要更換代理類,這就是靜態模式的缺點,只能爲給定接口下的實現類作代理,若是接口不一樣就須要定義不一樣的代理類,隨着系統的複雜度增長,就會很難維護這麼多代理類和被代理類之間的關係,這時動態代理就應運而生,當須要頻繁的更換接口,更換代理類時,採用動態代理是一個更好的選擇,動態代理能夠經過一個代理類來代理N多個被代理類,它在更換接口時,不須要從新定義代理類,由於動態代理不須要根據接口提早定義代理類,它把代理類的建立推遲到代碼運行時來完成

四、與動態代理的區別

咱們先來複習一下class文件的加載,咱們編寫的.Java文件通過javac編譯以後,會產生.class文件,這種.class文件是二進制文件,裏面的內容是隻有JVM可以識別,在代碼運行以前,JVM會讀取.class文件,解析.class文件內的信息,取出二進制數據,加載進內存中,從而生成對應的Class對象。

而靜態代理和動態代理最主要的區別就是:靜態代理在咱們的代碼運行以前,代理類的.class文件就已經存在,例如上述的RoomAgency.java,在通過javac編譯以後,就會變成RoomAgency.class;而動態代理則與靜態代理相反,在代碼運行以前不存在代理類的.class文件,在代碼運行時才動態的生成代理類。

下面來說解動態代理,並經過動態代理從新實現一遍小明買房的例子。

動態代理

一、類圖

這個就是動態代理的大概類圖結構,其中Subject 、ProxySubject、 RealSubject和Client角色的做用和靜態代理的同樣,這裏就不在累述,與靜態代理相比,多了一個InvocationHandler角色和一個Proxy角色,InvocationHandler是java提供的一個接口,咱們須要定義一個類實現InvocationHandler接口,這裏就叫DynamicProxy角色;Proxy是java提供用於動態生成ProxySubject的一個類,它須要ProxySubject繼承。

咱們看到DynamicProxy在ProxySubject和RealSubject以前起到了中間人的角色,ProxySubject會把事情委託給DynamicProxy來作,而DynamicProxy最終把事情委託給RealSubject來作,能夠這樣說:ProxySubject代理了DynamicProxy,而DynamicProxy代理了RealSubject,其中最重要的一點是ProxySubject是在代碼運行時才動態生成的,這是和靜態代理的最大區別。

接下來簡單介紹一下InvocationHandler接口,Proxy類。

一、InvocationHandler和Proxy的做用

爲了讓咱們更加容易的實現動態代理,java提供了動態代理接口InvocationHandler和動態代理類Proxy供咱們使用,它們都在java.lang.reflect包中,可見動態代理和反射有不可逃脫的關係。

InvocationHandler接口 定義以下:

public interface InvocationHandler {
  /** * 這個方法的含義是:代理對象proxy要調用真實對象的method * @param proxy 代理對象 * @param method 真實對象被調用的方法 * @param args 被調用的方法的參數 */
  Object invoke(Object proxy, Method method, Object[] args)throws Throwable; } 複製代碼

InvocationHandler接口的做用就是在invoke方法中執行真實對象的方法,能夠看到裏面只有一個invoke方法,咱們須要爲真實對象定義一個實現了這個接口中的invoke方法的動態代理類,同時在建立這個動態代理類的實例的時候,咱們還要在方法或構造中傳入真實對象的引用,即InvocationHandler的實現類須要持有真實對象的引用,這樣才能執行真實對象的方法。

Proxy類定義以下:

public class Proxy implements Serializable {
    
    protected InvocationHandler h;//持有一個InvocationHandler類型的引用

    protected Proxy(InvocationHandler h) {
        this.h = h;
    }

    //根據指定的類加載器和接口來獲取代理對象的Class對象
    public static Class<?> getProxyClass(ClassLoader loader, Class... interfaces) throws IllegalArgumentException {
        //...
    }

    //根據指定的類加載器和接口生成代理對象
    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException {
        //...
    }
    
    //...
}

複製代碼

Proxy這個類的做用就是用來動態的建立一個代理對象,它內部會持有一個InvocationHandler引用,在構造中傳入,它提供了許多的方法,可是咱們經常使用的是 getProxyClass方法和newProxyInstance方法:

  • getProxyClass(重點方法):這個方法的做用是在運行時根據.class的結構生成一個代理Class二進制流,並經過傳入的ClassLoader去把代理Class二進制流加載成一個代理Class對象,該代理Class對象繼承Proxy並實現了傳入的第二個參數對應的Interface列表。
  • newProxyInstance(常使用的方法): 這個方法的做用是在運行時根據代理Class對象生成代理對象實例,這個方法中會先調用了getProxyClass方法生成代理Class對象,在獲取到代理Class對象後,根據第三個參數InvocationHandler引用經過反射建立代理對象實例,因此newProxyInstance最終的結果是生成一個代理對象實例,該代理對象會繼承Proxy類並實現給定的接口列表,同時內部持有一個InvocationHandler引用。

以上兩個方法過程如今看不懂沒關係,下面在講解動態代理的源碼分析時還會再分析一遍,咱們一般會使用newProxyInstance方法來生成一個代理對象實例。

三、使用動態代理

使用動態代理的基本步驟以下:

一、定義代理對象和真實對象的公共接口;(與靜態代理步驟相同)

二、真實對象實現公共接口中的方法;(與靜態代理步驟相同)

三、定義一個實現了InvocationHandler接口的動態代理類;

四、經過Proxy類的newProxyInstance方法建立代理對象,調用代理對象的方法。

1和2步驟都是和靜態代理步驟相同的,就不在累述了,和靜態代理相比,少了的一個步驟是:代理對象實現公共接口的方法,由於前面講過代理對象是代碼運行時經過Proxy動態建立的,因此不須要提早編寫代理對象的類;和靜態代理相比,多了的兩個步驟是:三、定義一個實現了InvocationHandler接口的動態代理類和四、經過Proxy類的newProxyInstance方法建立代理對象,調用代理對象的方法,咱們接着靜態代理的小明買房的例子,下面分別講解:

步驟3:咱們須要定義一個動態代理類,它用於執行真實對象的方法:

//實現了InvocationHandler接口的動態代理類
public class DynamicProxy implements InvocationHandler {

    private Object mObject;//真實對象的引用
    
    public DynamicProxy(Object object){
        this.mObject = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //經過反射調用真實對象的方法
        Object result = method.invoke(mObject, args);
        return result;
    }
}
複製代碼

在該類中,咱們聲明瞭一個Object引用,該引用指向真實對象,真實對象在構造函數中傳入,而在invoke方法中經過反射調用真實對象的具體方法,這裏須要注意的是指向真實對象的引用類型最好定義爲Objec類型而不是真實對象的具體類型如XiaoMing,這樣作的好處是,當你要代理另一我的時,例如xiaoHong,我在DynamicProxy的構造函數中只須要傳入xiaoHong引用而不用更改DynamicProxy的類結構,這樣一個DynamicProxy就能夠代理不少人。

接着步驟4:經過Proxy類newProxyInstance方法建立代理對象,調用代理對象的方法,下面是Client端邏輯:

public class Client {
    public static void main(String[] args) {
        //構造一個小明
        IRoom xiaoMing = new XiaoMing();
        //構造一個動態代理
        InvocationHandler dynamicProxy = new DynamicProxy(xiaoMing);
        //獲取被代理類小明的ClassLoader
        ClassLoader classLoader = xiaoMing.getClass().getClassLoader();
        
        //一、經過Proxy類的newProxyInstance方法動態構造一個代理人房產中介
        IRoom roomAgency = (IRoom) Proxy.newProxyInstance(classLoader, new Class[]{IRoom.class}, dynamicProxy);
        
        //調用代理對象的方法
        
        //房產中介找房
        roomAgency.watchRoom();
        //房產中介看房
        roomAgency.seekRoom();
        //房產中介租房
        roomAgency.room();
        //房產中介完成租房
        roomAgency.finish();
    }
}

複製代碼

運行結果和前面的靜態代理一致,就再也不貼出,在介紹Proxy時講過,Proxy的newProxyInstance方法會根據傳入的類加載器動態生成代理對象實例,生成的代理對象會繼承Proxy類並實現傳入的接口列表,這裏的類加載器是小明的ClassLoader,即真實對象的類加載器,而接口列表則是IRoom,傳入的IRoom的Class對象,除了這個兩個參數,還傳入了動態代理類InvocationHandler實例,這樣Proxy類在建立代理對象的實例時就會把這個InvocationHandler引用傳給代理對象,接下來當咱們調用代理對象的方法時,這個方法的處理邏輯就會委託給InvocationHandler實例的invoke方法執行,invoke方法中就會經過反射調用咱們真實對象的方法

下面咱們經過源碼看一下是怎樣生成代理對象以及生成的代理對象是長什麼樣的。

四、源碼分析

咱們看Client的註釋1:

//一、經過Proxy類的newProxyInstance方法動態構造一個代理人房產中介
IRoom roomAgency = (IRoom) Proxy.newProxyInstance(classLoader, new Class[]{IRoom.class}, dynamicProxy);
複製代碼

咱們先看Client的註釋1,Proxy的newProxyInstance方法會根據傳入的類加載器動態生成代理對象實例,咱們點進Proxy的newProxyInstance方法看一下,以下:

//Proxy.java 
private static final Class<?>[] constructorParams = { InvocationHandler.class };

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

	//clone一下傳入的接口列表
    final Class<?>[] intfs = interfaces.clone();
    
    //getProxyClass會把邏輯轉發給getProxyClass0,因此getProxyClass的做用 = getProxyClass0的做用,它們的區別只是一個是public,一個是private的
    //一、調用getProxyClass0,得到一個代理Class對象
    Class<?> cl = getProxyClass0(loader, intfs);

    try {
        
        //constructorParams = InvocationHandler.class
        //二、這裏經過代理Class對象獲取構造參數爲InvocationHandler的Constructor
        final Constructor<?> cons = cl.getConstructor(constructorParams);
        
        //傳入的InvocationHandler引用
        final InvocationHandler ih = h;
        //這個Constructor是protected的,因此要設置爲Public
        if (!Modifier.isPublic(cl.getModifiers())) {
            cons.setAccessible(true);
        }
        
        //三、經過構造參數爲InvocationHandler的Constructor反射建立代理對象實例,並傳入InvocationHandler引用給構造
        return cons.newInstance(new Object[]{h});
    } 
    //...省略異常處理
}
複製代碼

這個方法裏面的流程仍是很簡單的,首先註釋1,調用getProxyClass0,得到一個代理Class對象,getProxyClass0等於前面講過的getProxyClass的做用,以下:

public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) throws IllegalArgumentException{
        //getProxyClass裏面只是調用了getProxyClass0
        return getProxyClass0(loader, interfaces);
}
複製代碼

能夠看到,getProxyClass裏面也是簡單的調用getProxyClass0方法,因此getProxyClass0方法的做用就是我前面講的getProxyClass方法的做用:在運行時根據.class的結構生成一個代理Class二進制流,並經過傳入的ClassLoader去把代理Class二進制流加載成一個代理Class對象,該代理Class對象繼承Proxy並實現了傳入的第二個參數對應的Interface列表。

咱們來看一下這個動態生成的代理Class對象的真實面目,首先在Client的main函數的開頭填入下面的一代碼:

System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
複製代碼

填入後,運行Client的main函數就會在你的idea工做空間下的com/sun/proxy/目錄下生成一個$Proxy0.class文件,這個$Proxy0.class就是動態生成的代理Class對象,即代理對象,以下:

這個.class文件裏面都是JVM才能看懂的二進制,用idea打開,它會自動替你反編譯成.java文件,以下:

public final class $Proxy0 extends Proxy implements IRoom{
  private static Method m1;
  private static Method m3;
  private static Method m4;
  private static Method m2;
  private static Method m5;
  private static Method m6;
  private static Method m0;
  
  //調用父類Proxy的構造函數,傳入InvocationHandler引用
  public $Proxy0(InvocationHandler paramInvocationHandler){
      super(paramInvocationHandler);
  }
  
  //下面四個方法都是實現自IRoom的方法,能夠看到它們只是簡單的調用了父類的h的invoke方法,並把代理對象 $Proxy0實例、要調用的方法method,還有參數傳了進去
  public final void watchRoom(){
    try{
      this.h.invoke(this, m3, null);
      return;
    }
    //...省略異常處理
  }
  
  public final void room(){
    try{
      this.h.invoke(this, m4, null);
      return;
    }
    //...省略異常處理
  }
  
  public final void seekRoom(){
    try{
      this.h.invoke(this, m5, null);
      return;
    }
    //...省略異常處理
  }
    
      
  public final void finish(){
    try{
      this.h.invoke(this, m6, null);
      return;
    }
    //...省略異常處理
  }
  
 //...咱們只關注IRoom接口中的方法,因此我省略了Object中繼承而來的toSting,hashcode方法等,裏面邏輯都同樣,都是調用父類的h的invoke方法

  static{
    try{
      m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
      m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
      m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
      //獲取IRoom接口方法的Method對象
      m3 = Class.forName("com.example.hy.designpatternDemo.proxy.IRoom").getMethod("watchRoom", new Class[0]);
      m4 = Class.forName("com.example.hy.designpatternDemo.proxy.IRoom").getMethod("room", new Class[0]);
      m5 = Class.forName("com.example.hy.designpatternDemo.proxy.IRoom").getMethod("seekRoom", new Class[0]);
      m6 = Class.forName("com.example.hy.designpatternDemo.proxy.IRoom").getMethod("finish", new Class[0]);
      return;
    }
    //...省略異常處理
  }
}
複製代碼

爲了閱讀方便,我省略了無關代碼,能夠看到Proxy類的getProxyClass0方法會替咱們動態生成代理對象$Proxy0.class,這個代理對象會繼承Proxy類,和實現接口列表,而這裏傳入的接口只有IRoom,因此$Proxy0會實現IRoom的方法,這些方法裏面的邏輯都是調用父類的h的invoke方法,父類的h就是InvocationHandler引用,咱們回去看newProxyInstance方法的註釋2和3,你就會發現這個InvocationHandler引用是在經過反射建立$Proxy0實例時在構造中傳入的。

咱們在$Proxy0中還發現了不少Method對象,在$Proxy0的底部的static塊中經過反射獲取到咱們IRoom接口全部方法的Method對象,當咱們調用某個方法時,相應方法的method 和 代理對象$Proxy0實例、還有方法參數一塊兒傳進了父類的h的invoke方法中,因此咱們在invoke方法中就能夠根據method經過反射調用真實對象的相應方法,以下:

//實現了InvocationHandler接口的動態代理類
public class DynamicProxy implements InvocationHandler {

    private Object mObject;//真實對象的引用
    
    public DynamicProxy(Object object){
        this.mObject = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //經過反射調用真實對象的方法
        Object result = method.invoke(mObject, args);
        return result;
    }
}
複製代碼

咱們回到Client的註釋1,因此當咱們調用Proxy類的newProxyInstance方法,這個方法在裏面建立了代理對象,並返回代理對象$Proxy0實例,因此當咱們調用代理對象的方法時,咱們就是在調用$Proxy0相應的方法,這個方法處理邏輯就會委託給InvocationHandler實例的invoke方法執行(代理對象的父類持有InvocationHandler引用),invoke方法中就會經過反射調用咱們真實對象的方法(InvocationHandler的實現類中持有真實對象的引用),這就是整個動態代理的過程。

五、原理

經過使用和源碼分析,相信你們對動態代理有一個更加深刻的瞭解,動態代理的原理就是經過反射機制動態生成代理類,這是由於因爲JVM能夠經過.class文件的二進制信息加載class對象的,那麼,若是咱們在代碼運行時,遵循.class文件的格式和結構,生成相應的二進制數據,而後再把這個二進制數據經過JVM加載成對應的class對象,有了class對象,咱們就能夠在運行時經過反射建立出代理對象的實例,這樣就完成了在代碼運行時,動態的建立一個代理對象的能力,這就是動態代理的原理。

結語

在靜態代理模式中,代理類ProxySubject中的方法,都指定地調用了特定ReadSubject對應的方法;而在動態代理模式中,代理類ProxySubject中每個方法的調用,都會交給InvocationHandler來處理,而InvocationHandler則調用了RealSubject的方法,以上就是我對靜態代理和動態代理的理解,下面用一張表總結本文:

優勢 缺點 區別
動態代理 一、代理類在程序運行時由反射自動生成,無需咱們手動編寫代理類代碼,簡化編程工做
二、一個動態代理類InvocationHandler就能代理多個被代理類,較爲靈活
一、動態代理只能代理實現實現了接口的類,而不能代理實現抽象類的類
二、經過反射調用被代理類的方法,效率低
不須要提早實現接口編寫代理類,在代碼運行時,由JVM來動態的建立代理類
靜態代理 一、代理類做爲客戶端和被代理類之間的中介,起到了保護被代理類的做用
二、經過接口對代理類和被代理類進行解耦,下降了系統的耦合度
一、只能爲給定接口下的實現類作代理,若是接口不同那麼就要從新定義不一樣的代理類,維護複雜
二、因爲在客戶端和被代理類之間增長了代理對象,所以會形成請求的處理速度變慢
須要提早實現接口編寫代理類,在代碼運行以前,代理類的.class文件就已經存在

代理模式應用普遍,在實際開發中要根據實際狀況進行選擇。

本文源碼位置

參考資料:

這是一份全面 & 清晰的動態代理模式(Proxy Pattern)學習指南

相關文章
相關標籤/搜索