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
接口的實現。當使用者調用了代理對象所代理的接口中的方法的時候,這個調用的信息會被傳遞給InvocationHandler
的invoke()
方法。在 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();