Antony Lee的技術(shù)之路
步步為營 |
一、基本概念
所謂動態(tài)代理,基本上是如下場景:假設(shè)我有個接口IHelloWorld
問題是,現(xiàn)在,我打算為HelloWorldImpl增強(qiáng)功能,需要在調(diào)用sayHello方法前后各執(zhí)行一些操作。在有些情況下,你無法修改HelloWorldImpl的源代碼,那怎么辦呢?
從道理上來說,我們可以攔截對HelloWorldImpl對象里sayHello()函數(shù)的調(diào)用。也就是說,每當(dāng)有代碼調(diào)用sayHello函數(shù)時,我們都把這種調(diào)用請求攔截下來之后,做自己想做的事情。
那怎么攔截呢?
首先,需要開發(fā)一個InvocationHandler。這個東東表示的是,你攔截下函數(shù)調(diào)用之后,究竟想干什么。InvocationHandler是一個接口,里面的聲明的函數(shù)只有一個:
Object invoke(Object proxy, Method method, Object[] args) throws Throwable
這個函數(shù)表示一次被攔截的函數(shù)調(diào)用。因此,proxy表示這個被攔截的調(diào)用,原本是對哪個對象調(diào)用的;method表示這個被攔截的調(diào)用,究竟是調(diào)用什么方法;args表示這個被攔截的調(diào)用里,參數(shù)分別是什么。
我們下面寫一個攔截器,讓他在函數(shù)調(diào)用之前和之后分別輸出一句話。
有了這個Handler之后,下面要做的,就是把這個Handler和一個IHelloWorld類型的對象裝配起來。重點的函數(shù)只有一個,那就是java.lang.reflect.Proxy類中的一個靜態(tài)工廠方法:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
這個方法返回一個對象,我們稱返回的對象為代理對象(proxy)。
而后,我們就不把真正的原對象暴露給外接,而使用這個代理對象。這個代理對象接受對源對象的一切函數(shù)調(diào)用(也就是把所有調(diào)用都攔截了),然后根據(jù)我們寫的InvocationHandler,來對函數(shù)進(jìn)行處理。
產(chǎn)生代理對象的過程,我把它理解成一個裝配的過程:由源對象、源對象實現(xiàn)的接口、InvocationHandler裝配產(chǎn)生一個代理對象。
相應(yīng)的測試代碼如下:
利用ant編譯運行的結(jié)果:
[java] ################################
[java] method name : sayHello
[java] Do Before
[java] Hello, World
[java] Do After
[java] ################################
二、更多理解
我們看產(chǎn)生代理對象的newProxyInstance函數(shù)的聲明:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
這個函數(shù)的第一個參數(shù)是ClassLoader,第三個參數(shù)是InvocationHandler,基本都沒什么問題。
第二個參數(shù)是一個Class類型的數(shù)組,名字叫interfaces,表示的是產(chǎn)生的動態(tài)代理對象實現(xiàn)的接口。
仔細(xì)想想,有兩個問題。第一,產(chǎn)生一個代理對象,需要源對象么?第二,我能不能產(chǎn)生一個動態(tài)代理對象,來實現(xiàn)源對象沒有實現(xiàn)的接口?
第一個問題和第二個問題其實是一致的。我們完全可以脫離源對象,而直接產(chǎn)生一個代理對象,也可以利用動態(tài)代理,讓源對象實現(xiàn)更多的接口,為源對象增強(qiáng)功能。
例如,假設(shè)我們希望讓源對象實現(xiàn)java.io.Closeable接口,則首先修改一下我們的Handler的invoke方法,讓他在獲取colse方法時,不要傳遞給源對象(因為源對象沒有實現(xiàn)該方法):
然后,我們在裝配的過程中,改變一下參數(shù),并強(qiáng)轉(zhuǎn)之后調(diào)用一下close方法:
ant運行結(jié)果:
[java] ################################
[java] method name : close
[java] Do Before
[java] I got the close() method!
[java] Do After
[java] ################################
三、更多的代理~
我們現(xiàn)在能夠讓sayHello()函數(shù)執(zhí)行之前和之后,輸出一些內(nèi)容了。那如果我還想在裝配一個Handler呢?
最簡單的方法:
和人討論設(shè)計模式的時候,看到這樣一句話:
大阿亮<yighter@qq.com> 22:24:40
java擴(kuò)展功能就是繼承和組合。肯定結(jié)構(gòu)都很相似。模式思想都是從解決問題背景和目的來區(qū)分的。
恍然大悟。原來,很多情況下,所謂設(shè)計模式,是對同一種技術(shù)、實現(xiàn)的不同角度的理解。
所以,設(shè)計無所謂好壞,只要能解決問題的,就是好設(shè)計。至于所謂“強(qiáng)耦合”,“Bad Smell”,本質(zhì)上是因為采用這些設(shè)計無法解決問題(就是無法快速應(yīng)對需求變化)。
“不管黑貓白貓,只要能抓住耗子,就是好貓”,這句話蘊(yùn)含著深刻的設(shè)計思想。
在啃《The Java Programming Language 4th Edition》時看到的一個小知識點。先描述一下問題。
一個類中,靜態(tài)初始代碼塊中的代碼會在類加載時自動運行。考慮下面這種情況:
ClassA定義了靜態(tài)初始代碼塊,其中調(diào)用了ClassB的一個方法m(靜態(tài)非靜態(tài)均可)。而在ClassB的m方法中,又使用了ClassA類的信息。則,當(dāng)虛擬機(jī)在沒有ClassB類的情況下,加載ClassA類時,會遇到這樣一條線索:
加載ClassA --> 調(diào)用ClassA的靜態(tài)初始化代碼塊 --> 調(diào)用ClassB的m方法 --> 加載ClassB --> 使用ClassA的信息
注意這條線索的一頭一尾,我們要在對ClassA還沒完成加載時,使用ClassA的信息!
示例代碼:
首先,編譯器無法解決這個問題,因為在編譯ClassA類時,無法找到ClassB的代碼,也就無法檢查是否存在靜態(tài)初始化代碼塊循環(huán)問題。事實上,上述程序在java中是能夠編譯通過的。
其次,運行時的結(jié)果。當(dāng)程序運行到第3行時,JVM加載ClassA類,此時,會執(zhí)行ClassA類中的靜態(tài)初始化代碼塊。當(dāng)程序執(zhí)行到第12行時,調(diào)用ClassB的print方法,此時,程序跳轉(zhuǎn)到18行。
關(guān)鍵在這兒:此時的print方法需要調(diào)用ClassA的信息,并打印其靜態(tài)屬性。而ClassA的信息正在加載過程中。此時,JVM采用的策略是:在print方法中使用ClassA不完整的信息。在print方法中ClassA的信息,是在第12行對ClassB.print方法之前的信息。此時ClassA.a1已經(jīng)被賦值為10,而ClassA.a2還未被賦值,它的值為默認(rèn)值。因此,最后打印出的是10、0。
| |||||||||
日 | 一 | 二 | 三 | 四 | 五 | 六 | |||
---|---|---|---|---|---|---|---|---|---|
29 | 30 | 1 | 2 | 3 | 4 | 5 | |||
6 | 7 | 8 | 9 | 10 | 11 | 12 | |||
13 | 14 | 15 | 16 | 17 | 18 | 19 | |||
20 | 21 | 22 | 23 | 24 | 25 | 26 | |||
27 | 28 | 29 | 30 | 31 | 1 | 2 | |||
3 | 4 | 5 | 6 | 7 | 8 | 9 |