設計模式之動態代理(dynamic proxy)

1 動態代理與靜態代理

我們從上一篇設計模式之代理模式一文中已經知道,在代理模式中代理對象和被代理對象一般實現相同的接口,調用者與代理對象進行交互。代理的存在對於調用者來說是透明的,調用者看到的只是接口。這就是傳統的代理模式靜態代理的特點。

那麼傳統的靜態代理模式有什麼問題呢?如果需要代理的類只有一個,那麼靜態代理沒什麼問題,如果有很多類需要代理呢,用靜態代理的話就需要爲每一個類創建一個代理類,顯然這麼做太過繁瑣也容易出錯。爲此,JDK 5引入的動態代理機制,允許開發人員在運行時刻動態的創建出代理類及其對象。也就是說,我們不用爲每個類再單獨創建一個代理對象了。

比如Spring的aop框架,它可以通過簡單的配置,在不新增、修改任何業務邏輯代碼情況下,動態的給我們的業務邏輯增加諸如日誌打印、事務處理、異常處理等,這就是利用的動態代理機制。

2 動態代理的作用

  • 數據庫連接以及事物管理
  • 單元測試中的動態 Mock 對象
  • 自定義工廠與依賴注入(DI)容器之間的適配器
  • 類似 AOP 的方法攔截器
  • 日誌、緩存等業務增強
  • Java RMI遠程通信
  • 各種訪問控制器、驗證器
  • … …

3 動態代理的原理

動態代理主要是利用了Java的反射機制。

4 動態代理類的創建

要創建一個動態代理,只需要利用Java API提供的兩個類:

  • java.lang.reflect.InvocationHandler: 這是調用處理器接口,它自定義了一個 invoke() 方法,我們就在這個方法裏觸發代理對象自己的方法,你可以在它的前後增加我們自己的增強方法。
  • java.lang.reflect.Proxy: 這是 Java 動態代理機制的主類,它提供了一組靜態方法來爲一組接口動態地生成代理類及其對象,也就是動態生成代理對象的方法。

每個代理類的對象都會關聯一個表示內部處理邏輯的InvocationHandler接口的實現。當使用者調用了代理對象所代理的接口中的方法的時候,這個調用的信息會被傳遞給InvocationHandlerinvoke()方法。在 invoke()方法的參數中可以獲取到代理對象、方法對應的Method對象和調用的實際參數。invoke()方法的返回值被返回給使用者。這種做法實際上相 當於對方法調用進行了攔截。熟悉AOP的人對這種使用模式應該不陌生。但是這種方式不需要依賴AspectJ等AOP框架。

4.1 創建一個代理

我們可以通過Proxy.newProxyInstance()方法來動態的創建一個代理。這個方法有3個參數:

1. ClassLoader :負責加載動態代理類
2. 接口數組
3. InvocationHandler:把方法調用轉到代理上

Proxy類動態創建代理類:

InvocationHandler handler = new MyInvocationHandler();
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
                        MyInterface.class.getClassLoader(),
                        new Class[] { MyInterface.class },
                        handler);

在執行完這段代碼之後,變量proxy 包含一個 MyInterface 接口的的動態實現。所有對 proxy 的調用都被轉向到實現了 InvocationHandler 接口的 handler 上。有關 InvocationHandler 的內容會在下一段介紹。

4.3 關於InvocationHandler接口

在前面提到了當你調用Proxy.newProxyInstance()方法時,你必須要傳入一個InvocationHandler接口的實現。所有對動態代理對象的方法調用都會被轉向到InvocationHandler接口的實現上,下面是 InvocationHandler 接口的定義:

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

傳入invoke()方法中的proxy參數是實現要代理接口的動態代理對象。通常你是不需要他的。invoke()方法中的Method對象參數代表了被動態代理的接口中要調用的方法,從這個method對象中你可以獲取到這個方法名字,方法的參數,參數類型等等信息。Object數組參數包含了被動態代理的方法需要的方法參數。注意:原生數據類型(如int,long等等)方法參數傳入等價的包裝對象(如Integer, Long等等)。

5 說再多不如舉個實例

比如我們有兩個業務,要爲這兩個業務添加日誌打印功能。如果是靜態代理,那麼就需要分別爲每個業務類寫一個代理類,而如果用動態代理,只需要實現一個日誌打印功能的handler即可,完全不需要自己再單獨寫代理類,下面我們具體看一下這個例子。

5.1 準備兩個業務接口及其實現

接口A和接口B:

public interface SubjectA {
   public void setUser(String name,String password);
}

public interface SubjectB {
   public void sayHello(String name);
}

接口A和接口B的實現:

public class RealSubjectA implements SubjectA {
   public void setUser(String name,String password){
      System.out.println("-------------set user,name:"+name+" password:"+password+"-------------");
   }
}

public class RealSubjectB implements SubjectB{
   public void sayHello(String name) {
      System.out.println("--------------say hello:"+name+"-------------");
   }
}

5. 2 寫一個日誌打印的handler

/**
 * 日誌打印handler,打印調用代理對象的方法及其參數值
 * **/
public class LogHandler implements InvocationHandler{
   private Object proxied;
   LogHandler(Object proxied){
      this.proxied=proxied;
   }
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      System.out.println("begin to invoke method:"+method.getName()+" params:"+ Arrays.toString(args));
      Object result=method.invoke(proxied,args);
      System.out.println("invoke "+method.getName()+" end");
      return result;
   }
}

5.3 最後用Proxy類生產動態代理對象

public class TestDynamicProxy {
   public static void main(String[] args) {
  RealSubjectA realA = new RealSubjectA();
  SubjectA proxySubjectA = (SubjectA) Proxy.newProxyInstance(SubjectA.class.getClassLoader(),
        new Class[]{SubjectA.class},
        new LogHandler(realA));//生成一個業務A的動態代理對象
  RealSubjectB realB = new RealSubjectB();
  SubjectB proxySubjectB = (SubjectB) Proxy.newProxyInstance(SubjectB.class.getClassLoader(),
        new Class[]{SubjectB.class},
        new LogHandler(realB));//生成一個業務B的動態代理對象
  proxySubjectA.setUser("heaven","123456");
  proxySubjectB.sayHello("heaven");
   }
}

運行結果

begin to invoke method:setUser params:[heaven, 123456]
-------------set user,name:heaven password:123456-------------
invoke setUser end
begin to invoke method:sayHello params:[heaven]
--------------say hello:heaven-------------
invoke sayHello end

結果說明

1. 通過動態代理,我們的業務邏輯沒有做任何修改便實現了日誌打印功能,實現瞭解耦(這其實就是一個aop編程的例子)
2. 我們沒有爲每個業務單獨去寫代理類,代理的代碼量不會因爲業務增加而龐大

5.4 數據庫連接以及事物管理

Spring 框架中有一個事物代理可以讓你提交/回滾一個事物,如果用動態代理的話,其方法調用序列如下:

web controller --> proxy.execute(...);
proxy --> connection.setAutoCommit(false);
proxy --> realAction.execute();
realAction does database work
proxy --> connection.commit();
發佈了98 篇原創文章 · 獲贊 1135 · 訪問量 154萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章