1. 程式人生 > >設計模式——動態代理

設計模式——動態代理

文章目錄

1. 引言

動態代理的意義在於生成一個佔位(又稱代理物件),來代理真實物件,從而控制真實物件的訪問,代理的作用就是在真實物件訪問之前或者之後加入對應的邏輯,或者根據其他規則控制是否使用真實物件,所以代理必須實現兩個步驟:

  • 代理物件和真實物件建立代理關係
  • 實現代理物件的代理邏輯方法

他的優點是可以隱藏真實類的實現,可以實現客戶與委託類間的解耦,在不修改真實類程式碼的情況下能夠做一些額外的處理。Java中最常用的動態代理技術有JDK動態代理和CGLIB,JDK必須使用介面而CGLIB不需要。

2. 靜態代理

若代理類在程式執行前就已經存在,那麼這種代理方式被稱為靜態代理,靜態代理中的代理類和真實類會實現同一介面或是派生自相同的父類。下面我們用Vendor類代表生產廠家,BusinessAgent類代表微商代理,來介紹下靜態代理的簡單實現,委託類和代理類都實現了Sell介面,Sell介面的定義如下:

/**
 * 委託類和代理類都實現了Sell介面
 */
public interface Sell { 
    void sell(); 
    void ad(); 
} 

Vendor類的定義如下:

/**
 * 生產廠家
 */
public class Vendor implements Sell { 
    public void sell() { 
        System.out.println("In sell method"); 
    } 
    
    public void ad() { 
        System,out.println("ad method");
    }
} 

代理類BusinessAgent的定義如下:

/**
 * 代理類
 */
public class BusinessAgent implements Sell {
    private Sell vendor;
    
    public BusinessAgent(Sell vendor){
        this.vendor = vendor;
    }

    public void sell() { 
        vendor.sell();
    } 
    
    public void ad() {
        vendor.ad();
    }
} 

從BusinessAgent類的定義我們可以瞭解到,靜態代理可以通過聚合來實現,讓代理類持有一個委託類的引用即可。
下面我們考慮一下這個需求:給Vendor類增加一個過濾功能,只賣貨給大學生。通過靜態代理,我們無需修改Vendor類的程式碼就可以實現,只需在BusinessAgent類中的sell方法中新增一個判斷即可如下所示:

/**
 * 代理類
 */
public class BusinessAgent(){ implements Sell {
    private Sell vendor;
    
    public BusinessAgent(Sell vendor){
        this.vendor = vendor;
    }

    public void sell() {
        if (isCollegeStudent()) {
            vendor.sell();
        }
    } 
    
    public void ad() {
        vendor.ad();
    }
} 

這樣就可以實現客戶與委託類間的解耦,在不修改委託類程式碼的情況下能夠做一些額外的處理。靜態代理的侷限在於執行前必須編寫好代理類。

3. JDK動態代理

靜態代理的代理關係在編譯時就確定了,而動態代理的代理關係是在執行時確定的,代理物件是在程式執行時產生的,而不是編譯期。
對代理物件的所有介面方法呼叫都會轉發到InvocationHandler.invoke()方法,在invoke()方法裡我們可以加入任何邏輯,比如修改方法引數,加入日誌功能、安全檢查功能等,之後我們通過反射的方式執行真正的方法體。

3.1 定義介面

JDK動態代理是java.lang.reflect.*包提供的方式,他必須藉助一個接口才能產生代理物件,所以先定義介面

/**
 * 使用jdk動態代理必須要使用介面
 * @author littlemotor
 *
 */
public interface HelloWorld {
  abstract public void sayHelloWorld();
}

3.2 實現類

public class HelloWorldImp implements HelloWorld{

  @Override
  public void sayHelloWorld() {
    System.out.println("Hello World!");
  }

}

3.3 動態代理繫結和代理邏輯實現

public class JdkProxy implements InvocationHandler{

  //真實物件
  private Object target = null;
  
  /**
   * 建立代理物件和真實物件的代理關係,並返回代理物件
   * @param target
   * @return 代理物件
   */
  public Object bind(Object target) {
    this.target = target;
    return Proxy.newProxyInstance(target.getClass().getClassLoader(),
        target.getClass().getInterfaces(), this);
  }
  
  /**
   *代理方法邏輯
   * @param proxy 代理物件
   * @param method 當前排程物件
   * @param args 當前方法引數
   * @return 代理結果返回
   * @throws Throwable
   */
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println("進入代理邏輯方法");
    System.out.println("呼叫真實物件之前的服務");
    Object obj = method.invoke(target, args);
    System.out.println("呼叫真實物件之後的服務");
    return obj;
  }
}

JDK動態代理總共分為兩大步:

  • 建立代理物件和真實物件的關係
  • 實現代理邏輯方法

3.4 建立代理物件和真實物件的關係

這裡使用bind方法完成,方法裡首先用類的屬性target儲存了真實物件,然後通過如下程式碼建立並生成代理物件

Proxy.newProxyInstance(target.getClass().getClassLoader(),
        target.getClass().getInterfaces(), this);

newProxyInstance方法包含三個引數

  • 第1個是類載入器,我們採用target本身的類載入器
  • 第2個是把生成的動態代理物件下掛到哪些介面下面
  • 第3個是定義實現方法邏輯的代理類,這個this表示當前物件,他必須實現InvocationHandler介面的inoke方法

3.5 實現代理邏輯方法

invoke方法可以實現代理邏輯,invoke有三個引數含義如下:

  • proxy,代理物件,就是bind方法生成的物件
  • method,當天排程的方法
  • args,排程方法的引數
Object obj = method.invoke(target,args);

3.6 main方法

public class JdkProxyMain {
  public static void main(String[] args) {
    JdkProxy jdkProxy = new JdkProxy();
    //System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
    HelloWorld helloWorld = (HelloWorld) jdkProxy.bind(new HelloWorldImp());
    helloWorld.sayHelloWorld();
  }
}

//進入代理邏輯方法
//呼叫真實物件之前的服務
//Hello World!
//呼叫真實物件之後的服務

4. 總結

動態代理真正將程式碼中橫向切面的邏輯剝離了出來,起到程式碼複用的目的。但是動態代理也有缺點,一是它的實現比靜態代理更加複雜也不好理解;二是它存在一定的限制,例如它要求需要代理的物件必須實現了某個介面;三是某些方面也不夠靈活,動態代理會為介面中的宣告的所有方法新增上相同的代理邏輯。當然,這只是JDK動態代理所存在的一些缺陷,動態代理還有另外的實現如使用CGLIB庫,在本文不做介紹,讀者可以自行去了解。

參考:
《Java動態代理》https://juejin.im/post/5ad3e6b36fb9a028ba1fee6a#comment
JDK動態代理系列(勞夫子)https://www.cnblogs.com/liuyun1995/p/8144628.html