Java Reflection(十一):動態代理
原文地址 作者: Jakob Jenkov 譯者:葉文海([email protected])
內容索引
建立代理
InvocationHandler介面
利用Java反射機制你可以在執行期動態的建立介面的實現。java.lang.reflect.Proxy類就可以實現這一功能。這個類的名字(譯者注:Proxy意思為代理)就是為什麼把動態介面實現叫做動態代理。動態的代理的用途十分廣泛,比如資料庫連線和事物管理(transaction management)還有單元測試時用到的動態mock物件以及AOP中的方法攔截功能等等都使用到了動態代理。
建立代理
你可以通過使用Proxy.newProxyInstance()方法建立動態代理。newProxyInstance()方法有三個引數:
1、類載入器(ClassLoader)用來載入動態代理類。
2、一個要實現的介面的陣列。
3、一個InvocationHandler把所有方法的呼叫都轉到代理上。
如下例:
InvocationHandler handler = new MyInvocationHandler(); MyInterface proxy = (MyInterface) Proxy.newProxyInstance( MyInterface.class.getClassLoader(), new Class[] { MyInterface.class }, handler);
在執行完這段程式碼之後,變數proxy包含一個MyInterface介面的的動態實現。所有對proxy的呼叫都被轉向到實現了InvocationHandler介面的handler上。有關InvocationHandler的內容會在下一段介紹。
InvocationHandler介面
在前面提到了當你呼叫Proxy.newProxyInstance()方法時,你必須要傳入一個InvocationHandler介面的實現。所有對動態代理物件的方法呼叫都會被轉向到InvocationHandler介面的實現上,下面是InvocationHandler介面的定義:
public interface InvocationHandler{ Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }
下面是它的實現類的定義:
public class MyInvocationHandler implements InvocationHandler{ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //do something "dynamic" } }
傳入invoke()方法中的proxy引數是實現要代理介面的動態代理物件。通常你是不需要他的。
invoke()方法中的Method物件引數代表了被動態代理的介面中要呼叫的方法,從這個method物件中你可以獲取到這個方法名字,方法的引數,引數型別等等資訊。關於這部分內容可以查閱之前有關Method的文章。
Object陣列引數包含了被動態代理的方法需要的方法引數。注意:原生資料型別(如int,long等等)方法引數傳入等價的包裝物件(如Integer, Long等等)。
常見用例
動態代理常被應用到以下幾種情況中
- 資料庫連線以及事物管理
- 單元測試中的動態Mock物件
- 自定義工廠與依賴注入(DI)容器之間的介面卡
- 類似AOP的方法攔截器
資料庫連線以及事物管理
web controller --> proxy.execute(...); proxy --> connection.setAutoCommit(false); proxy --> realAction.execute(); realAction does database work proxy --> connection.commit();
單元測試中的動態Mock物件
Butterfly Testing工具通過動態代理來動態實現樁(stub),mock和代理類來進行單元測試。在測試類A的時候如果用到了介面B,你可以傳給A一個實現了B介面的mock來代替實際的B介面實現。所有對介面B的方法呼叫都會被記錄,你可以自己來設定B的mock中方法的返回值。
而且Butterfly Testing工具可以讓你在B的mock中包裝真實的B介面實現,這樣所有呼叫mock的方法都會被記錄,然後把呼叫轉向到真實的B介面實現。這樣你就可以檢查B中方法真實功能的呼叫情況。例如:你在測試DAO時你可以把真實的資料庫連線包裝到mock中。這樣的話就與真實的情況一樣,DAO可以在資料庫中讀寫資料,mock會把對資料庫的讀寫操作指令都傳給資料庫,你可以通過mock來檢查DAO是不是以正確的方式來使用資料庫連線,比如你可以檢查是否呼叫了connection.close()方法。這種情況是不能簡單的依靠呼叫DAO方法的返回值來判斷的。
自定義工廠與依賴注入(DI)容器之間的介面卡
依賴注入容器Butterfly Container有一個非常強大的特性可以讓你把整個容器注入到這個容器生成的bean中。但是,如果你不想依賴這個容器的介面,這個容器可以適配你自己定義的工廠介面。你僅僅需要這個介面而不是介面的實現,這樣這個工廠介面和你的類看起來就像這樣:
public interface IMyFactory { Bean bean1(); Person person(); ... }
public class MyAction{ protected IMyFactory myFactory= null; public MyAction(IMyFactory factory){ this.myFactory = factory; } public void execute(){ Bean bean = this.myFactory.bean(); Person person = this.myFactory.person(); } }
當MyAction類呼叫通過容器注入到構造方法中的IMyFactory例項的方法時,這個方法呼叫實際先呼叫了IContainer.instance()方法,這個方法可以讓你從容器中獲取例項。這樣這個物件可以把Butterfly Container容器在執行期當成一個工廠使用,比起在建立這個類的時候進行注入,這種方式顯然更好。而且這種方法沒有依賴到Butterfly Container中的任何介面。
類似AOP的方法攔截器
Spring框架可以攔截指定bean的方法呼叫,你只需提供這個bean繼承的介面。Spring使用動態代理來包裝bean。所有對bean中方法的呼叫都會被代理攔截。代理可以判斷在呼叫實際方法之前是否需要呼叫其他方法或者呼叫其他物件的方法,還可以在bean的方法呼叫完畢之後再呼叫其他的代理方法。